PageSourceSearch

https://www.alexandria-magazin.at/ddm-framework/ddm-framework.js?v=1.4.19

js alexandria-magazin.at collected 2026-10-05 03:41:53 UTC 78,844 bytes, 2,037 lines download raw bytes

1/** GLOBAL POST NAMES */
2
3const POST_NAME_PAGE_TAG = "ddm-page-tag";
4const POST_NAME_PAGE_REFERRER = "ddm-page-referrer";
5const POST_NAME_TRANSLATION_KEY = "ddm-translation-key";
6const POST_NAME_TRANSLATION_TEXT = "ddm-translation-text";
7const POST_NAME_TRANSLATION_KEY_ARRAY = "ddm-translation-key-array";
8
9const POST_NAME_MEDIA_ID = "ddm-media-id";
10const POST_NAME_MEDIA_FILE_PATH = "ddm-media-file-path";
11const POST_NAME_MEDIA_FILE_NAME = "ddm-media-file-name";
12const POST_NAME_MEDIA_TYPE_NAME = "ddm-media-type-name";
13const POST_NAME_MEDIA_UPLOAD_FILE = "ddm-media-upload-file";
14const POST_NAME_MEDIA_UPLOAD_REPLACE_FILE = "ddm-media-upload-replace-file";
15
16const POST_NAME_PAGE_MEDIA_KEY = "ddm-media-key";
17const POST_NAME_PAGE_MEDIA_ALT_TEXT = "ddm-media-alt-text";
18
19const POST_NAME_PARAMETER_KEY = "ddm-parameter-key";
20const POST_NAME_PARAMETER_VALUE = "ddm-parameter-value";
21const POST_NAME_PARAMETER_IS_VARIABLE = "ddm-parameter-is-variable";
22
23const POST_NAME_DUI_PARENT_ID = "ddm-dui-parent-id";
24const POST_NAME_DUI_SORT_ORDER_CHANGE_DIRECTION = "ddm
24-dui-sort-order-change-direction";
25const POST_NAME_DUI_PARENT_DEFINITION_KEY = "ddm-dui-parent-definition-key";
26const POST_NAME_DUI_COLLECTION_DEFINITION_KEY = "ddm-dui-collection-definition-key";
27const POST_NAME_DUI_OPTIONS = "ddm-dui-options";
28const POST_NAME_DUI_FILTER_CATEGORY_KEY = "ddm-dui-filter-category-key";
29const POST_NAME_DUI_FILTER_OPTION_ID = "ddm-dui-filter-option-id";
30
31/** GLOBAL COOKIE NAMES */
32
33const COOKIE_NAME_LANGUAGE = "ddm-language";
34
35/** MEDIA TYPES */
36
37const MEDIA_TYPE_IMAGE = "image";
38const MEDIA_TYPE_VIDEO = "video";
39const MEDIA_TYPE_AUDIO = "audio";
40const MEDIA_TYPE_DOCUMENT = "document";
41
42/** EVENTS */
43
44const EVENT_LISTENER_DYNAMIC_UI_PARENT_ADDED = "dynamic-ui-parent-added";
45const EVENT_LISTENER_DYNAMIC_UI_PARENT_DELETED = "dynamic-ui-parent-deleted";
46const EVENT_LISTENER_MEDIA_EDITOR_CONTENT_CHANGED  = "media-editor-content-changed";
47
48/** FILE UPLOAD **/
49
50const UPLOAD_RESULT_CODE_OK = 0x3a6746;
51const UPLOAD_RESULT_CODE_ERROR = 0x88cae1;
52const UPLOAD_RESULT_CODE_FILE_EXISTS = 0xd45a0b;
53
54const classNameTextFieldEditMode = "ddm-admin-textinput-active";
55var mediumEditor = null;
56
57$(document).ready(function(){
58    InitDdmFramework();
59});
60
61/**
62 * Initializes DDM Framework
63 * Checks cookie permission and takes measures if needed
64 * Adds padding-top if ddm-page-start-after-header class name was found
65 * 
66 */
67function InitDdmFramework(){
68
69    CheckCookiePermission();
70
71    var clientPage = $('.ddm-page-start-after-header');
72    var header = $('.ddm-specific-header');
73    if(clientPage.length > 0 && header.length > 0){
74        setTimeout(function(){ // fix for getBoundingClientRect().top returning zero sometimes
75            clientPage.css("padding-top", header[0].getBoundingClientRect().top + header.height());
76        }, 1);
77    }
78}
79
80/**
81 * Stores text in database using ajax call
82 * @param {string} textKey database translation key
83 * @param {string} text desired text
84 * @param {function} onTextSaved callback when the saving process is done
85 */
86function SaveTextToDatabase(textKey, text, onTextSaved) {
87    var postData = { };
88    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
89    postData[POST_NAME_TRANSLATION_KEY] = textKey;
90    postData[POST_NAME_TRANSLATION_TEXT] = text;
91
92    jQuery.ajax({
93        url: "/ddm-framework/util/ajax/update-page-text.php",
94        type: 'POST',
95        data: postData,
96        responseType: 'json',
97        success:function(response){
98            if(response.result == true)
99                onTextSaved();
100            else AjaxErrorAlert("something went wrong, could not store text in database: " + response.message);
101        },
102        error:function (request, status, error){
103            AjaxErrorAlert("something went wrong, could not store text in database: " + request.responseText);
104        }
105    });
106}
107
108function UpdateParameterInDatabase(parameterKey, parameterValue, isVariable, onSuccess, onError){
109    var postData = { };
110    postData[POST_NAME_PARAMETER_KEY] = parameterKey;
111    postData[POST_NAME_PARAMETER_VALUE] = parameterValue;
112    postData[POST_NAME_PARAMETER_IS_VARIABLE] = isVariable;
113
114    jQuery.ajax({
115        url: "/ddm-framework/util/ajax/update-parameter.php",
116        type: 'POST',
117        data: postData,
118        responseType: 'json',
119        success:function(response){
120            if(response.result == true){
121                if(typeof onSuccess === "function")
122                    onSuccess();
123            }
124            else console.error(response.message);
125        },
126        error:function (request, status, error){
127            if(typeof onError === "function")
128                onError(request.responseText);
129            else console.error(request.responseText);
130        }
131    });
132}
133
134/**
135 * Loads multiple texts from database using ajax. Returns json including the texts
136 * @param {string} textKeys separated by comma
137 * @param {function} onComplete callback
138 */
139function LoadMultipleTextsFromDatabase(textKeys, onComplete){
140
141    var postData = {};
142    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
143    postData[POST_NAME_TRANSLATION_KEY_ARRAY] = textKeys;
144
145    jQuery.ajax({
146        url: "/ddm-framework/util/ajax/load-page-text-multiple.php",
147        type: 'POST',
148        data: postData,
149        responseType: 'json',
150        success:function(response){
151            if(response.result == true)
152                onComplete(response.data);
153            else{
154                AjaxErrorAlert("something went wrong, could load texts from database: " + response.message);
155            }
156        },
157        error:function (request, status, error){
158            AjaxErrorAlert("something went wrong, could load texts from database: " + request.responseText);
159        }
160    });
161}
162
163/**
164 * Checks whether the passed element or one the parent elements has set "position=fixed"
165 * @param {element} element 
166 * @return {boolean} isFixed
167 */
168function IsElementOrParentFixed(element) {
169    var $element = $(element);
170    var $checkElements = $element.add($element.parents());
171    var isFixed = false;
172    $checkElements.each(function(){
173        if ($(this).css("position") === "fixed") {
174            isFixed = true;
175            return false;
176        }
177    });
178    return isFixed;
179}
180
181/**
182 * @deprecated since version 1.0.9 - please use /modules/content-editor/url-editor instead
183 * Shows an overlay letting the user change the text of the url and the url itself
184 * @param {element} element The label having an <A> element as parent (is a url)
185 */
186function ShowUrlEditor(element, hideTextInput){
187    var editor = $('#ddm-url-editor');
188    editor.show();
189
190    var inputText = $('#ddm-url-edit-input-text');
191    var inputUrl = $('#ddm-url-edit-input-url');
192    var inputTarget = $('#ddm-url-edit-input-target');
193    var errorLabel = $('#ddm-url-editor-error-label');
194    errorLabel.hide();
195    editor.find('#ddm-url-editor-text-edit').toggle(!hideTextInput); // hide text input if requested
196
197    var href = element.parent().attr('href');
198    var urlTarget = element.parent().attr('target');
199    inputUrl.val(href);
200    inputText.val(element.html());
201    inputTarget.prop('checked', urlTarget == '_blank');
202
203    $('#ddm-url-edit-button-cancel').unbind('click').bind('click', function(){
204        editor.hide();
205    });
206
207    $('#ddm-url-edit-button-save').unbind('click').bind('click', function(){
208        var text = inputText.val();
209        
210        if(!IsTextValid(text)){
211            ShowTextInvalidAlert();
212            return;
213        }
214
215        var key = element.attr('data-key');
216        var urlKey = key + "-url"; // url database key is same as text id but with "-url" suffix - see DDM-Toolbox.DDMUrl (function)
217        var urlTarget = key + "-target"; // target database key is same as text id but with "-target" suffix - see DDM-Toolbox.DDMUrl (function)
218        var target = inputTarget.prop('checked') ? '_blank' : '_self';
219
220        var valuesToBeSaved = [];
221        valuesToBeSaved.push({key: key, value: text});
222        valuesToBeSaved.push({key: urlKey, value: inputUrl.val()});
223        valuesToBeSaved.push({key: urlTarget, value: target});
224
225        valuesToBeSaved.forEach(function(item, index){
226            SaveTextToDatabase(item.key, item.value, function(){
227                if(index == valuesToBeSaved.length - 1){
228                    element.html(text);
229                    element.parent().attr("href", inputUrl.val());
230                    element.parent().attr("target", target);
231                    editor.hide();
232                }
233            });
234        });
235    });
236}
237
238/**
239 * @deprecated since version 1.0.9 - please use /modules/content-editor/url-editor instead
240 * Shows available url editor actions next to the url text field
241 * @param {element} textField textField that is referenced
242 */
243function ShowUrlEditorActions(textField){
244    $("#textEditorActions").hide();
245    var urlEditorActionContainer = $("#urlEditorActions");
246    SetEditorActionContainerPosition(textField, urlEditorActionContainer);
247    var followUrlButton = $("#urlEditorActionFollowUrl");
248    var editUrlButton = $("#urlEditorActionEdit");
249    var cancelButton = $("#urlEditorActionCancel");
250    
251    followUrlButton.unbind('click').bind('click', function(){
252        urlEditorActionContainer.hide();
253        var href = textField.parent().attr('href');
254        window.open(href, "_self");
255    });
256
257    editUrlButton.unbind('click').bind('click', function(){
258        urlEditorActionContainer.hide();
259        ShowUrlEditor(textField);
260    });
261
262    cancelButton.unbind('click').bind('click', function(){
263        urlEditorActionContainer.hide();
264    });
265
266    urlEditorActionContainer.show();
267}
268
269/**
270 * @deprecated since version 1.0.9
271 * Shows an overlay letting the user replace an image by uploading a new one
272 * @param {boolean} visibility true or false => visible or hidden
273 * @param {string} imageKey id/key of the image to be changed
274 * @param {int} mediaID id of the media entry in database table "media"
275 */
276function SetImageEditorVisibility(visibility, imageKey, mediaID){
277    var editor = $('#ddm-image-editor');
278    if(visibility) 
279        editor.show(); 
280    else editor.hide();
281    $('#uploadForm').removeClass('dark');
282    $('#uploadForm').css('backgroundImage', 'none');
283    $('#ddm-image-upload-preview').hide();
284    $('.ddm-image-upload-alt-text-input').hide();
285    $('#ddm-image-upload-alt-text-input').val('');
286    $('#uploadMessageBox').empty();
287    $('#imageKey').val(imageKey);
288    $('#ddm-upload-form-button-set-image').prop('disabled', true);
289    if(mediaID != undefined && mediaID != '' && mediaID > 0){
290        var image = $('#' + imageKey);
291        var imageSrc = image.attr('src');
292        var imageAltText = image.attr('alt');
293        window.mediaFilePath = imageSrc;
294        window.mediaID = mediaID;
295        if(imageSrc != undefined && imageSrc != ''){
296            $('#uploadForm').css('backgroundImage', 'url("' + imageSrc + '")');
297            $('#uploadForm').addClass('dark');
298            $('#ddm-upload-form-button-set-image').prop('disabled', false);
299            $('#ddm-image-upload-alt-text-input').val(imageAltText);
300            $('.ddm-image-upload-alt-text-input').show();
301        }
302    }
303}
304
305/**
306 * @deprecated since version 1.0.9
307 * Upload
308 * please use /modules/content-editor/media-editor instead
309 */
310
311var textKeyUploading = "ddm-page-text-image-upload-uploading";
312var textKeyChooseFile = "ddm-page-text-image-upload-choose-another-file";
313var textKeyError = "ddm-page-text-image-upload-error";
314var textKeySuccess = "ddm-page-text-image-upload-success";
315var textKeys = textKeyUploading + "," + textKeyChooseFile + "," + textKeyError + "," + textKeySuccess;
316
317window.onload = function() {
318
319    var adminLoggedIn = $("body").hasClass("ddm-admin-mode");
320    if(!adminLoggedIn) return;
321    LoadMultipleTextsFromDatabase(textKeys, function(texts){
322        InitUploadDialog(texts);
323    });
324
325};
326
327/**
328 * @deprecated since version 1.0.9
329 * Inits updload dialog using the passed texts from database
330 * @param {Array} texts 
331 */
332function InitUploadDialog(texts){
333    var textUploading = texts[textKeyUploading] != null ? texts[textKeyUploading] : "Uploading...";
334    var textChooseFile = texts[textKeyChooseFile] != null ? texts[textKeyChooseFile] : "Choose another file";
335    var textError = texts[textKeyError] != null ? texts[textKeyError] : "Upload failed.";
336    var textSuccess = texts[textKeySuccess] != null ? texts[textKeySuccess] : "successfully uploaded.";
337
338    var buttonSetImage = $('#ddm-upload-form-button-set-image');
339    buttonSetImage.prop('disabled', true);
340
341    buttonSetImage.unbind("click").bind("click", function(){
342
343        var imageKey = $('#imageKey').val();
344
345        var postData = {};
346        postData[POST_NAME_MEDIA_ID] = window.mediaID;
347        postData[POST_NAME_PAGE_MEDIA_KEY] = imageKey;
348        postData[POST_NAME_PAGE_MEDIA_ALT_TEXT] = $('#ddm-image-upload-alt-text-input').val();
349
350        jQuery.ajax({
351            url: "/ddm-framework/util/ajax/update-page-media.php",
352            type: 'POST',
353            responseType: 'json',
354            data: postData,
355            success:function(data){
356                var img = $('img[data-key="' + imageKey + '"');
357                img.attr("src", data.mediaFilePath);
358                img.attr("alt", $('#ddm-image-upload-alt-text-input').val());
359                SetImageEditorVisibility(false);
360            },
361            error:function (){
362
363            }
364        });
365    });
366
367    var buttonUpload = document.getElementById('uploadBtn'),
368        progressBar = document.getElementById('progressBar'),
369        progressOuter = document.getElementById('progressOuter'),
370        uploadMessageBox = document.getElementById('uploadMessageBox');
371
372    $('ddm-image-upload-preview').hide();
373    var imageUploader = NewUploader(
374        buttonUpload,
375        'imageUploadDropZone',
376        'image/*',
377        function(){ /* OnStartXHR */
378            progressOuter.style.display = 'block'; // make progress bar visible
379        },
380        function() { /* OnSubmit */
381            var data = {};
382            data[POST_NAME_MEDIA_TYPE_NAME] = "image";
383            imageUploader.setData(data);
384            uploadMessageBox.innerHTML = ''; // empty the message box
385            buttonUpload.innerHTML = textUploading;
386        },
387        function( filename, response ) { /* onComplete */
388            buttonUpload.innerHTML = textChooseFile;
389            progressOuter.style.display = 'none'; // hide progress bar when upload is completed
390            if ( !response ) {
391                uploadMessageBox.innerHTML = textError;
392                return;
393            }
394            if ( response.result === true ) {
395                buttonSetImage.prop('disabled', false);
396                $('#uploadForm').css('backgroundImage', 'url("' + EscapeTags( response.mediaFilePath ) + '")');
397                $('#uploadForm').css('backgroundSize', 'cover');
398                $('.ddm-image-upload-alt-text-input').show();
399                $('#ddm-image-upload-alt-text-input').focus();
400                $('#uploadForm').addClass('dark');
401                window.mediaFilePath = response.mediaFilePath;
402                window.mediaID = response.mediaID;
403            } else {
404                buttonSetImage.prop('disabled', true);
405                $('#ddm-image-upload-preview').hide();
406                if ( response.msg )  {
407                    uploadMessageBox.innerHTML = "Error: " + EscapeTags( response.message );
408                } else {
409                    uploadMessageBox.innerHTML = textError;
410                }
411                $('#uploadForm').css('backgroundImage', 'url(/ddm-framework/img/upload_image_error.png)');
412                $('#uploadForm').css('backgroundSize', '70%');
413                $('#uploadForm').addClass('dark');
414                $('.ddm-image-upload-alt-text-input').hide();
415                $('#ddm-image-upload-alt-text-input').val('');        
416            }
417        },
418        function() { /* onError */
419            progressOuter.style.display = 'none';
420            uploadMessageBox.innerHTML = 'Unable to upload file';
421            $('#uploadForm').css('backgroundImage', 'url(/ddm-framework/img/upload_image_error.png)');
422            $('#uploadForm').css('backgroundSize', '70%');
423            $('#uploadForm').addClass('dark');
424            $('.ddm-image-upload-alt-text-input').hide();
425            $('#ddm-image-upload-alt-text-input').val('');        
426        }
427    );
428    imageUploader.setProgressBar( progressBar );
429}
430
431
432/**
433 * Initializes a new ajax uploader
434 * @param {Element} uploadButton Button that should trigger the upload system
435 * @param {Element} dropzone Element that is used for drag-and-drop events (e.g. image is being dragged from desktop to this element and upload will start)
436 * @param {String} fileType Type of the file to be uploaded - in MIME type format (i.e. image/*)
437 * @param {Function} onStartXHR Called when upload starts
438 * @param {Function} onSubmit Called when upload was submitted
439 * @param {Function} onComplete Called when upload is completed successfully
440 * @param {Function} onError Called when an error occurred while uploading
441 */
442function NewUploader(uploadButton, dropzone, fileType, onStartXHR, onSubmit, onComplete, onError){
443    var imageUploader = new ss.SimpleUpload({
444        button: uploadButton,
445        dropzone: dropzone,
446        accept: fileType,
447        url: '/ddm-framework/modules/upload/upload.php',
448        progressUrl: '/ddm-framework/lib/upload/uploadProgress.php',
449        name: 'uploadfile',
450        multipart: true,
451        responseType: 'json',
452        debug: false,
453        startXHR: function(){
454            onStartXHR();
455        },
456        onSubmit: function(filename, extension, uploadBtn, size){
457            onSubmit(filename, extension, uploadBtn, size);
458        },
459        onComplete: function(filename, response, uploadBtn, size) {
460            onComplete(filename, response, uploadBtn, size);
461        },
462        onError: function(filename, type, status, statusText, response, uploadBtn, size){
463            onError(filename, type, status, statusText, response, uploadBtn, size);
464        },
465        onExtError: function( filename, extension ) {
466            onError("Error: Extension '." + extension + "' not allowed");
467        },
468        onSizeError: function( filename, fileSize, uploadBtn ) {
469            onError("Error: Filesize exceeded");
470        },
471    });
472    return imageUploader;
473}
474
475/* Cookies */
476
477/**
478 * Checks if the cookie 'cookiePermission' is set to "1" -> calls "DeleteAllCookies" if true
479 */
480function CheckCookiePermission(){
481    if(GetCookieValue('cookie-consent-accepted') != 'true'){
482        DeleteAllCookies();
483    }
484}
485
486/**
487 * Returns value of cookie with desired key "cookieName"
488 * @param {String} cookieName 
489 */
490function GetCookieValue(cookieName) {
491    var cookieValue = document.cookie.match('(^|[^;]+)\\s*' + cookieName + '\\s*=\\s*([^;]+)');
492    return cookieValue ? cookieValue.pop() : '';
493}
494
495/**
496 * Deletes all cookies except ddm-language
497 */
498function DeleteAllCookies() {
499    var cookies = document.cookie.split(";");
500
501    for (var i = 0; i < cookies.length; i++) {
502        var cookie = cookies[i];
503        var eqPos = cookie.indexOf("=");
504        var name = eqPos >
504 -1 ? cookie.substr(0, eqPos) : cookie;
505        document.cookie = name + "=;expires=Thu, 01 Jan 1970 00:00:00 GMT";
506    }
507}
508
509/**
510 * Sets a value ("cookieValue") to a cookie using key "cookieName" and expires "expireDays"
511 * @param {String} cookieName key of the cookie
512 * @param {String} cookieValue value of the cookie
513 * @param {Number} expireDays Amount of days the cookie shall be valid
514 */
515function SetCookie(cookieName, cookieValue, expireDays) {
516    var date = new Date();
517    date.setTime(date.getTime() + (expireDays*24*60*60*1000));
518    var expires = "expires="+ date.toUTCString();
519    document.cookie = cookieName + "=" + cookieValue + ";" + expires + ";path=/";
520}
521
522/* Util */
523
524/**
525 * Returns base url of website. E.g. https://ddm.studio/
526 * Does not end with a "/"
527 */
528function GetBaseUrl(){
529    var currentUrl = window.location;
530    return currentUrl.protocol + "//" + currentUrl.host;
531}
532
533/**
534 * Shows an error modal using sweetAlert, shows the desired text
535 * @param {String} errorText 
536 */
537function AjaxErrorAlert(errorText){
538    if(typeof Swal == "function")
539        Swal.fire({
540            icon: 'error',
541            html: errorText
542        });
543    else alert(errorText);
544}
545
546/**
547 * Sets the language cookie if the user accepted cookies before
548 * @param {String} name languageName
549 */
550function ChangeLanguage(name){
551    SetCookie(COOKIE_NAME_LANGUAGE, name, 31);
552    location.reload();
553}
554
555function EscapeTags( str ) {
556    return String( str )
557        .replace( /&/g, '&amp;' )
558        .replace( /"/g, '&quot;' )
559        .replace( /'/g, '&#39;' )
560        .replace( /</g, '&lt;' )
561        .replace( />/g, '&gt;' );
562}
563
564/**
565 * Removes all html tags from text while keeping the line breaks
566 * @param {string} html 
567 */
568function UntagHtml(html)
569{
570  html = html.replace(/<br>/g,"$br$"); 
571  html = html.replace(/(?:\r\n|\r|\n)/g, '$n$');
572  var tmp = document.createElement("DIV");
573  tmp.innerHTML = html;
574  html = tmp.textContent||tmp.innerText;
575  html = html.replace(/\$br\$/g,"<br>");  
576  html = html.replace(/\$n\$/g,"<br>");
577  return html;
578}
579/*
580----------------------------------------------------------------------------------------------------------
581----------------------------------------------------------------------------------------------------------
582--																										--
583--											DYNAMIC UI													--
584--																										--
585----------------------------------------------------------------------------------------------------------
586----------------------------------------------------------------------------------------------------------
587*/
588
589$(document).ready(function(){
590    $(document).on('click', '.ddm-dui-add-parent', function(event){
591        event.preventDefault();
592        event.stopPropagation();
593        var contextMenu = $(this).parent().find('.ddm-context-menu');
594        if(contextMenu.children('ul').children().length == 1)
595            $(contextMenu).find('li').click();
596        else contextMenu.toggleClass("active");
597    });
598
599    /* Load more functionality */
600    $(document).on('click', '[data-dui-load-more="true"]', function(event){
601        LoadMore(GetCollectionDefinitionKey(this));
602    });
603});
604
605/**
606 * Uses the passed element to find out the collection definition key of the parent collection div
607 * @param {Element} element Any child element of dui collection
608 * @returns string collectionDefinitionKey
609 */
610function GetCollectionDefinitionKey(element){
611    return $(element).parents('.ddm-dui-collection-container').attr('data-dui-collection-definition-key');
612}
613
614/**
615 * Adds a new DUI parent in the database and appends a new one to the user interface
616 * @param {String} collectionDefinitionKey 
617 * @param {String} parentDefinitionKey 
618 * @param {bool} openDuiParentEditor
619 */
620function AddDuiParent(collectionDefinitionKey, parentDefinitionKey, openDuiParentEditor){
621    let pageTag = $('#pageTag').val();
622
623    if(openDuiParentEditor){
624        location.href = GetBaseUrl()
625        + "/ddm-framework/modules/dynamic-ui/form/dynamic-ui-parent-editor-form.php?"
626        + POST_NAME_DUI_COLLECTION_DEFINITION_KEY + "=" + collectionDefinitionKey + "&"
627        + POST_NAME_DUI_PARENT_DEFINITION_KEY + "=" + parentDefinitionKey + "&"
628        + POST_NAME_PAGE_TAG + "=" + pageTag + "&"
629        + POST_NAME_PAGE_REFERRER + "=" + location.href;
630        return;
631    }
632
633    var postData = { };
634    postData[POST_NAME_DUI_COLLECTION_DEFINITION_KEY] = collectionDefinitionKey;
635    postData[POST_NAME_DUI_PARENT_DEFINITION_KEY] = parentDefinitionKey;
636    postData[POST_NAME_PAGE_TAG] = pageTag;
637    postData[POST_NAME_DUI_OPTIONS] = JSON.stringify(GetDuiOptions(collectionDefinitionKey));
638
639    var parentAppendingElement = $('div[data-dui-collection-definition-key="' + collectionDefinitionKey + '"]').children('*[data-dui-parent-addition-elememt="true"]');
640    var url = "/ddm-framework/modules/dynamic-ui/ajax/create-parent.php";
641    $.ajax({
642        type: "POST",
643        url: url,
644        data: postData,
645        dataType: 'json',
646        success: function(response, text)
647        {
648            if(response.result){
649                let descending = GetDuiOptions(collectionDefinitionKey).descending;
650                if(descending)
651                    $(response.data).insertAfter(parentAppendingElement).hide().show('normal');
652                else $(response.data).insertBefore(parentAppendingElement).hide().show('normal');
653
654                let $parent = $(response.data);
655                let parentID = $parent.attr("data-parent-id");
656                $(document).trigger(EVENT_LISTENER_DYNAMIC_UI_PARENT_ADDED, { collectionDefinitionKey: collectionDefinitionKey, parentID: parentID });
657            }
658            else AjaxErrorAlert("Error while adding element: " + response.message);
659            parentAppendingElement.find('.ddm-context-menu').removeClass("active");
660        },
661        error: function(request, status, error){
662            AjaxErrorAlert("Error while adding element: " + request.responseText);
663        }
664    });
665}
666
667/**
668 * Deletes a DUI parent from database and removes it from the user interface
669 * @param {Event} event 
670 * @param {Number} parentID 
671 */
672function DeleteDuiParentFromCollection(event, parentID){
673    $(event.target).parents(".ddm-context-menu").remove(); // remove context menu from DOM
674
675    DeleteDuiParent(parentID, 
676        function(data){
677            // success
678            let parent = $('#dui_parent_' + parentID);
679            let collectionDefinitionKey = parent.closest(".ddm-dui-collection-container").attr("data-dui-collection-definition-key");
680            if(data.result == true)
681                parent.hide('normal', function(){ 
682                    parent.remove();
683                    $(document).trigger(EVENT_LISTENER_DYNAMIC_UI_PARENT_DELETED, { collectionDefinitionKey: collectionDefinitionKey, parentID: parentID });
684                });
685            else AjaxErrorAlert("Error while deleting element: " + data.message);
686        },
687        function(errorText){
688            // error
689            AjaxErrorAlert("Unknown eror while deleting parent element: " + errorText);
690        }
691    );
692}
693
694/**
695 * Deletes a DUI parent from database and calls 'successUrl' on success
696 * @param {Event} event 
697 * @param {String} successUrl URL to be opened after deleting the parent. If null: function tries to remove the parent from DOM
698 */
699function DeleteDuiParentWithUserConfirmation(parentID, successUrl){
700    Swal.fire({
701        icon : "question",
702        title : "Delete?",
703        text : "All linked components (pages, texts) are also deleted during the deletion process. Are you sure you want to continue deleting?",
704        showCloseButton: true,
705        showCancelButton: true,
706        showConfirmButton: true
707    }).then(function(result){
708        if (!result.value) return;
709
710        DeleteDuiParent(parentID, 
711            function(data){
712                // success
713                if(data.result == true){
714                    if(successUrl)
715                        window.location.href = successUrl;
716                    else{
717                        let parent = $('#dui_parent_' + parentID);
718                        let collectionDefinitionKey = parent.closest(".ddm-dui-collection-container").attr("data-dui-collection-definition-key");
719                        parent.hide('normal', function(){ 
720                            parent.remove();
721                            $(document).trigger(EVENT_LISTENER_DYNAMIC_UI_PARENT_DELETED, { collectionDefinitionKey: collectionDefinitionKey, parentID: parentID });
722                        });    
723                    }
724                }
725                else AjaxErrorAlert("Error while deleting element: " + data.message);
726            },
727            function(errorText){
728                // error
729                AjaxErrorAlert("Unknown eror while deleting parent element: " + errorText);
730            }
731        );
732    });
733}
734
735/**
736 * Deletes a DUI parent from database
737 * @param {Number} parentID 
738 * @param {Function} onSuccess called when parent has been deleted successfully
739 * @param {Function} onError called when an error occurred while deleting the parent
740 */
741function DeleteDuiParent(parentID, onSuccess, onError){
742    var url = "/ddm-framework/modules/dynamic-ui/ajax/delete-parent.php";
743
744    var postData = {};
745    postData[POST_NAME_DUI_PARENT_ID] = parentID;
746
747    $.ajax({
748        type: "POST",
749        url: url,
750        data: postData,
751        dataType: 'json',
752        success: function(data)
753        {
754            if(typeof onSuccess === 'function')
755                onSuccess(data);
756        },
757        error: function(request, status, error){
758            if(typeof onError === 'function')
759                onError(request.responseText);
760            AjaxErrorAlert("Unknown eror while deleting parent element: " + request.responseText);
761        }
762    });
763}
764
765/**
766 * Changes the sort order of a parent using ajax and swaps 2 elements in the user interface
767 * @param {Number} parentID 
768 * @param {String} changeDirection "forward" or "backward"
769 */
770function ChangeDuiSortOrder(event, parentID, changeDirection){
771    var url = "/ddm-framework/modules/dynamic-ui/ajax/change-parent-sort-order.php";
772
773    var postData = {};
774    postData[POST_NAME_DUI_PARENT_ID] = parentID;
775
776    var parent = $('#dui_parent_' + parentID);
777
778    let descending = GetDuiOptions(GetCollectionDefinitionKey(parent)).descending;
779    let databaseChangeDirection = changeDirection;
780
781    if(changeDirection == 'forward' && descending) // change direction if the sort order is descending
782        databaseChangeDirection = 'backward';
783  
784    if(changeDirection == 'backward' && descending) // change direction if the sort order is descending
785        databaseChangeDirection = 'forward';
786
787    postData[POST_NAME_DUI_SORT_ORDER_CHANGE_DIRECTION] = databaseChangeDirection;
788    
789    $(event.target).parents(".ddm-context-menu").remove(); // remove context menu from DOM
790
791    $.ajax({
792        type: "POST",
793        url: url,
794        data: postData,
795        dataType: 'json',
796        success: function(data)
797        {
798            if(data.result != true){ 
799                AjaxErrorAlert("Eror while changing sort order of parent element: " + data.message);
800                return;
801            }
802
803            // swap them
804            if(changeDirection == 'forward'){
805                var swapParent = parent.prev(); 
806                swapParent.swap({  
807                    target: parent.attr('id'),
808                    opacity: "1",
809                    speed: 300,
810                    callback: function() {
811                        swapParent.removeAttr('style');
812                        parent.removeAttr('style');
813                        swapParent.before(parent);
814                    }  
815                });  
816            }
817            else if(changeDirection == 'backward'){
818                var swapParent = parent.next();
819                if(swapParent.attr('data-dui-parent-addition-elememt') != 'true'){ // admin parent item used to add items, don't change its position
820                    parent.swap({  
821                        target: swapParent.attr('id'),
822                        opacity: "1",
823                        speed: 300,
824                        callback: function() {
825                            swapParent.removeAttr('style');
826                            parent.removeAttr('style');
827                            parent.before(swapParent);
828                        }  
829                    });  
830                }
831            }
832        },
833        error: function(request, status, error){
834            AjaxErrorAlert("Unknown eror while changing sort order of parent element: " + request.responseText);
835        }
836    });
837}
838
839/**
840 * Opens the dynamic ui parent editor
841 * @param {Number} parentID 
842 */
843function OpenDuiParentEditor(parentID){
844    var parentElement = $('#dui_parent_' + parentID);
845    var form = parentElement.children('form');
846    form.find('[name="' + POST_NAME_PAGE_REFERRER + '"]').val(location.href);
847    form.attr("action", "/ddm-framework/modules/dynamic-ui/form/dynamic-ui-parent-editor-form.php");
848    form.submit();
849}
850
851/**
852 * Returns options of collection with passed collectionDefinitionKey
853 * Javascript equivalent object of /ddm-framework/modules/dynamic-ui/dui-options.php
854 * @param {String} collectionDefinitionKey
855 */
856function GetDuiOptions(collectionDefinitionKey){
857    let optionsRaw = $('div[data-dui-collection-definition-key="' + collectionDefinitionKey + '"]' + ' > input[name="' + POST_NAME_DUI_OPTIONS + '"]').val();
858    let options = {};
859    if(optionsRaw)
860        options = JSON.parse(optionsRaw);
861    return options;
862}
863
864/**
865 * Updates the options input field with the passed options (converts them to json before)
866 * @param {String} collectionDefinitionKey 
867 * @param {Object} options 
868 */
869function SetDuiOptions(collectionDefinitionKey, options){
870    let optionsJson = JSON.stringify(options);
871    $('div[data-dui-collection-definition-key="' + collectionDefinitionKey + '"]' + ' input[name="' + POST_NAME_DUI_OPTIONS + '"]').val(optionsJson);
872}
873
874/**
875 * Reloads collection using the passed collectionDefinitionKey and options
876 * @param {string} collectionDefinitionKey
877 * @param {Object} options Javascript equivalent object of /ddm-framework/modules/dynamic-ui/dui-options.php
878 */
879function ReloadDuiCollection(collectionDefinitionKey, options, onSuccess){
880    if(!options) options = {};
881
882    var postData = {};
883    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
884    postData[POST_NAME_DUI_COLLECTION_DEFINITION_KEY] = collectionDefinitionKey;
885    postData[POST_NAME_DUI_OPTIONS] = JSON.stringify(options);
886    $.ajax({
887        url: "/ddm-framework/modules/dynamic-ui/ajax/load-collection.php",
888        data: postData,
889        type: "POST",
890        dataType: 'json',
891        success: function(response, text)
892        {
893            if(response.result){
894                $('[data-dui-collection-definition-key="' + collectionDefinitionKey + '"]').replaceWith(response.data);
895                let parentCount = $(response.data).children('.ddm
895-dui-parent').not('[data-dui-parent-addition-elememt="true"]').length;
896                if(typeof onSuccess === 'function')
897                    onSuccess(parentCount);
898            }
899            else AjaxErrorAlert("Error while filtering: " + response.message);
900        },
901        error: function(request, status, error){
902            AjaxErrorAlert("Error while filtering: " + request.responseText);
903        }
904    });
905}
906
907/**
908 * Loads parents from database using the passed collectionDefinitionKey
909 * Appends the newly loaded parents to the collection
910 * @param {string} collectionDefinitionKey
911 */
912function LoadMore(collectionDefinitionKey){
913    let options = GetDuiOptions(collectionDefinitionKey);
914    var loadMoreWrapper = $('[data-dui-collection-definition-key="' + collectionDefinitionKey + '"] .ddm-dui-load-more');
915    loadMoreWrapper.find('.ddm-button').hide();
916    var progressCircle = loadMoreWrapper.find('progress');
917    progressCircle.show();
918
919    options.startIndex += options.maxElementCount;
920
921    var postData = {};
922    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
923    postData[POST_NAME_DUI_COLLECTION_DEFINITION_KEY] = collectionDefinitionKey;
924    postData[POST_NAME_DUI_OPTIONS] = JSON.stringify(options);
925    $.ajax({
926        url: "/ddm-framework/modules/dynamic-ui/ajax/load-parents.php",
927        data: postData,
928        type: "POST",
929        dataType: 'json',
930        success: function(response, text)
931        {
932            progressCircle.hide();
933            if(response.result){
934                $(response.data).insertBefore(loadMoreWrapper).hide().show('normal');
935                loadMoreWrapper.remove();
936                SetDuiOptions(collectionDefinitionKey, options);
937            }
938            else AjaxErrorAlert("Error while filtering: " + response.message);
939        },
940        error: function(request, status, error){
941            progressCircle.hide();
942            AjaxErrorAlert("Error while filtering: " + request.responseText);
943        }
944    });
945}
946/** Cookie Consent **/
947
948const cookieTypes = [
949    "essential",
950    "functional",
951    "statistics",
952    "marketing"
953];
954
955$(function(){
956    $(".cookie-consent-popover").each(function(index, element){
957        let popover = $(element);
958        let popoverOptions = {
959            content: function () {
960                return popover.siblings('.cookie-consent-popover-content').html();
961            },
962            html: true,
963            sanitize: false
964        };
965
966        popover.popover(popoverOptions);
967    });
968    
969    // close popovers on outside click
970    $("html").on("mousedown", function (e) {
971        var l = $(e.target);
972        if (l.parents(".popover").length == 0 && l.parents(".medium-editor-toolbar").length == 0) {
973            $(".popover").each(function () {
974                $(this).popover("hide");
975            });
976        }
977    });
978
979    $(document).on('click', '#cookie-consent-button-accept-all', function(event){
980        event.preventDefault();
981        $('.cookie-consent-option input[type="checkbox"]').each(function(index, item){
982            let $this = $(item);
983            let cookieType = $this.attr("data-cookie-type");
984            $this.prop("checked", true);
985            SetCookie("cookie-permission-" + cookieType, true, 365);
986         
987            SetCookieTrigger(cookieType);
988        });
989        SetCookie("cookie-consent-accepted", true, 365);
990        setTimeout(HideCookieConsent, 400);
991    });
992
993    $(document).on('click', '#cookie-consent-button-accept-selected', function(event){
994        event.preventDefault();
995        cookieTypes.forEach(function(cookieType, index){
996            let cookieAccepted = $('#checkbox-cookie-type-' + cookieType).prop("checked");
997            SetCookie("cookie-permission-" + cookieType, cookieAccepted, 365);
998            if(cookieAccepted)
999                SetCookieTrigger(cookieType);
1000        });
1001        SetCookie("cookie-consent-accepted", true, 365);
1002        HideCookieConsent();
1003    });
1004
1005    if(GetCookieValue("cookie-consent-accepted") !== "true"){
1006        ShowCookieConsent();
1007    }
1008});
1009
1010function ShowCookieConsent(){
1011    $('.cookie-consent-overlay, .cookie-consent').addClass('active');
1012    cookieTypes.forEach(function(cookieType, index){
1013        $('#checkbox-cookie-type-' + cookieType).prop("checked", GetCookieValue("cookie-permission-" + cookieType) == "true");
1014    });
1015    $('#checkbox-cookie-type-essential').prop("checked", true);
1016}
1017
1018function HideCookieConsent(){
1019    $('.cookie-consent-overlay, .cookie-consent').removeClass('active');
1020}
1021
1022function SetCookieTrigger(cookieType){
1023    switch(cookieType){
1024        case "statistics":
1025            window.dataLayer.push({
1026                event: 'ddmStatisticalCookieTrigger'
1027            });
1028        break;
1029        case "marketing":
1030            window.dataLayer.push({
1031                event: 'ddmMarketingCookieTrigger'
1032            });
1033        break;
1034        default:
1035            // nothing to do here
1036        break;
1037    }
1038}
1039/** Text Editor **/
1040
1041$(function(){
1042    $(document.body).on('paste','.ddm-admin-textinput', function(event){
1043      //  event.preventDefault();
1044        // let originalEvent = event.originalEvent;
1045        // let pastedText = '';
1046        // if (typeof originalEvent.clipboardData === 'undefined')
1047        //     pastedText = window.clipboardData.getData('Text');
1048        // else
1049        //     pastedText = originalEvent.clipboardData.getData('text/plain');
1050        
1051        // var untaggedText = UntagHtml(pastedText);
1052        // PasteHtmlAtCaret(untaggedText);
1053        SetEditorActionContainerPosition($(this), $("#textEditorActions"));
1054    });
1055});
1056
1057
1058/**
1059 * 
1060 * @param {function} event callback
1061 * @param {string} key elementId of label
1062 * Creates an editable textField of a passed label (key), adds text editor action buttons besides the label
1063 */
1064function MakeTextEditable(event, element){
1065    event.stopPropagation();
1066    event.preventDefault();
1067
1068    $("#urlEditorActions").hide();
1069    
1070    var textField = $(element);
1071    var key = textField.attr('data-key');
1072
1073    if(textField.attr('contenteditable') === 'true')
1074        return; // do not start editing again if it is already active
1075
1076    if(textField.parents(".ddm-editable-url-box").length > 0){ // textField is child of EditableUrlBox
1077        if(event.ctrlKey) return; // do not start editing text field if it is a child of editableUrlBox and user holds ctrlKey down to open the url
1078    }
1079
1080    var onOriginalValueRestored = function(){
1081        var isParentAnchor = textField.parent().prop("tagName") == "A";
1082        var isEditableUrl = isParentAnchor 
1083                            & textField.parent().attr("data-editable") == "true" 
1084                            & !textField.parent().hasClass("ddm-editable-url-box");
1085        var extendedTextFormatting = $('#extended-text-formatting').val() == 1;
1086
1087        if(isEditableUrl){
1088            ShowUrlEditorActions(textField);
1089            return;
1090        }
1091
1092        var originalValue = textField.val(); //store the value of the element
1093
1094        textField.attr("contenteditable", "true");
1095        textField.addClass(classNameTextFieldEditMode);
1096        textField.focus();
1097        textField.val(''); //clear the value of the element
1098        textField.val(originalValue);
1099
1100        let mediumEditorButtons = [
1101            'bold', 
1102            'italic', 
1103            'underline', 
1104            'strikethrough',
1105            'html'
1106        ];
1107        if(!isParentAnchor)
1108            mediumEditorButtons.push('anchor');
1109
1110        if(extendedTextFormatting){
1111            mediumEditorButtons.push('orderedlist');
1112            mediumEditorButtons.push('unorderedlist');
1113        }
1114
1115        let allowHtmlPasing = (textField.attr("data-html-paste") == 'true');
1116
1117        mediumEditor = new MediumEditor(element, {
1118            autoLink: false,
1119            buttonLabels: 'fontawesome',
1120            spellcheck: false,
1121            imageDragging: false,
1122            placeholder: false,
1123            paste: {
1124                forcePlainText: !allowHtmlPasing,
1125                cleanPastedHTML: !allowHtmlPasing,
1126                // cleanTags: ['meta', 'p'],
1127                // unwrapTags: ['p'],
1128                doPaste: function (pastedHTML, pastedPlain, editable) {
1129                    if(!allowHtmlPasing){
1130                        var untaggedText = UntagHtml(pastedPlain);
1131                        PasteHtmlAtCaret(untaggedText);
1132                    }
1133                    SetEditorActionContainerPosition(textField, $("#textEditorActions"));
1134                }
1135            },
1136            toolbar: {
1137                /* These are the default options for the toolbar,
1138                   if nothing is passed this is what is used */
1139                allowMultiParagraphSelection: true,
1140                buttons: mediumEditorButtons,
1141                diffLeft: 0,
1142                diffTop: -10,
1143                firstButtonClass: 'medium-editor-button-first',
1144                lastButtonClass: 'medium-editor-button-last',
1145                relativeContainer: null,
1146                standardizeSelectionStart: false,
1147                static: false,
1148                /* options which only apply when static is true */
1149                align: 'center',
1150                sticky: false,
1151                updateOnEmptySelection: true
1152            },
1153            anchor: {
1154                targetCheckbox: true,
1155                targetCheckboxText: 'Open in new tab'
1156            }
1157        });
1158
1159        var textEditorActionContainer = $("#textEditorActions");
1160        SetEditorActionContainerPosition(textField, textEditorActionContainer);
1161
1162        var eventType = /Trident/.test( navigator.userAgent ) ? 'keyup' : 'input';
1163
1164        textField.unbind(eventType).bind(eventType, function() {
1165            // change position of text editor action container when the textField size changes
1166            SetEditorActionContainerPosition(textField, textEditorActionContainer);
1167        });
1168
1169        window.addEventListener("resize", function(){
1170            if(textField.hasClass(classNameTextFieldEditMode))
1171                SetEditorActionContainerPosition(textField, textEditorActionContainer);
1172        });
1173
1174        var saveButton = $("#textEditorActionSave");
1175        var cancelButton = $("#textEditorActionCancel");
1176        
1177        saveButton.unbind('click').bind('click', function(){
1178            var text = textField.html();
1179            if(!IsTextValid(text)){
1180                ShowTextInvalidAlert(function(){
1181                    textField.focus();
1182                });
1183                return;
1184            }
1185
1186            let breakPlaceholder = '%%BREAK_PLACEHOLDER%%';
1187            text = text.replace(/<p[^>]*><br><\/p>/g, breakPlaceholder);
1188            text = text.replace(/<br><\/p>/g, breakPlaceholder);
1189            text = text.replace(/<br>/g, breakPlaceholder);
1190            text = text.replace(/<\/p>/g, breakPlaceholder);
1191            text = text.replace(/<p[^>]*>/g, '');
1192            text = text.replace(/<div[^>]*>/g, '').replace(/<\/div>/g, '');
1193            text = text.replace(/%%BREAK_PLACEHOLDER%%/g, '<br>');
1194
1195            // check if text ends with "<br>"
1196            // replace break tag if text ends with a single break tag and not with multiple (like '<br><br>')
1197            const BreakTag = "<br>";
1198            const BreakTagLength = BreakTag.length;
1199            if(text.substring(text.length - BreakTagLength, text.length) === BreakTag){
1200                if(text.substring(text.length - 2 * BreakTagLength, text.length - BreakTagLength) !== BreakTag)
1201                    text = text.substring(0, text.length - BreakTagLength);
1202            }
1203
1204            let isParameter = textField.attr("data-is-parameter") === "true";
1205            let isVariable = textField.attr("data-parameter-is-variable") === "true";
1206
1207            if(isParameter){
1208                UpdateParameterInDatabase(key, text, isVariable, function(){
1209                    textEditorActionContainer.hide();
1210                    $("label[data-key='" + key + "']").each(function(){
1211                        $(this).attr("contenteditable", "false").html(text);
1212                    });
1213                });            
1214            } else {
1215                SaveTextToDatabase(key, text, function(){
1216                    textEditorActionContainer.hide();
1217                    $("label[data-key='" + key + "']").each(function(){
1218                        $(this).attr("contenteditable", "false").html(text);
1219                    });
1220                });
1221            }
1222         
1223            ResetTextFieldToDefault(textField);
1224        });
1225
1226        cancelButton.unbind('click').bind('click', function(){
1227            RestoreOriginalValue(textField);
1228            textEditorActionContainer.hide();
1229        });
1230
1231        textEditorActionContainer.show();
1232    }
1233
1234    // reset other active text editor fields to default
1235    RestoreOriginalValue($('label[contenteditable="true"]:first'), onOriginalValueRestored);
1236}
1237
1238function RestoreOriginalValue(textField, onFinishedCallback){
1239    if(textField == null || textField.length == 0){
1240        if(onFinishedCallback != undefined)
1241            onFinishedCallback();
1242        return;
1243    }
1244    if(mediumEditor !== null) mediumEditor.destroy();
1245    var key = textField.attr('data-key');
1246
1247    let isParameter = textField.attr("data-is-parameter") === "true";
1248    
1249    if(isParameter){
1250        LoadParameterFromDatabase(key, function(parameterValue){
1251            textField.html(parameterValue);
1252            ResetTextFieldToDefault(textField);
1253        });
1254    }else{
1255        LoadTextFromDatabase(key, function(text){
1256            textField.html(text);
1257            ResetTextFieldToDefault(textField);
1258        });
1259    }
1260    
1261}
1262
1263function LoadTextFromDatabase(translationKey, callback){
1264    var postData = { };
1265    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
1266    postData[POST_NAME_TRANSLATION_KEY] = translationKey;
1267    jQuery.ajax({
1268        url: "/ddm-framework/util/ajax/load-page-text-single.php",
1269        type: 'POST',
1270        data: postData,
1271        responseType: 'json',
1272        success:function(response){
1273            if(response.result == true){
1274                if(typeof callback === "function")
1275                    callback(response.data);
1276            }else AjaxErrorAlert("something went wrong, could load text from database: " + response.message);
1277        },
1278        error:function (request, status, error){
1279            AjaxErrorAlert("something went wrong, could load text from database: " + request.responseText);
1280        }
1281    });
1282}
1283
1284function LoadParameterFromDatabase(parameterKey, callback){
1285    var postData = { };
1286    postData[POST_NAME_PAGE_TAG] = $('#pageTag').val();
1287    postData[POST_NAME_PARAMETER_KEY] = parameterKey;
1288    jQuery.ajax({
1289        url: "/ddm-framework/util/ajax/load-parameter-value.php",
1290        type: 'POST',
1291        data: postData,
1292        responseType: 'json',
1293        success:function(response){
1294            if(response.result == true){
1295                if(typeof callback === "function")
1296                    callback(response.data);
1297            }else AjaxErrorAlert("something went wrong, could load text from database: " + response.message);
1298        },
1299        error:function (request, status, error){
1300            AjaxErrorAlert("something went wrong, could load text from database: " + request.responseText);
1301        }
1302    });
1303}
1304
1305/**
1306 * Sets contenteditable to false and removes the active editor styles
1307 * Label is in default mode afterwards
1308 * @param {element} textField label with contenteditable tag
1309 */
1310function ResetTextFieldToDefault(textField){
1311    textField.attr("contenteditable", "false");
1312    textField.removeClass("ddm-admin-textinput-active");
1313    textField.unbind("input");
1314}
1315
1316/**
1317 * Returns false when text contains nothing other than spaces or is empty
1318 * @param {string} text 
1319 */
1320function IsTextValid(text){
1321    return text.length >= 1 && text.replace(/&nbsp;/g, "").trim() != '';
1322}
1323
1324/**
1325 * shows an alert telling the user that the entered text is too short
1326 * @param {function} onConfirm callback when user clicks on the confirmation button
1327 */
1328function ShowTextInvalidAlert(onConfirm){
1329    var textKeyTitle = "ddm-page-text-text-editor-text-too-short-title";
1330    var textKeyMessage = "ddm-page-text-text-editor-text-too-short-message";
1331    var textKeyButtonText = "ddm-page-text-ok";
1332    var textKeys = textKeyTitle + "," + textKeyMessage + "," + textKeyButtonText;
1333    LoadMultipleTextsFromDatabase(textKeys, function(texts){
1334        var title = texts[textKeyTitle] != null ? texts[textKeyTitle] : "Oops!";
1335        var message = texts[textKeyMessage] != null ? texts[textKeyMessage] : "Text must contain at least one character.";
1336        var buttonText = texts[textKeyButtonText] != null ? texts[textKeyButtonText] : "OK";
1337        Swal.fire({
1338            title: title,
1339            text: message,
1340            icon: 'error',
1341            confirmButtonText: buttonText
1342          }).then( function(result) {
1343            if (result.value) {
1344                if (onConfirm != null && onConfirm instanceof Function) {
1345                    onConfirm();
1346                }
1347            }
1348          });
1349    });
1350}
1351
1352/**
1353 * Sets position of text editor action container based on the textField position
1354 * @param {element} textField label with contenteditable tag
1355 */
1356function SetEditorActionContainerPosition(textField, editorActionContainer){
1357    var fixed = IsElementOrParentFixed(textField);
1358
1359    editorActionContainer.attr("style", ''); // clear inline styles
1360    editorActionContainer.css("position", fixed ? "fixed" : "absolute");
1361    if(fixed)
1362        editorActionContainer.css("z-index", "710");
1363
1364    var leftPos = textField.offset().left + textField.width() + 10;
1365    var viewPortWidth = $(window).width();
1366    if(leftPos + editorActionContainer.width() > viewPortWidth)
1367        leftPos = viewPortWidth - editorActionContainer.width() - 5;
1368    editorActionContainer.css("left", leftPos + 'px');
1369    if(fixed)
1370    editorActionContainer.css("top", textField.offset().top - $(document).scrollTop() + 'px');
1371    else editorActionContainer.css("top", textField.offset().top + 'px');
1372}
1373
1374/**
1375 * Pastes text into active textField
1376 * @param {text} html 
1377 */
1378function PasteHtmlAtCaret(html) {
1379    var sel, range;
1380    if (window.getSelection) {
1381        // IE9 and non-IE
1382        sel = window.getSelection();
1383        if (sel.getRangeAt && sel.rangeCount) {
1384            range = sel.getRangeAt(0);
1385            range.deleteContents();
1386
1387            // Range.createContextualFragment() would be useful here but is
1388            // only relatively recently standardized and is not supported in
1389            // some browsers (IE9, for one)
1390            html = html.replace(/<p[^>]*>/g, '').replace(/<\/p>/g, '<br><br>');
1391            html = html.replace(/<div[^>]*>/g, '').replace(/<\/p>/g, '<br><br>');
1392            var el = document.createElement("div");
1393            el.innerHTML = html;
1394            var frag = document.createDocumentFragment(), node, lastNode;
1395            while ( (node = el.firstChild) ) {
1396                lastNode = frag.appendChild(node);
1397            }
1398            range.insertNode(frag);
1399
1400            // Preserve the selection
1401            if (lastNode) {
1402                range = range.cloneRange();
1403                range.setStartAfter(lastNode);
1404                range.collapse(true);
1405                sel.removeAllRanges();
1406                sel.addRange(range);
1407            }
1408        }
1409    } else if (document.selection && document.selection.type != "Control") {
1410        // IE < 9
1411        document.selection.createRange().pasteHTML(html);
1412    }
1413}
1414
1415    const validImageUploadFileExtensions = ['gif','png','jpg','jpeg','svg'];
1416    const validVideoUploadFileExtensions = ['mp4','webm'];
1417    const validAudioUploadFileExtensions = ['wav','mpeg','ogg','mp3'];
1418    const validDocumentUploadFileExtensions = ['pdf'];
1419
1420    const acceptImageUploadFileExtensions = '.gif,.png,.jpg,.jpeg,.svg';
1421    const acceptVideoUploadFileExtensions = '.mp4,.webm';
1422    const acceptAudioUploadFileExtensions = '.wav,.mpeg,.ogg,.mp3';
1423    const acceptDocumentUploadFileExtensions = '.pdf';
1424
1425    const maxUploadFileSizeImage = 10000;
1426    const maxUploadFileSizeVideo = 100000;
1427    const maxUploadFileSizeAudio = 120000;
1428    const maxUploadFileSizeDocument = 30000;
1429    const textMediaEditorUploading = "Wird hochgeladen...";
1430    const textMediaEditorChooseFile = "Datei auswählen";
1431    const textMediaEditorError = "Fehlgeschlagen. Upload war nicht erfolgreich.";
1432    const textMediaEditorSuccess = "Erfolgreich hochgeladen.";
1433/** Media Editor **/
1434
1435function MediaEditor(mediaKey){
1436    $('*[data-media-reference="' + mediaKey + '"]').click();
1437}
1438
1439$(function(){
1440    InitMediaEditor();
1441});
1442
1443var fileBuffer = null;
1444var replaceFileIfExists = false;
1445
1446
1447function InitMediaEditor(){
1448
1449    /* Variables */
1450    var mediaUploader = null; // initialized later
1451    var mediaUploadForm = $('#media-upload-form');
1452    var videoPreviewWrapper = $('.media-editor-video-preview-wrapper');
1453    var audioPreviewWrapper = $('.media-editor-audio-preview-wrapper');
1454    var documentPreviewWrapper = $('.media-editor-document-preview-wrapper');
1455    var altTextContainer = $('.ddm-image-upload-alt-text-input');
1456    var altTextInput = $('#ddm-media-editor-alt-text');
1457    var uploadMessageBoxSuccess = $('#upload-message-box-success');
1458    var uploadMessageBoxError = $('#upload-message-box-error');
1459    var buttonMediaEditorSet = $('#ddm-media-upload-form-button-set');
1460    var buttonMediaEditorReplaceFile = $('#ddm-media-upload-form-button-replace-file');
1461    var buttonMediaEditorCancel = $('#ddm-media-upload-form-button-cancel');
1462    var storageMediaKey = $('#ddm-media-editor-media-key');
1463    var storageMediaID = $('#ddm-media-editor-media-id');
1464    var storageMediaFilePath = $('#ddm-media-editor-media-file-path');
1465    var storageMediaMimeType = $('#ddm-media-editor-media-mime-type');
1466
1467    function SetMediaEditorVisibility(visible){
1468        var editor = $('#ddm-media-editor');
1469        if(visible) 
1470            editor.show();
1471        else editor.hide();
1472    }
1473
1474    $("body").on("click", "*[data-editor='true']", function(event){
1475        event.preventDefault();
1476        event.stopPropagation();
1477
1478        var editorButton = $(this);
1479        var mediaKey =  editorButton.attr("data-media-reference");
1480        var mediaType = editorButton.attr("data-media-type");
1481        var mediaSrc = editorButton.attr("data-media-src");
1482        var mediaAlt = editorButton.attr("data-media-alt");
1483        var mediaID = editorButton.attr("data-media-id");
1484
1485        InitMediaEditor(mediaType);
1486        BufferMediaInformation(mediaKey, mediaID, mediaSrc);
1487        var mediaElement = $('*[data-key="' + mediaKey + '"]');
1488
1489        if(mediaType === MEDIA_TYPE_IMAGE)
1490            SetupEditorWithImage(mediaSrc, mediaAlt);
1491        if(mediaType === MEDIA_TYPE_VIDEO)
1492            SetupEditorWithVideo(mediaSrc, mediaElement.attr("type"));
1493        if(mediaType === MEDIA_TYPE_AUDIO)
1494            SetupEditorWithAudio(mediaSrc, mediaElement.attr("type"));
1495        if(mediaType === MEDIA_TYPE_DOCUMENT)
1496            SetupEditorWithDocument(mediaSrc, mediaElement.attr("type"));
1497
1498        SetMediaEditorVisibility(true);
1499    });
1500
1501    function InitMediaEditor(mediaType){
1502        mediaUploadForm.removeClass('dark');
1503        mediaUploadForm.css('backgroundImage', 'none');
1504        altTextContainer.hide();
1505        altTextInput.val('');
1506        videoPreviewWrapper.hide();
1507        audioPreviewWrapper.hide();
1508        documentPreviewWrapper.hide();
1509        uploadMessageBoxSuccess.hide();
1510        uploadMessageBoxError.empty();
1511        buttonMediaEditorSet.show();
1512        buttonMediaEditorSet.prop('disabled', true);
1513        buttonMediaEditorReplaceFile.hide();
1514        replaceFileIfExists = false;
1515
1516        // init uploader
1517        if(mediaUploader != null)
1518            mediaUploader.destroy();
1519        InitUploader(mediaType);
1520    }
1521
1522    function SetupEditorWithImage(src, alt){
1523        if(src != undefined && src != ''){
1524            mediaUploadForm.css('backgroundImage', 'url("' + src + '?' + new Date().getTime() + '")');
1525            mediaUploadForm.addClass('dark');
1526            buttonMediaEditorSet.prop('disabled', false);
1527            altTextInput.val(alt);
1528            altTextContainer.show();
1529        }
1530    }
1531
1532    function SetupEditorWithVideo(src, fileType){
1533        if(src != undefined && src != ''){
1534            var videoPreview = $('#media-editor-video-preview');
1535            videoPreview.attr("src", src + '?' + new Date().getTime());
1536            videoPreview.attr("type", fileType);
1537            var video =  videoPreview.parent()[0];
1538            video.load();
1539            video.play();
1540            mediaUploadForm.addClass('dark');
1541            buttonMediaEditorSet.prop('disabled', false);
1542            videoPreviewWrapper.show();
1543        }
1544    }
1545
1546    function SetupEditorWithAudio(src, fileType){
1547        if(src != undefined && src != ''){
1548            var audioPreview = $('#media-editor-audio-preview');
1549            audioPreview.attr("src", src + '?' + new Date().getTime());
1550            audioPreview.attr("type", fileType);
1551            var audio =  audioPreview.parent()[0];
1552            audio.load();
1553            mediaUploadForm.addClass('dark');
1554            buttonMediaEditorSet.prop('disabled', false);
1555            audioPreviewWrapper.show();
1556        }
1557    }
1558
1559    function SetupEditorWithDocument(src, fileType){
1560        if(src != undefined && src != ''){
1561            var docPreview = $('#media-editor-document-preview');
1562            mediaUploadForm.addClass('dark');
1563            buttonMediaEditorSet.prop('disabled', false);
1564            documentPreviewWrapper.show();
1565        }
1566    }
1567
1568    function BufferMediaInformation(mediaKey, mediaID, src){
1569        storageMediaKey.val(mediaKey);
1570        storageMediaID.val(mediaID);
1571        storageMediaFilePath.val(src);
1572    }
1573
1574    function InitUploader(mediaType){
1575        // following texts are generated in media-editor.php (textMediaEditorUploading, textMediaEditorChooseFile, textMediaEditorError, textMediaEditorSuccess)
1576        var textUploading = textMediaEditorUploading;
1577        var textChooseFile = textMediaEditorChooseFile;
1578        var textError = textMediaEditorError;
1579        var textSuccess = textMediaEditorSuccess;
1580
1581        var allowedExtensions = null;
1582        var acceptedExtensions = null;
1583        var maxFileSize = null;
1584
1585        switch(mediaType){
1586            case MEDIA_TYPE_IMAGE:
1587                allowedExtensions = validImageUploadFileExtensions;
1588                acceptedExtensions = acceptImageUploadFileExtensions;
1589                maxFileSize = maxUploadFileSizeImage;
1590                break;
1591            case MEDIA_TYPE_VIDEO:
1592                allowedExtensions = validVideoUploadFileExtensions;
1593                acceptedExtensions = acceptVideoUploadFileExtensions;
1594                maxFileSize = maxUploadFileSizeVideo;
1595                break;
1596            case MEDIA_TYPE_AUDIO:
1597                allowedExtensions = validAudioUploadFileExtensions;
1598                acceptedExtensions = acceptAudioUploadFileExtensions;
1599                maxFileSize = maxUploadFileSizeAudio;
1600                break;
1601            case MEDIA_TYPE_DOCUMENT:
1602                allowedExtensions = validDocumentUploadFileExtensions;
1603                acceptedExtensions = acceptDocumentUploadFileExtensions;
1604                maxFileSize = maxUploadFileSizeDocument;
1605                break;
1606        }
1607    
1608        var buttonUpload = document.getElementById('ddm-media-editor-button-upload'),
1609        progressBar = document.getElementById('ddm-media-editor-progress-bar'),
1610        progressOuter = document.getElementById('ddm-media-editor-progress-wrapper');
1611
1612
1613        mediaUploader = new ss.SimpleUpload({
1614            button: buttonUpload,
1615            dropzone: 'media-upload-dropzone',
1616            allowedExtensions: allowedExtensions,
1617            accept: acceptedExtensions,
1618            url: '/ddm-framework/modules/upload/upload.php',
1619            progressUrl: '/ddm-framework/lib/upload/uploadProgress.php',
1620            name: POST_NAME_MEDIA_UPLOAD_FILE,
1621            multipart: true,
1622            responseType: 'json',
1623            debug: false,
1624            autoSubmit: false,
1625            maxSize: maxFileSize,
1626            startXHR: function(){
1627                progressOuter.style.display = 'flex'; // make progress bar visible
1628            },
1629            onChange: function( filename, extension, uploadBtn, fileSize, file ){
1630                CheckFileExistence(filename, mediaType, function(result, message){
1631                    if(result === true)
1632                        mediaUploader.submit();
1633                    else OnUploadFileExists(message);
1634                });
1635            },
1636            onSubmit: function(filename, extension, uploadBtn, size){
1637                this.setProgressBar(progressBar);
1638                var data = {};
1639                data[POST_NAME_MEDIA_TYPE_NAME] = mediaType;
1640                data[POST_NAME_MEDIA_UPLOAD_REPLACE_FILE] = true;
1641                mediaUploader.setData(data);
1642                uploadMessageBoxSuccess.hide();
1643                uploadMessageBoxError.empty();
1644                buttonUpload.innerHTML = textUploading;
1645            },
1646            onComplete: function(filename, response, uploadBtn, size) {
1647                replaceFileIfExists = false;
1648                progressOuter.style.display = 'none'; // hide progress bar when upload is completed
1649                if (!response) {
1650                    uploadMessageBoxError.html(textError);
1651                    return;
1652                }
1653
1654                let resultCode = response.resultCode;
1655
1656                switch(resultCode){
1657                    case UPLOAD_RESULT_CODE_OK:
1658                        uploadMessageBoxSuccess.show();
1659                        buttonUpload.innerHTML = textChooseFile;
1660                        OnUploadSuccess(response.mediaID, response.mediaFilePath, response.fileType);
1661                        break;
1662                    case UPLOAD_RESULT_CODE_ERROR:
1663                        buttonUpload.innerHTML = textChooseFile;
1664                        let errorMessage = response.message;
1665                        if (errorMessage){
1666                            errorMessage = "Error: " + EscapeTags(errorMessage);
1667                        } else {
1668                            errorMessage = textError;
1669                        }
1670                        OnUploadError(errorMessage);
1671                        break;
1672                    case UPLOAD_RESULT_CODE_FILE_EXISTS:
1673                        OnUploadFileExists(response.message);
1674                        break;
1675                }
1676            },
1677            onError: function(filename, type, status, statusText, response, uploadBtn, size){
1678                progressOuter.style.display = 'none';
1679                OnUploadError(textError);  
1680            },
1681            onExtError: function( filename, extension ) {
1682                console.log("exterror");
1683                OnUploadError("Error: Filetype '." + extension + "' not allowed. Allowed file types: " + acceptedExtensions);
1684            },
1685            onSizeError: function( filename, fileSize, uploadBtn ) {
1686                console.log("onSizeError");
1687                OnUploadError("Error: File size exceeded. (" + (fileSize/1000).toFixed(1) + "MB instead of max. " + (maxFileSize/1000).toFixed(1) + "MB)");
1688            },
1689        });
1690    }
1691
1692    function OnUploadSuccess(mediaID, mediaFilePath, fileType){
1693        buttonMediaEditorSet.show();
1694        buttonMediaEditorSet.prop('disabled', false);
1695        buttonMediaEditorReplaceFile.hide();
1696        mediaUploadForm.css('backgroundImage', 'none');
1697        var mediaType = GetEditingMediaType();
1698        switch(mediaType){
1699            case MEDIA_TYPE_IMAGE:
1700                // show alt text input if it's about an image
1701                altTextContainer.show();
1702                altTextInput.focus();
1703                SetMediaUploadFormBackground(mediaFilePath);
1704            break;
1705            case MEDIA_TYPE_VIDEO:
1706                SetupEditorWithVideo(mediaFilePath, fileType);
1707            break;
1708            case MEDIA_TYPE_AUDIO:
1709                SetupEditorWithAudio(mediaFilePath, fileType);
1710            break;
1711            case MEDIA_TYPE_DOCUMENT:
1712                SetupEditorWithDocument(mediaFilePath, fileType);
1713            break;
1714        }
1715        storageMediaFilePath.val(mediaFilePath);
1716        storageMediaID.val(mediaID);
1717        storageMediaMimeType.val(fileType);
1718    }
1719
1720    function OnUploadError(errorMessage){
1721        buttonMediaEditorSet.show();
1722        buttonMediaEditorSet.prop('disabled', true);
1723        buttonMediaEditorReplaceFile.hide();
1724        uploadMessageBoxError.html(errorMessage);
1725        altTextContainer.hide();
1726        videoPreviewWrapper.hide();
1727        audioPreviewWrapper.hide();
1728        SetMediaUploadFormBackground('/ddm-framework/img/upload_image_error.png')
1729    }
1730
1731    function OnUploadFileExists(message){
1732        buttonMediaEditorSet.hide();
1733        buttonMediaEditorReplaceFile.show();
1734        altTextContainer.hide();
1735        uploadMessageBoxError.html(message);
1736        SetMediaUploadFormBackground(null);
1737    }
1738
1739    function GetEditingMediaType(){
1740        var editingElement = $('*[data-media-reference="' + storageMediaKey.val() + '"');
1741        return editingElement.attr("data-media-type");
1742    }
1743
1744    function SetMediaUploadFormBackground(mediaFilePath){
1745        if(mediaFilePath)
1746            mediaUploadForm.css('backgroundImage', 'url("' + EscapeTags(mediaFilePath) + '?' + new Date().getTime() + '")');
1747        else mediaUploadForm.css('backgroundImage', null);
1748        mediaUploadForm.css('backgroundSize', 'cover');
1749        mediaUploadForm.addClass('dark');
1750    }
1751
1752    /** Button Click Listeners */
1753
1754    buttonMediaEditorCancel.unbind("click").bind("click", function(event){
1755        event.preventDefault();
1756        SetMediaEditorVisibility(false);
1757    });
1758    
1759    buttonMediaEditorReplaceFile.unbind("click").bind("click", function(){
1760        mediaUploader.submit();
1761    });
1762
1763    buttonMediaEditorSet.unbind("click").bind("click", function(event){
1764        event.preventDefault();
1765
1766        var mediaKey = storageMediaKey.val();
1767        var mediaID = storageMediaID.val();
1768        var altText = altTextInput.val();
1769
1770        var postData = {};
1771        postData[POST_NAME_MEDIA_ID] = mediaID;
1772        postData[POST_NAME_PAGE_MEDIA_KEY] = mediaKey;
1773        postData[POST_NAME_PAGE_MEDIA_ALT_TEXT] = altText;
1774
1775        mediaUploadForm.find("video")[0].pause();
1776        mediaUploadForm.find("audio")[0].pause();
1777
1778        jQuery.ajax({
1779            url: "/ddm-framework/util/ajax/update-page-media.php",
1780            type: 'POST',
1781            responseType: 'json',
1782            data: postData,
1783            success:function(data){
1784                let mediaElement = $('*[data-key="' + mediaKey + '"');
1785                let editorButton = $('*[data-media-reference="' + mediaKey + '"');
1786                let mediaFilePath = data.mediaFilePath;
1787                let mediaType = editorButton.attr("data-media-type");
1788
1789                mediaElement.attr("src", mediaFilePath + '?' + new Date().getTime());
1790                editorButton.attr("data-media-src", mediaFilePath);
1791                editorButton.attr("data-media-id", mediaID);
1792                switch(mediaType){
1793                    case "image":
1794                        // image -> set alternate text after changing source
1795                        mediaElement.attr("alt", altText);
1796                        editorButton.attr("data-media-alt", altText);
1797                        break;
1798                    case "video":
1799                    case "audio":
1800                        // video/audio -> reload video or audio after changing source
1801                        mediaElement.parent()[0].load();
1802                        mediaElement.attr("type", storageMediaMimeType.val());
1803                        break;
1804                    case "document":
1805                        let fileName = mediaFilePath.split('\\').pop().split('/').pop();
1806                        mediaElement.attr("href", mediaFilePath + '?' + new Date().getTime());
1807                        mediaElement.attr("download", fileName);
1808                        break;
1809                }
1810                var changedData = {};
1811                changedData[POST_NAME_PAGE_MEDIA_KEY] = mediaKey;
1812                changedData[POST_NAME_MEDIA_ID] = mediaID;
1813                changedData[POST_NAME_MEDIA_FILE_PATH] = mediaFilePath;
1814                changedData[POST_NAME_PAGE_MEDIA_ALT_TEXT] = altText;
1815                $(document).trigger(EVENT_LISTENER_MEDIA_EDITOR_CONTENT_CHANGED, changedData);
1816                SetMediaEditorVisibility(false);
1817            },
1818            error:function (xhr, status, error){
1819                Swal.fire({
1820                    type: "error",
1821                    html: xhr.responseText
1822                });
1823            }
1824        });
1825    });
1826
1827    function CheckFileExistence(fileName, mediaType, callback){
1828        var formData = new FormData();
1829        formData.append(POST_NAME_MEDIA_FILE_NAME, fileName);
1830        formData.append(POST_NAME_MEDIA_TYPE_NAME, mediaType);
1831        formData.append(POST_NAME_MEDIA_UPLOAD_REPLACE_FILE, replaceFileIfExists);
1832
1833        $.ajax({
1834            url: '/ddm-framework/modules/upload/file-validation.php',
1835            data: formData,
1836            type: 'POST',
1837            dataType: 'json',
1838            contentType: false,
1839            processData: false,
1840            success: function(response){
1841                if(typeof callback === "function")
1842                    callback(response.resultCode === UPLOAD_RESULT_CODE_OK, response.message);               
1843            },
1844            error: function(xhr, status, error){
1845                console.error("Error while checking file existence... " + error);
1846                if(typeof callback === "function")
1847                    callback(false, error);               
1848            }
1849        });
1850    }
1851}
1852/** Url Editor **/
1853
1854const EditableUrlBoxHeightThreshold = 100; // height (in px) - if height of url box is smaller than this value, the "center-vertical" class will be added to the contextMenuOpener
1855
1856$(function(){
1857    var urlBoxes = $('.ddm-editable-url-box');
1858    urlBoxes.each(function(index, urlBox){
1859        let $urlBox = $(urlBox);
1860        let height = $urlBox.height();
1861        let contextMenuOpener = $urlBox.children('.ddm-context-menu-opener');
1862        if(height < 100 && contextMenuOpener.length > 0){
1863            contextMenuOpener.addClass("center-vertical");
1864        }
1865    });
1866
1867    urlBoxes.on("mouseover", function(event){
1868        urlBoxes.find(".ddm-admin-textinput").css('cursor', event.ctrlKey ? 'pointer' : '');
1869    });
1870
1871    $(document).on('keydown keyup', function(event) {
1872        urlBoxes.find(".ddm-admin-textinput").css('cursor', event.ctrlKey ? 'pointer' : '');
1873    });
1874
1875    urlBoxes.find(".ddm-admin-textinput").click(function(event){
1876        if(event.ctrlKey){
1877            event.preventDefault();
1878            OpenUrl(event, $(this).parents(".ddm-editable-url-box").attr("data-key"));
1879        } 
1880    });
1881});
1882
1883/**
1884 * Shows an overlay letting the user change the text of the url and the url itself
1885 * @param {Event} event 
1886 * @param {String} key 
1887 */
1888function EditUrl(event, key){
1889    event.preventDefault();
1890
1891    CloseContextMenu(); // close the context menu
1892
1893    let urlEditor = $('#ddm-url-editor');
1894    urlEditor.show();
1895
1896    let urlElement = $('a[data-key=' + key + ']');
1897
1898    var inputUrl = $('#ddm-url-edit-input-url');
1899    var inputTarget = $('#ddm-url-edit-input-target');
1900    var errorLabel = $('#ddm-url-editor-error-label');
1901    var inputText = urlEditor.find('#ddm-url-editor-text-edit');
1902    inputText.hide(); // hide text input
1903    errorLabel.hide();
1904
1905    var href = urlElement.attr('href');
1906    var urlTarget = urlElement.attr('target');
1907    inputUrl.val(href);
1908    inputTarget.prop('checked', urlTarget == '_blank');
1909
1910    $('#ddm-url-edit-button-cancel').unbind('click').bind('click', function(){
1911        urlEditor.hide();
1912    });
1913
1914    $('#ddm-url-edit-button-save').unbind('click').bind('click', function(){
1915        inputText.hide(); // hide text input
1916        var urlKey = key + "-url"; // url database key is same as text id but with "-url" suffix - see DDM-Toolbox.DDMUrl (function)
1917        var urlTarget = key + "-target"; // target database key is same as text id but with "-target" suffix - see DDM-Toolbox.DDMUrl (function)
1918        var target = inputTarget.prop('checked') ? '_blank' : '_self';
1919
1920        var valuesToBeSaved = [];
1921        valuesToBeSaved.push({key: urlKey, value: inputUrl.val()});
1922        valuesToBeSaved.push({key: urlTarget, value: target});
1923
1924        valuesToBeSaved.forEach(function(item, index){
1925            SaveTextToDatabase(item.key, item.value, function(){
1926                if(index == valuesToBeSaved.length - 1){
1927                    urlElement.attr("href", inputUrl.val());
1928                    urlElement.attr("target", target);
1929                    urlEditor.hide();
1930                }
1931            });
1932        });
1933    });
1934}
1935
1936/**
1937 * Opens URL based on tag information (URL, target)
1938 * @param {Object} event event
1939 * @param {String} key database storage key of the editable url
1940 */
1941function OpenUrl(event, key){
1942    event.preventDefault();
1943    let urlElement = $('a[data-key=' + key + ']')[0];
1944    window.open(urlElement.href, urlElement.target, '' );
1945}
1946/** Checkbox Editor **/
1947
1948$(document).on('change', '.ddm-checkbox[data-editable="true"]', function(){
1949    let ddmCheckbox = $(this);
1950    let parameterKey = ddmCheckbox.attr('data-parameter-key');
1951    let checkbox = ddmCheckbox.children('input[type="checkbox"]');
1952    let checkboxValue = 0;
1953    if(checkbox.length > 0)
1954        checkboxValue = checkbox.prop('checked') ? 1 : 0;
1955    UpdateParameterInDatabase(parameterKey, checkboxValue, false, function(){
1956        ddmCheckbox.attr("data-checked", checkbox.prop('checked'));
1957    });
1958});
1959/** Hideable **/
1960
1961$(function(){
1962    $('.ddm-hideable > .ddm-hideable-controls .ddm-admin-checkbox input[type="checkbox"]').on("change", function(){
1963
1964        let checked = this.checked;
1965        let hideableContainer = $(this).parents(".ddm-hideable");
1966        let hideableContent = hideableContainer.children(".ddm-hideable-content");
1967
1968        if(checked)
1969            hideableContent.slideUp("slow");
1970        else hideableContent.slideDown("slow");
1971    });
1972});
1973$(function(){
1974    const vw = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
1975    const vh = Math.max(document.documentElement.clientHeight, window.innerHeight || 0
1975);
1976
1977    $(document).on('click', ".ddm-context-menu-opener", function(e){
1978        e.preventDefault();
1979        e.stopPropagation();
1980        let $this = $(this);
1981        let fixed = IsElementOrParentFixed(this);
1982        let contextMenu = $this.parent().children('.ddm-context-menu');
1983        let clone = contextMenu.clone(true);
1984        let positionX = $this.offset().left - contextMenu.width();
1985        let positionY = $this.offset().top;
1986
1987        let screenPadding = 10;
1988        let documentWidth = fixed ? vw : $(document).width();
1989        let documentHeight = fixed ? vh : $(document).height();
1990
1991        if(fixed)
1992            positionY = positionY - $(document).scrollTop();    
1993
1994        // prevent going off screen left
1995        if(positionX <= screenPadding)
1996            positionX = screenPadding;
1997
1998        // prevent going off screen right
1999        if(positionX + contextMenu.width + screenPadding > documentWidth)
2000            positionX = documentWidth - screenPadding - contextMenu.width;
2001
2002        // prevent going off screen top
2003        if(positionY < screenPadding)
2004            positionY = screenPadding;
2005
2006        // prevent going off screen bottom
2007        if(positionY + contextMenu.height() + screenPadding > documentHeight)
2008            positionY = documentHeight - contextMenu.height() - screenPadding;
2009
2010
2011        clone.css("position", fixed ? "fixed" : "absolute");
2012        clone.css("left", positionX + "px");
2013        clone.css("right", "auto");
2014        clone.css("top", positionY + "px");
2015        if(fixed)
2016            clone.css("z-index", 700);
2017        $('body').append(clone);
2018
2019        setTimeout(function(){
2020            clone.toggleClass("active");
2021        }, 20);
2022    });
2023
2024    $('.ddm-context-menu').unbind('click').bind('click', function(e){
2025        e.stopPropagation();
2026    });
2027
2028    $('html').unbind('click').bind('click', function(event) {
2029        if($(event.target).hasClass('ddm-admin-action-button')) return; // do not close context menu if user clicked on admin action button, such as text editor buttons (save, cancel)
2030        CloseContextMenu();
2031    });
2032});
2033
2034function CloseContextMenu(){
2035    $('.ddm-context-menu').removeClass("active");
2036    $('body > .ddm-context-menu').remove();
2037}

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.