PageSourceSearch

https://refbl.ch/bundles/cloudtecadmin/js/shared.js?0310ba1c

js refbl.ch collected 2026-10-09 00:49:27 UTC 46,341 bytes, 1,111 lines download raw bytes

1// Makeshift interface of sorts for javascript, these functions need to be defined in luna.js and public.js.
2// This is suboptimal and only done like this to ensure backwards compatibility. A better solution would be to
3// use events, which are then listened to in the public and luna files separately.
4const mustBeDefinedFunctions = ['initialize_all_form_fields', 'maskElement', 'unmaskElement', 'reset_captcha', 'unmaskForm'];
5mustBeDefinedFunctions.forEach((functionName) => {
6    if (typeof functionName === 'undefined') {
7        throw new Error(`The function ${functionName} must be defined`);
8    }
9});
10
11// This is for JS which is shared by the public and admin layouts.
12function init_richtext() {
13    $('.l-richtext-editor').each(function (i, el) {
14        let toolbar = [
15            ['style', ['bold', 'italic', 'underline',]],
16            ['para', ['ul', 'ol',]],
17        ];
18
19        let settings = {
20            toolbar: toolbar,
21            // We add all classes so the styles work for public (ui-kit) and admin (bootstrap)
22            tableClassName: 'table table-striped uk-table uk-table-striped',
23            lang: locale,
24            callbacks: {
25                // onPaste-callback only gets called when not in codeview
26                onPaste: function (event) {
27                    // Firefox doesn't allow access to clipboard in JS so the purifier is not used here but only after submit
28                    if (navigator.userAgent.indexOf("Firefox") !== -1) {
29                        return;
30                    }
31                    let targetElement = $(event.currentTarget);
32                    // If the user selects text and then pastes, the selection is replaced with the pasted text.
33                    // This line is responsible for deleting the selection before pasting.
34                    $(el).summernote('createRange').deleteContents();
35
36                    // using a promise to wait for the paste event to finish (avoiding race-condition)
37                    Promise.resolve().then(function () {
38                        let clipboardData = event.originalEvent.clipboardData;
39
40                        $.ajax({
41                            method: 'POST',
42                            url: PATH_TO_PURIFY_RTE,
43                            data: {
44                                rte_html: clipboardData.getData('text/html') !== '' ? clipboardData.getData('text/html') : clipboardData.getData('text/plain')
45                            },
46                            success: function (formattedPastedText) {
47                                // The reason we're using the summernote pasteHTML function is because it takes the current
48                                // cursor position into account. There is a side effect where it will remove quite a lot of the
49                                // html, which we generally find pretty cool, since it lessens the work on configuring the html purifier.
50                                // It's important to be aware of this though, since it can lead to unexpected results.
51                                $(el).summernote('pasteHTML', formattedPastedText);
52                            },
53                        });
54                    });
55
56                    // we need to prevent the event so it doesn't paste twice. It's also after the promise so the promise runs.
57                    event.preventDefault();
58                },
59                onDialogShown: function () {
60                    let linkDialog = $('.link-dialog.show.modal'),
61                        textarea = $(this);
62
63                    // By default the close button of the link dialog is just an x
64                    // we replace it with the icon and make sure it closes only this dialog
65                    // and not other modals below it
66                    if (linkDialog.length && $('.close', linkDialog).length) {
67                        let button = $('> .modal-dialog .modal-header button', linkDialog);
68                        button.html('<i aria-hidden="true" class="ki ki-close"></i>');
69                        button.removeAttr('data-dismiss');
70
71                        button.click(function () {
72                            $(this).closest('.modal').modal('hide');
73                        });
74                    }
75
76                    if (linkDialog.length && !$(linkDialog).hasClass('ct-page-and-media-buttons') && (textarea.data('media-text') || textarea.data('page-text'))) {
77                        $(linkDialog).addClass('ct-page-and-media-buttons');
78
79                        $('#ct-form-modal-container > .modal').on('hide.bs.modal', function () {
80                            $('.modal-backdrop').remove();
81                        });
82
83                        $('.note-link-url', linkDialog).parent().append('<a href="javascript:" class="btn btn-light ct-summernote-media-button">' + textarea.data('media-text') + '</a>');
84                        $('.note-link-url', linkDialog).parent().append('<a href="javascript:" class="btn btn-light ct-summernote-page-button">' + textarea.data('page-text') + '</a>');
85
86                        $('> .modal .close', textarea.parent()).click(function () {
87                            $('> .modal', textarea.parent()).modal('hide');
88                        });
89
90                        $('.ct-summernote-media-button').click(function () {
91                            $(' > .modal', textarea.parent()).modal('show', {keyboard: false});
92                            // Loading the standard media selector overlay to select media
93                            loadMediaSelectorUrl(textarea.data('media-uri'), textarea.parent(), false, function (parentContainer, modalBody) {
94                                $('.cloudtec-media-grid .datatable tbody tr', modalBody).click(function () {
95                                    let downloadUri = $('td[data-field="name"] span', this).data('uri');
96
97                                    // If it's inside a translation widget, we need to adjust the link locale
98                                    if (textarea.attr('name').indexOf('[translations]') > -1) {
99                                        let locale = textarea.parents('.ct-translation-pane').data('language');
100
101                                        if (!locale) {
102                                            locale = textarea.attr('name').match(/\[translations\]\[([a-z]{2})]/)[1];
103                                        }
104
105                                        downloadUri = '/' + locale + '/' + downloadUri.slice(4);
106                                    }
107
108                                    $('.note-link-url', linkDialog).val(downloadUri);
109
110                                    if ($('.note-link-text', linkDialog).val()) {
111                                        $('.note-link-btn', linkDialog).prop('disabled', false);
112                                        $('.note-link-btn', linkDialog).removeClass('disabled');
113                                    }
114
115                                    $('> .modal', parentContainer).modal('hide');
116                                });
117                            })
118                        });
119
120                        $('.ct-summernote-page-button').click(function () {
121                            $(' > .modal', textarea.parent()).modal('show', {keyboard: false});
122                            // Loading the standard page selector overlay to select page
123                            loadPageSelectorUrl(textarea.data('page-uri'), textarea.parent(), function (parentContainer, modalBody) {
124                                $('.cloudtec-page-grid .datatable tbody tr', modalBody).click(function () {
125                                    let pageUri = $('td[data-field="title"] span', this).data('uri');
126                                    // If it's inside a translation widget, we need to adjust the link locale
127                                    if (textarea.attr('name').indexOf('[translations]') > -1) {
128                                        let locale = textarea.parents('.ct-translation-pane').data('language');
129
130                                        if (!locale) {
131                                            locale = textarea.attr('name').match(/\[translations\]\[([a-z]{2})]/)[1];
132                                        }
133
134                                        pageUri = '/' + locale + '/' + pageUri.slice(4);
135                                    }
136
137                                    $('.note-link-url', linkDialog).val(pageUri);
138
139                                    if ($('.note-link-text', linkDialog).val()) {
140                                        $('.note-link-btn', linkDialog).prop('disabled', false);
141                                        $('.note-link-btn', linkDialog).removeClass('disabled');
142                                    }
143
144                                    $('> .modal', parentContainer).modal('hide');
145                                });
146                            })
147                        });
148                    }
149                },
150                onImageUpload: function (data) {
151                },
152                onChange: function(changedText, $textarea) {
153                    $(".l-textarea-counter .l-character-count", $textarea.parents(".note-editor").first().parent()).text($textarea.text().length);
154                }
155            }
156        };
157
158        if ($(el).data('enable-images')) {
159            delete settings.callbacks.onImageUpload;
160        }
161
162        if ($(el).data('enable-hr')) {
163            toolbar.push(['para', ['hr']]);
164        }
165
166        if ($(el).data('enable-links')) {
167            toolbar.push(['para', ['link']]);
168        }
169
170        if ($(el).data('enable-fullscreen')) {
171            toolbar.push(['style', ['fullscreen']]);
172        }
173
174        if ($(el).data('enable-tables')) {
175            toolbar.push(['table', ['table']]);
176        }
177
178        if ($(el).data('enable-codeview')) {
179            toolbar.push(['style', ['codeview']]);
180        }
181
182        if ($(el).data('enable-styleset')) {
183            toolbar.push(['style', ['style']]);
184            if ($(el).data('restricted-styleset')) {
185                settings.styleTags = [
186                    {title: 'Standard', tag: 'p', className: '', value: 'p'},
187                    {title: 'Blockquote', tag: 'blockquote', className: 'blockquote', value: 'blockquote'},
188                    'pre', 'h3', 'h4', 'h5'
189                ];
190            }
191        }
192
193        $(el).summernote(settings);
194
195        if (el.disabled) {
196            $(el).summernote('disable');
197        }
198    });
199}
200
201function init_dropzones() {
202    let dropzones = $('div.dropzone:not(.l-no-init)');
203
204    dropzones.each(function (index, zone) {
205        if (typeof zone.dropzone === 'undefined') {
206            let myDropzone = initDropzone(zone, $(zone).parents('[data-upload-url]').data('upload-url'));
207        }
208    });
209}
210
211// display flash messages stored in cloudtec_flashes
212function init_flashes() {
213    let flashTypes = ['success', 'error', 'info', 'warning'];
214    for (let type of flashTypes) {
215        if (type in cloudtec_flashes) {
216            while (cloudtec_flashes[type].length) {
217                let flash = cloudtec_flashes[type].shift(); // remove element from array to prevent double display
218                let metadata = $('span.flash-metadata', '<div>' + flash + '</div>');
219                let id = metadata.data('id');
220                if (id) {
221                    $('.l-index-card table tr[data-id="' + id + '"]').addClass('flash-focus');
222                }
223
224                toastr[type](flash, null, {
225                    'onclick': function () {
226                        if (metadata.data('url')) {
227                            window.location.href = metadata.data('url');
228                        }
229                    }
230                });
231            }
232        }
233    }
234}
235
236/**
237 * Does an ajax form request to update specific form fields.
238 * This won't save the object on the server since we don't send the token.
239 * ! Supports concurrent requests but won't check for race conditions.
240 * ! Empty multi select dropdowns will not generate any post data. This can result in a 500 error due to the POST request.
241 * ! Checkboxes will only be submitted if they are checked. Otherwise, no data is sent for its form field
242 * @param sources List of jquery input fields. They will be sent to the server to update the target fields.
243 * @param targets List of jquery input fields. Will be updated depending on the source field data.
244 */
245function updateFormFields(sources, targets) {
246    let form = $(sources.get(0).closest('form')), fields = $([].concat(sources.toArray(), targets.toArray())),
247        data = {}, method = form.find('input[name="_method"]').val();
248
249    if (!method) {
250        method = form.attr('method');
251    }
252
253    fields.each(function () {
254        if ($(this).is(':checkbox') && $(this).is(':checked')) {
255            data[$(this).attr('name')] = "1";
256        } else if (!$(this).is(':checkbox')) {
257            data[$(this).attr('name')] = $(this).val();
258        }
259    });
260
261    // lock fields during request
262    targets.each(function () {
263        maskElement($(this).parent());
264    });
265
266    $.ajax({
267        method: method, url: form.attr('action'), type: form.attr('method'), data: data, complete: function (xhr) {
268            targets.each(function () {
269                unmaskElement($(this).parent());
270            });
271
272            if (xhr.status === 200 || xhr.status === 422) {
273                let html = xhr.responseText;
274
275                targets.each(function () {
276                    const rowClassSelect = isBackend ? '.form-group, .form-compound' : '.uk-form-control
276s';
277                    let fieldHtml = $(html).find('#' + $(this).attr('id')).closest(rowClassSelect);
278                    const errorClass = isBackend ? 'is-invalid' : 'invalid';
279                    //dont show field errors
280                    fieldHtml.find('.' + errorClass).removeClass('is-invalid');
281                    fieldHtml.find('.form-error').remove();
282
283                    $('#' + $(this).attr('id')).closest(rowClassSelect).replaceWith(fieldHtml);
284                });
285
286                initialize_all_form_fields();
287
288                // trigger ajax update event on new element
289                // this has to be triggered after the dropdowns are initiated or the mask will break because the dropdown init changes the markup.
290                targets.each(function () {
291                    $('#' + $(this).attr('id')).trigger('ajax-update', [html]);
292                });
293                form.trigger('ajax-update', [html]);
294            }
295        }
296    });
297}
298
299/**
300 * Initiates ajax requests based on ajax-source and ajax-target data attributes.
301 */
302function init_ajax_field_loader() {
303    const initiateAjaxUpdate = function (select) {
304        let source = $(select).data('ajax-source');
305        let form = $(select).closest('form');
306        //add all sources to request
307        let sources = form.find(`[data-ajax-source="${source}"]`);
308
309        //find all targets with source
310        let targets = form.find(`[data-ajax-target="${source}"]`);
311
312        updateFormFields(sources, targets);
313    };
314
315    let debInputChange = debounce(initiateAjaxUpdate, 500);
316    // single selects in backend should prock the ajax update instantly. If someone sets multiple to false, this event will not be triggered.
317    $(document).on('changed.bs.select', '.kt_selectpicker[data-ajax-source]:not([multiple])', (event) => {
318        initiateAjaxUpdate(event.target);
319    });
320    $(document).on('change', '[data-ajax-source]:not(.kt_selectpicker:not([multiple]))', (event) => {
321        debInputChange(event.target);
322    });
323    // Update depending fields when a field has both ajax-source and ajax-target.
324    $(document).on('ajax-update', '[data-ajax-source]', (event) => {
325        initiateAjaxUpdate(event.target);
326    });
327}
328
329// Returns a function, that, as long as it continues to be invoked, will not
330// be triggered. The function will be called after it stops being called for
331// N milliseconds. If `immediate` is passed, trigger the function on the
332// leading edge, instead of the trailing.
333function debounce(func, wait, immediate) {
334    let timeout;
335    return function () {
336        let context = this, args = arguments;
337        let later = function () {
338            timeout = null;
339
340            if (!immediate) func.apply(context, args);
341        };
342        let callNow = immediate && !timeout;
343        clearTimeout(timeout);
344        timeout = setTimeout(later, wait);
345
346        if (callNow) func.apply(context, args);
347    };
348}
349
350function init_autocompletes() {
351    const validateSelection = function (el) {
352        let allValues = [],
353            validValues = [],
354            strings = [];
355
356        strings = [$(el).val()];
357
358        // We need this validate to determine, if the given values are still in order with the saved selection.
359        // e.g. if someone has selected "Germany" before, which is valid and then removes the "y" and it becomes "German"
360        // it would be invalid and therefore we have to remove it from the selected values.
361        strings.forEach(function (label) {
362            label = label.trim()
363            if (label) {
364                let existing = el._previousSelections.filter(selection => selection[$(el).data('label-name')] === label);
365                if (existing.length && validValues.indexOf(existing[0].id) === -1) {
366                    validValues.push(existing[0]);
367                }
368
369                allValues.push(label);
370            }
371        });
372
373        if (validValues.length === allValues.length && validValues.length) {
374            $('#' + $(el._autocomplete.input).data('hidden-id')).val(validValues.map(a => a.id).join(','));
375            $(el).addClass('ct-autocomplete-has-valid-selection');
376            // DataHolder is a hidden field which stores the actual value and all `change` event-listeners are bound to (ajax-source-target)
377            let dataHolderId = el.id.replace('_input', '');
378            $(`#${dataHolderId}`).trigger('change')
379        } else {
380            $('#' + $(el._autocomplete.input).data('hidden-id')).val('');
381            $(el).removeClass('ct-autocomplete-has-valid-selection');
382            // We dispatch an event so it is possible in further implementation to e.g. reset a selection view e.g. medic in opancare
383            el.dispatchEvent(new Event("invalid-selection"));
384        }
385    };
386
387    const syncMultiSelection = function (el) {
388        let hiddenField = $('#' + $(el._autocomplete.input).data('hidden-id'));
389        let hiddenFieldValues = [];
390
391        let selectionList = $('<ul class="ct-autocomplete-selection"></ul>');
392        el._previousSelections.forEach(function (selection) {
393            // they should have a delete link
394            let selectionItem = $('<li></li>');
395            selectionItem.text(selection[$(el).data('label-name')]);
396            selectionItem.append('<a href="javascript:" class="ct-autocomplete-selection-delete"><i class="fa fa-circle-xmark"></i></a>');
397            selectionItem.find('a').on('click', function () {
398                el._previousSelections = el._previousSelections.filter(item => item.id !== selection.id);
399                syncMultiSelection(el);
400            });
401            selectionList.append(selectionItem);
402            hiddenFieldValues.push(selection.id);
403        });
404        hiddenField.val(hiddenFieldValues.join(','));
405
406        let listContainer = $(el);
407
408        if ($(el).parent().hasClass('uk-inline')) {
409            listContainer = $(el).parent();
410        }
411
412        listContainer.next('.ct-autocomplete-selection').remove();
413        listContainer.after(selectionList);
414    };
415
416    const elements = document.querySelectorAll('.ct-autocomplete');
417    elements.forEach(function (el) {
418        //set a data attribute on each autocomplete element to prevent multiple initializations of event listeners
419        if ($(el).data('initialized')) {
420            return;
421        }
422        $(el).attr('data-initialized', true);
423
424        // We store every selection in an array to later validate if a string value in the field is a valid selection or just a string
425        el._previousSelections = $(el).data('pre-selection') && $(el).data('pre-selection').length ? $(el).data('pre-selection') : [];
426
427        // todo: This is maybe not the fasted way to handle this
428        let prototype = Object.getPrototypeOf(el);
429        prototype.resetSelection = function () {
430            $(this).val(null);
431            $(this)._previousSelections = [];
432            $(this).removeClass('ct-autocomplete-has-valid-selection');
433            $('#' + $(this._autocomplete.input).data('hidden-id')).val('');
434
435            if ($(this).attr('multiple')) {
436                syncMultiSelection(this);
437            }
438        };
439        prototype.setValue = function (value, text) {
440            if (!$(this).attr('multiple')) {
441                $(this).val(text);
442            }
443
444            $('#' + $(this._autocomplete.input).data('hidden-id')).val(value);
445            let selection = {id: value};
446            selection[$(el).data('label-name')] = text;
447            this._previousSelections = [selection];
448
449            if ($(this).attr('multiple')) {
450                syncMultiSelection(this);
451            } else {
452                validateSelection(this, false);
453            }
454        };
455        Object.setPrototypeOf(el, prototype);
456
457        let config = {
458            selector: '#' + $(el).attr('id'),
459            debounce: 300,
460            threshold: $(el).data('min-chars'),
461            placeholder: $(el).data('placeholder'),
462            wrapper: false,
463            // Make sure all items from api are returned and they are not filtered by the library again
464            searchEngine: function searchEngine(query, record) {
465                return record;
466            },
467            resultsList: {
468                element: (list, data) => {
469                    const info = document.createElement("p");
470                    info.setAttribute("class", "ct-autocomplete-info");
471                    if (data.results.length === 0) {
472                        info.innerHTML = $(el).data('no-results-text');
473                        list.append(info);
474                    } else {
475                        $('.ct-autocomplete-info', list).remove();
476                    }
477                },
478                class: 'ct-autocomplete-list',
479                noResults: true,
480                maxResults: $(el).data('max-results'),
481                tabSelect: true
482            },
483            resultItem: {
484                // We need to add an own highlighting algorithm as we do not use the original search engine
485                element: (item, data) => {
486                    let match = data.match,
487                        value = el.value;
488
489                    let escapedValue = value
490                        .replaceAll('&', '&amp;')
491                        .replaceAll('<', '&lt;')
492                        .replaceAll('>', '&gt;')
493                        .replaceAll('"', '&quot;')
494                        .replaceAll("'", '&#039;');
495
496                    match = match.replace(value, '<mark>' + escapedValue + '</mark>');
497                    // Then we do the same with the uppercase version of the value
498                    value = value.charAt(0).toUpperCase() + value.slice(1);
499                    match = match.replace(value, '<mark>' + escapedValue + '</mark>');
500
501                    item.innerHTML = `${match}`;
502                },
503            },
504            data: {
505                src: async (query) => {
506                    try {
507                        let url = $(el).data('url'),
508                            searchEvent = new Event("before-search");
509
510                        searchEvent.url = url;
511                        el.dispatchEvent(searchEvent);
512                        // Url could be changed in the event
513                        url = searchEvent.url;
514
515                        let token = url.indexOf('?') > -1 ? '&' : '?';
516
517                        const encodedQuery = encodeURIComponent(query);
518                        const source = await fetch(`${url}${token}query=${encodedQuery}`);
519
520                        return await source.json();
521                    } catch (error) {
522                        return error;
523                    }
524                },
525                // Data source 'Object' key to be searched
526                keys: [$(el).data('label-name')]
527            },
528            events: {
529                input: {
530                    focus() {
531                        if (el._autocomplete.input.value.length) {
532                            el._autocomplete.start();
533                        }
534                    },
535                    selection(event) {
536                        const feedback = event.detail;
537                        el._autocomplete.input.blur();
538                        el._autocomplete.input.value = feedback.selection.value[feedback.selection.key];
539                        $('#' + $(el._autocomplete.input).data('hidden-id')).val(feedback.selection.value.id);
540
541                        el._previousSelections.push(feedback.selection.value);
542
543                        validateSelection(el);
544                    },
545                    // When hitting enter, first result is selected automatically
546                    results(event) {
547                        $(this).one('keydown', function (e) {
548                            if (e.key === 'Enter') {
549                                e.preventDefault();
550                                e.stopPropagation();
551                                let list = $(this).next('.ct-autocomplete-list');
552                                if (list.length && list.children().length) {
553                                    let firstItem = list.children().first();
554                                    firstItem.trigger('click');
555                                }
556                            }
557                        });
558                    }
559                },
560            }
561        };
562
563        if (el.getAttribute('multiple')) {
564            config.events = {
565                input: {
566                    focus() {
567                        if (el._autocomplete.input.value.length) {
568                            el._autocomplete.start();
569                        }
570                    },
571                    selection(event) {
572                        const feedback = event.detail;
573                        // Clear the input field
574                        el._autocomplete.input.value = '';
575
576                        // do not add it if it's already in the list
577                        if (el._previousSelections.filter(item => item.id === feedback.selection.value.id).length === 0) {
578                            el._previousSelections.push(feedback.selection.value);
579                        }
580
581                        syncMultiSelection(el);
582                    }
583                }
584            }
585        }
586
587        el._autocomplete = new autoComplete(config);
588
589        $(el._autocomplete.input).on('keyup', function () {
590            if (!$(el).attr('multiple')) {
591                validateSelection(this);
592            }
593        });
594
595        if ($(el).attr('multiple')) {
596            syncMultiSelection(el);
597        }
598    });
599}
600
601
602/**
603 * This should hopefully replace the existing ajax-source/ajax-target handling. You can use it by adding an ajax_source_target
604 * definition to your form. Check the manual in the admin-bundle about how 
604it works.
605 */
606class AjaxFieldUpdater {
607    #definitions;
608    #form;
609    #triggerFields = {};
610    #requests;
611
612    constructor(form) {
613        this.#form = form;
614        this.#triggerFields = {};
615        this.#requests = new Set();
616
617        this.#definitions = this.#form.data('ajax-source-target');
618
619        if (this.#definitions) {
620            this.setupFieldTriggers();
621        }
622    }
623
624    setupFieldTriggers() {
625        for (const [key] of Object.entries(this.#definitions)) {
626            // We try to locate the trigger field in the html and convert its property path to field notation
627            let fieldName = this.#form.attr('name') + this.convertPropertyPathToFieldName(key),
628                field = $('[name="' + fieldName + '"]', this.#form);
629
630            // If no field was found it's possible that it's a multiselect field so we try again with []
631            if (!field.length) {
632                field = $('[name="' + fieldName + '[]"]', this.#form);
633            }
634
635            if (field.length) {
636                this.#triggerFields[key] = field;
637            }
638        }
639
640        for (const [key, field] of Object.entries(this.#triggerFields)) {
641            if (field.hasClass('kt_selectpicker') && !field.attr('multiple')) {
642                field.one('changed.bs.select', () => this.initAjaxFieldUpdateRequest(key));
643                // datetimepicker has its own event that gets triggered when a date is selected over the popup-picker
644            } else if (field.parent().hasClass('l-datetimepicker')) {
645                field.parent().one('change.datetimepicker', () => this.initAjaxFieldUpdateRequest(key));
646            } else {
647                field.one('change', debounce(() => this.initAjaxFieldUpdateRequest(key), 500));
648            }
649        }
650    }
651
652    initAjaxFieldUpdateRequest(triggerKey) {
653        if (this.#requests.has(triggerKey)) {
654            return;
655        }
656
657        this.#requests.add(triggerKey);
658
659        let method = $('input[name="_method"]', this.#form).val() || this.#form.attr('method'),
660            data = {},
661            targetFields = [],
662            maskableFields = [];
663
664        for (const [fieldName, triggerField] of Object.entries(this.#triggerFields)) {
665            if (!triggerField.is(':checkbox') || triggerField.is(':checked')) {
666                data[triggerField.attr('name')] = triggerField.val();
667            }
668
669            // radio-elements (ChoiceType with `'expanded' => true`) have multiple input-elements, so we get the one
670            // that is checked (since `triggerField.val()` just gets the first one)
671            if (triggerField.toArray().every((el) => $(el).attr('type') === 'radio')) {
672                data[triggerField.attr('name')] = triggerField.filter(':checked').val();
673            }
674
675            for (const targetFieldName of this.#definitions[fieldName]) {
676                let foundTargetFields;
677
678                if ($('[data-ajax-field="' + targetFieldName + '"]', this.#form).length) {
679                    foundTargetFields = $('[data-ajax-field="' + targetFieldName + '"]', this.#form);
680                } else {
681                    foundTargetFields = $('[name^="' + $(this.#form).attr('name') + this.convertPropertyPathToFieldName(targetFieldName) + '"]', this.#form);
682                }
683
684                foundTargetFields.each(function () {
685                    let targetField = $(this);
686
687                    targetFields.push(targetField);
688
689                    if (fieldName === triggerKey) {
690                        maskableFields.push(targetField);
691                    }
692
693                    if (!targetField.data('ajax-field') && (!targetField.is(':checkbox') || targetField.is(':checked'))) {
694                        data[targetField.attr('name')] = targetField.val();
695                    }
696                });
697            }
698        }
699
700        // We also want to mask the trigger field
701        maskableFields.push(this.#triggerFields[triggerKey]);
702
703        this.maskFields(maskableFields);
704
705        $.ajax({
706            method: method,
707            url: this.#form.attr('action'),
708            type: this.#form.attr('method'),
709            data: data,
710            complete: (xhr) => {
711                this.unmaskFields(maskableFields);
712                this.handleAjaxResponse(xhr, targetFields);
713                this.#requests.delete(triggerKey);
714            }
715        });
716    }
717
718    handleAjaxResponse(xhr, fields) {
719        if (xhr.status === 200 || xhr.status === 422) {
720            this.updateFieldsFromResponse(xhr.responseText, fields);
721            initialize_all_form_fields();
722            init_autocompletes();
723
724            this.#form.trigger('ajax-field-update', [xhr.responseText]);
725
726            fields.forEach(field => {
727                let eventField;
728                if (field.data('ajax-field')) {
729                    eventField = $('[data-ajax-field="' + field.data('ajax-field') + '"]', this.#form);
730                } else {
731                    eventField = $('#' + field.attr('id'));
732                }
733
734                eventField.trigger('ajax-field-update', [xhr.responseText]);
735            });
736        }
737
738        this.update();
739    }
740
741    updateFieldsFromResponse(responseText, fieldsToUpdate) {
742        fieldsToUpdate.forEach(field => {
743            if (field.data('ajax-field')) {
744                $(field).html($('[data-ajax-field="' + field.data('ajax-field') + '"]', responseText).html());
745            } else {
746                const rowClassSelect = isBackend ? '.form-group, .form-compound' : '.uk-form-controls';
747                let fieldHtml = $(responseText).find('#' + field.attr('id')).closest(rowClassSelect);
748                fieldHtml.find('.is-invalid, .invalid').removeClass('is-invalid');
749                fieldHtml.find('.form-error').remove();
750                $('#' + field.attr('id')).closest(rowClassSelect).replaceWith(fieldHtml);
751            }
752        });
753    }
754
755    update() {
756        this.setupFieldTriggers();
757    }
758
759    maskFields(fields) {
760        fields.forEach(field => maskElement(field.parent()));
761    }
762
763    unmaskFields(fields) {
764        fields.forEach(field => unmaskElement(field.parent()));
765    }
766
767    convertPropertyPathToFieldName(dotNotation) {
768        return dotNotation.split('.').reduce((acc, part, index) => {
769            if (index === 0) {
770                return `[${part}]`;
771            } else {
772                return `${acc}[${part}]`;
773            }
774        }, '');
775    }
776}
777
778
779function init_character_counter() {
780    $(document).on('keyup', 'input,textarea', function () {
781        let widget = $(this).closest('.form-group,.uk-form-controls');
782        let counter = $('.l-character-count', widget);
783        if (counter.length === 1) {
784            counter.text($(this).val().length);
785        }
786    });
787}
788
789function init_custom_forms() {
790    $(document).ready(function () {
791        $('form[name="cloudtec_form_request"][data-submit="page"]').on('submit', function (e) {
792            e.preventDefault();
793
794            let form = $(this);
795            let data = form.serializeArray();
796            let container = form.find('.ct-custom-form-content');
797
798            form.find('button[type="submit"]').prop('disabled', true);
799
800            if (form.find('button[type="submit"]').hasClass('l-submitted') === false) {
801                form.find('button[type="submit"]').addClass('l-submitted')
802            }
803
804            maskForm(form);
805
806            $.ajax({
807                type: form.attr('method'),
808                url: form.attr('action'),
809                data: data,
810                complete: function (xhr) {
811                    let status = xhr.status;
812                    let location = xhr.getResponseHeader('Location');
813
814                    if (location) {
815                        window.location.href = location;
816                        return;
817                    }
818
819                    if ((status >= 200 && status < 300) || status === 422) {
820                        let result = $('<div>').html(xhr.responseText)
821
822
823                        let scriptTag = result.find('script').filter(function () {
824                            return this.innerHTML.includes('let cloudtec_flashes');
825                        }).first();
826
827                        let flashes = false;
828
829                        if (scriptTag.length > 0) {
830                            flashes = scriptTag.html().match(/let cloudtec_flashes = (\{.*?\});/);
831                        }
832
833                        if (flashes) {
834                            cloudtec_flashes = JSON.parse(flashes[1]);
835                        }
836
837                        if (form.find('#ct-request-success').length === 0 && flashes) {
838                            init_flashes();
839                        } else if (flashes) {
840                            let messageTemplate = form.find('#ct-request-success').html();
841                            form.html('');
842                            let flashTypes = ['success', 'error', 'info', 'warning'];
843                            for (let type of flashTypes) {
844                                if (type in cloudtec_flashes) {
845                                    while (cloudtec_flashes[type].length) {
846                                        let message = $('<div>').html(messageTemplate);
847                                        message.find('.uk-alert').addClass(`uk-alert-${type}`);
848                                        message.find('p').text(cloudtec_flashes[type].shift());
849                                        form.append(message);
850                                    }
851                                }
852                            }
853                            if (form.find('.uk-alert').length) {
854                                form.find('.uk-alert').first().get(0).scrollIntoView({left: 0, block: 'start', behavior: 'smooth'});
855                            }
856                            return;
857                        }
858
859                        container.html(result.find(".ct-custom-form-content").html());
860
861                        initialize_all_form_fields();
862                        reset_captcha(form);
863
864                        form.find('button[type="submit"]').prop('disabled', false);
865
866                        if (form.find('button[type="submit"]').hasClass('l-submitted')) {
867                            form.find('button[type="submit"]').removeClass('l-submitted')
868                        }
869
870                        unmaskForm(form);
871                    } else {
872                        form.html(form.find('#ct-request-failed').html());
873                    }
874                }
875            });
876        });
877    });
878}
879
880const delay = ms => new Promise(res => setTimeout(res, ms));
881
882/**
883 * This class handles all server communication for mutex information. It is a singleton and prevents double initialization.
884 * Events:
885 *  - mutex-initial: the first time there is mutex data available
886 *  - mutex-update: whenever the mutex data changes. dispatched after the other events
887 *  - mutex-other-started: when someone else started editing
888 *  - mutex-other-stopped: when someone else stopped editing
889 *  - mutex-failed: when the mutex check failed. The provided message is not user friendly.
890 */
891class Mutex {
892    timeout = 60;
893    active = false; // if the mutex check is active
894    activityData = null; // stores the last activity data
895    someoneElseStoppedEditing = false;
896
897    constructor() {
898        let instance = $(document).data('mutex');
899        if (instance) {
900            return instance;
901        }
902
903        $(document).data('mutex', this);
904
905        this.start();
906    }
907
908    async check() {
909        if (this.active) {
910            return;
911        }
912        this.active = true;
913        while (this.active) {
914            let response, previousData;
915            try {
916                response = await fetch('?mutex');
917                if (!response.ok) {
918                    this.stop();
919                    let message = `${response.status}: ${response.statusText}`;
920                    console.error(message);
921                    document.dispatchEvent(new CustomEvent('mutex-failed', {detail: message}));
922                    return;
923                }
924
925                previousData = this.activityData;
926                this.activityData = await response.json();
927
928            } catch (e) {
929                this.stop();
930                console.error(e.message);
931                document.dispatchEvent(new CustomEvent('mutex-failed', {detail: e.message}));
932                return;
933            }
934
935            if (this.activityData !== previousData) {
936                // initial event
937                if (previousData === null) {
938                    document.dispatchEvent(new CustomEvent('mutex-initial', {detail: this}));
939                }
940
941                // someone else started editing
942                if (this.activityData.length > 0 && !previousData?.length) {
943                    document.dispatchEvent(new CustomEvent('mutex-other-started', {detail: this}));
944                }
945
946                // someone else stopped editing
947                if (this.activityData.length === 0 && previousData?.length > 0) {
948                    this.someoneElseStoppedEditing = true;
949                    document.dispatchEvent(new CustomEvent('mutex-other-stopped', {detail: this}));
950                }
951
952                // generic update
953                document.dispatchEvent(new CustomEvent('mutex-update', {detail: this}));
954            }
955
956            await delay(this.timeout / 2 * 1000);
957        }
958    }
959
960    start() {
961        this.check();
962    }
963
964    stop() {
965        this.activityData = null;
966        this.active = false;
967    }
968}
969
970function initializePhoneField(inputGroup) {
971    let numberInput = $('.ct-phone-number', inputGroup);
972    if (numberInput.length === 0) {
973        return;
974    }
975    let cleave = null;
976    let currentTrunkPrefix = null;
977
978    let phoneSelect = isBackend ? '.ct-phone-flag-dropdown select' : '.ct-phone-flag-dropdown';
979    $(phoneSelect, inputGroup).change(function () {
980        if (cleave) {
981            // cleave needs to be destroyed/reinstated because i can't change the prefix on the fly.
982            cleave.destroy();
983        }
984
985        let selectedCountry = $(phoneSelect + ' :selected', inputGroup);
986        let countryCode = selectedCountry.data('code');
987        let trunkPrefix = String(selectedCountry.data('trunk-prefix'));
988        let countryPrefix = String(selectedCountry.data('prefix'));
989        if (!countryPrefix) {
990            countryCode = "";
991        }
992        cleave = new Cleave(numberInput.get(0), {
993            phone: true,
994            phoneRegionCode: countryCode,
995            prefix: trunkPrefix
996        });
997
998        // update prefix in the number field if only the prefix was filled in
999        if (currentTrunkPrefix === numberInput.val()) {
1000            numberInput.val(trunkPrefix);
1001        }
1002        currentTrunkPrefix = trunkPrefix;
1003    }).trigger('change');
1004}
1005
1006function init_newsletter_subscribe_form() {
1007    $(document).ready(function () {
1008        $('form[name="cloudtec_newsletter_recipient"][data-submit="page"]').on('submit', function (e) {
1009            e.preventDefault();
1010
1011            let form = $(this);
1012            let data = form.serializeArray();
1013            let formContent = form.find('.ct-newsletter-form-content');
1014            let submitButton = form.find('button[type="submit"]');
1015
1016            // disable button
1017            submitButton.prop('disabled', true);
1018            if (!submitButton.hasClass('l-submitted')) {
1019                submitButton.addClass('l-submitted');
1020            }
1021
1022            maskForm(form);
1023
1024            $.ajax({
1025                type: form.attr('method'),
1026                url: form.attr('action'),
1027                data: data,
1028                complete: function (xhr) {
1029                    let html = xhr.responseText;
1030                    let newContent = $('.ct-newsletter-form-content', html);
1031
1032                    if (xhr.status === 422) {
1033                        // render form with returned form content if validation fails
1034                        formContent.replaceWith(newContent);
1035                    } else if (xhr.status === 200) {
1036                        let successTemplate = form.find('#ct-request-success').html();
1037
1038                        form.prepend(successTemplate);
1039
1040                        formContent.replaceWith(newContent);
1041                    } else {
1042                        let failureTemplate = form.find('#ct-request-failed').html();
1043                        form.replaceWith(failureTemplate);
1044                    }
1045
1046                    initialize_all_form_fields();
1047                    reset_captcha(form);
1048                    unmaskForm(form);
1049
1050                    if (submitButton.hasClass('l-submitted')) {
1051                        submitButton.removeClass('l-submitted');
1052                    }
1053                    submitButton.prop('disabled', false);
1054                }
1055            });
1056        });
1057    });
1058}
1059
1060/**
1061 * resize iframes with data-resize="auto" to fit their content height
1062 * Note: This only works for same-origin iframes due to browser security policies.
1063 */
1064function init_iframe_auto_resize() {
1065    $(document).ready(function () {
1066        $('iframe[data-resize="auto"]').on('load', function () {
1067            const iframe = this;
1068            const iwin = iframe.contentWindow;
1069            const doc = iframe.contentDocument;
1070
1071            // properties are protected due to cors policy when source is external / non same origin
1072            if (!iwin || !doc) return;
1073
1074            const measureAndSet = function () {
1075                iframe.style.height = '0px';
1076                const b = doc.body;
1077                const e = doc.documentElement;
1078                const height = Math.max(
1079                    b.scrollHeight, e.scrollHeight,
1080                    b.offsetHeight, e.offsetHeight,
1081                    e.clientHeight
1082                );
1083                iframe.style.height = height + 'px';
1084            };
1085
1086            measureAndSet();
1087        });
1088    });
1089}
1090
1091function init_form_submit_disabled_on_submit() {
1092    // disable submit buttons on submit
1093    $('.l-submit[type="submit"]').closest('form').submit(function (event) {
1094        $(this).addClass('l-submitted');
1095        // show spinner on clicked submit button
1096        if ('originalEvent' in event && 'submitter' in event.originalEvent) {
1097            $(event.originalEvent.submitter).addClass('l-submitted');
1098        } else {
1099            // fallback when no button was used (e.g. control + enter)
1100            $('#cloudtec-save').addClass('l-submitted');
1101        }
1102
1103        // If the form has a modal that can be used to submit, do not disable submit buttons
1104        if ($(this).find('.modal').find('button[type="submit"]').length > 0) {
1105            return;
1106        }
1107
1108        // disable all submit buttons (after 10ms so the pressed button is still sent in the request)
1109        setTimeout(() => $("[type=submit]", this).prop('disabled', true), 10);
1110    });
1111}

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.