PageSourceSearch

https://cascadehills.com/wp-content/plugins/minervakb/assets/js/minerva-kb-common-ui.js?ver=2.0.9

js cascadehills.com collected 2026-09-26 10:40:15 UTC 19,048 bytes, 608 lines download raw bytes

1/**
2 * Project: Minerva KB
3 * Copyright: 2015-2016 @KonstruktStudio
4 */
5(function($) {
6    'use strict';
7
8    var GLOBAL_DATA = window.MinervaKB || {}; // TODO: broken in admin
9    var settings = GLOBAL_DATA.settings || {};
10    // var ajaxUrl = GLOBAL_DATA.ajaxUrl;
11    // var i18n = GLOBAL_DATA.i18n;
12
13    // polyfills
14    // https://tc39.github.io/ecma262/#sec-array.prototype.includes
15    if (!Array.prototype.includes) {
16        Object.defineProperty(Array.prototype, 'includes', {
17            value: function(searchElement, fromIndex) {
18
19                if (this == null) {
20                    throw new TypeError('"this" is null or not defined');
21                }
22
23                // 1. Let O be ? ToObject(this value).
24                var o = Object(this);
25
26                // 2. Let len be ? ToLength(? Get(O, "length")).
27                var len = o.length >>> 0;
28
29                // 3. If len is 0, return false.
30                if (len === 0) {
31                    return false;
32                }
33
34                // 4. Let n be ? ToInteger(fromIndex).
35                //    (If fromIndex is undefined, this step produces the value 0.)
36                var n = fromIndex | 0;
37
38                // 5. If n ≥ 0, then
39                //  a. Let k be n.
40                // 6. Else n < 0,
41                //  a. Let k be len + n.
42                //  b. If k < 0, let k be 0.
43                var k = Math.max(n >= 0 ? n : len - Math.abs(n), 0);
44
45                function sameValueZero(x, y) {
46                    return x === y || (typeof x === 'number' && typeof y === 'number' && isNaN(x) && isNaN(y));
47                }
48
49                // 7. Repeat, while k < len
50                while (k < len) {
51                    // a. Let elementK be the result of ? Get(O, ! ToString(k)).
52                    // b. If SameValueZero(searchElement, elementK) is true, return true.
53                    if (sameValueZero(o[k], searchElement)) {
54                        return true;
55                    }
56                    // c. Increase k by 1.
57                    k++;
58                }
59
60                // 8. Return false
61                return false;
62            }
63        });
64    }
65
66    /**
67     * General utils
68     */
69
70    function makeArray(thing) {
71        if (!thing) {
72            return [];
73        }
74
75        return [].slice.apply(thing);
76    }
77
78    function getElementIndex(node) {
79        var index = 0;
80
81        while ((node = node.previousElementSibling)) {
82            index++;
83        }
84
85        return index;
86    }
87
88    function humanFileSize(bytes, si) {
89        var thresh = si ? 1000 : 1024;
90
91        if (Math.abs(bytes) < thresh) {
92            return bytes + 'B';
93        }
94
95        var units = si
96            ? ['kB','MB','GB','TB','PB','EB','ZB','YB']
97            : ['KiB','MiB','GiB','TiB','PiB','EiB','ZiB','YiB'];
98        var u = -1;
99
100        do {
101            bytes /= thresh;
102            ++u;
103        } while(Math.abs(bytes) >= thresh && u < units.length - 1);
104
105        return bytes.toFixed(1) + '' + units[u];
106    }
107
108    /**
109     * Quill editor with optional file uploader
110     * @param options
111     */
112    function setupQuillEditor(options) {
113        var $form = options.$form;
114        var isStandaloneUploadButton = settings['tickets_create_use_standalone_upload_button'];
115
116        var toolbarOptions = options.toolbarOptions || [
117            [{'header': [1, 2, 3, 4, 5, 6, false]}],
118            ['bold', 'italic', 'underline', 'strike'],
119            [{'list': 'ordered'}, {'list': 'bullet'}],
120            ['link'],
121            ['blockquote', 'code-block'],
122            ['clean']
123        ];
124
125        if (options.fileUpload && !isStandaloneUploadButton) {
126            toolbarOptions.push(['upload']);
127        }
128
129        var quillEditor = new Quill(options.editorSelector, {
130            modules: {
131                toolbar: toolbarOptions,
132                history: {
133                    delay: 2000,
134                    maxStack: 500,
135                    userOnly: true
136                }
137            },
138            placeholder: options.placeholder || 'Write your message here...', // TODO: TK
139            theme: options.theme || 'snow' // TODO: fix
140        });
141
142        // remove formatting on paste, leave only strings
143        quillEditor.clipboard.addMatcher(Node.ELEMENT_NODE, function (node, delta) {
144            var ops = [];
145
146            delta.ops.forEach(function(op) {
147                var urlRegex = /https?:\/\/[^\s]+/g;
148
149                if (op.insert && typeof op.insert === 'string') {
150                    if (op.attributes && op.attributes.link) {
151                        // preserve links. TODO: maybe add safe attributes
152                        ops.push({ insert: op.insert.trim(), attributes: { link: op.attributes.link } });
153                    } else {
154                        ops.push({ insert: op.insert });
155                    }
156                }
157            });
158
159            delta.ops = ops;
160
161            return delta;
162        });
163
164        if (!options.fileUpload) {
165            return quillEditor;
166        }
167
168        var isAttachmentsOpen = false;
169        var uploadButton = isStandaloneUploadButton ? $form.find('.js-mkb-ticket-attach-files')[0] : $form.find('.ql-upload')[0];
170        var $attachmentsSection = $form.find('.js-mkb-editor-attachments-section');
171
172        /**
173         * Attachments
174         */
175        uploadButton.addEventListener('click', function(e) {
176            e.preventDefault();
177
178            if (!isStandaloneUploadButton) {
179                uploadButton.classList.toggle('state--on');
180            }
181
182            $attachmentsSection[isAttachmentsOpen ? 'slideUp' : 'slideDown'](250);
183            isAttachmentsOpen = !isAttachmentsOpen;
184        });
185
186        setupFileUpload({
187            $dropArea: $form.find('.js-mkb-editor-attachments-drop-area'),
188            $form: $form
189        });
190
191        return quillEditor;
192    }
193
194    /**
195     * File uploader
196     */
197    function setupFileUpload(options) {
198        var $dropArea = options.$dropArea;
199        var $form = options.$form;
200
201        if (!$dropArea || !$dropArea.length || !$form || !$form.length) {
202            console.log('Could not initialize file uploader');
203
204            return;
205        }
206
207        var dropArea = $dropArea.get(0);
208
209        var $preview = $dropArea.find('.js-mkb-file-upload-preview');
210        var $clearFiles = $dropArea.find('.js-mkb-file-upload-clear');
211        var $uploadBtn = $dropArea.find(".js-mkb-file-upload-store");
212        var $fileStore = $uploadBtn; // alias
213        var $dropErrors = $dropArea.find('.js-mkb-file-upload-drop-errors');
214
215        // control options
216        var allowedTypes = $fileStore.attr('accept') || '';
217        var maxFiles = $fileStore.data('maxFiles');
218        var maxFileSizeInMb = $fileStore.data('maxFileSize');
219
220        $form.attr('enctype', 'multipart/form-data');
221
222        allowedTypes = allowedTypes.split(',').map(function(type) {
223            return type.trim().replace('.', '');
224        });
225
226        ['dragenter', 'dragover', 'dragleave', 'drop'].forEach(function(eventName) {
227            /**
228             * NOTE: Can affect other plugins on page, watch this closely
229             * Needed to avoid opening of dropped file in browser.
230             * Must be on body or wider container, otherwise browser will show drop icon outside drop area
231             */
232            document.body.addEventListener(eventName, function preventDefaults(e) {
233                e.preventDefault();
234                e.stopPropagation();
235            }, false);
236        });
237
238        // Highlight drop area when item is dragged over it
239        ['dragenter', 'dragover'].forEach(function(eventName) {
240            dropArea.addEventListener(eventName, function highlight() {
241                $dropArea.addClass('mkb-drop-highlight');
242            }, false);
243        });
244
245        ['dragleave', 'drop'].forEach(function(eventName) {
246            dropArea.addEventListener(eventName, function unhighlight() {
247                $dropArea.removeClass('mkb-drop-highlight');
248            }, false);
249        });
250
251        // Handle dropped files
252        dropArea.addEventListener('drop', function(e) {
253            handleAddedFiles(e.dataTransfer.files, true);
254        } , false);
255
256        // upload btn click
257        $uploadBtn.get(0).addEventListener('change', function() {
258            handleAddedFiles(this.files);
259        }, false);
260
261        function createDataTransferObject() {
262            return new ClipboardEvent('').clipboardData || new DataTransfer();
263        }
264
265        // errors state
266        var hasNotAllowedFilesError = false;
267        var hasTooLargeFilesError = false;
268        var hasTooManyFiles = false;
269        var fileAlreadyAddedError = false;
270
271        function handleAddedFiles(addedFiles, isDragAndDrop) {
272            isDragAndDrop = isDragAndDrop || false;
273
274            // TODO: merge all files, check across browsers
275            var oldFilesArr = isDragAndDrop ? makeArray($fileStore.prop('files')) : [];
276            var newFilesArray = makeArray(addedFiles);
277            var oldFileNames = oldFilesArr.map(function(item) { return item.name; });
278
279            // reset errors state
280            hasNotAllowedFilesError = false;
281            hasTooLargeFilesError = false;
282            hasTooManyFiles = false;
283            fileAlreadyAddedError = false;
284
285            if (oldFilesArr.length >= maxFiles) {
286                hasTooManyFiles = true;
287
288                handleAddFileErrors();
289
290                return;
291            }
292
293            console.log('adding files', newFilesArray);
294
295            newFilesArray = newFilesArray.filter(function(item) {
296                var extension = item.name.split('.').pop().toLowerCase();
297                var isAllowed = allowedTypes.includes(extension);
298                var isValidSize = item.size / (1024 * 1024) <= maxFileSizeInMb;
299                var isAlreadyAdded = oldFileNames.includes(item.name);
300
301                if (!isAllowed) {
302                    hasNotAllowedFilesError = true;
303                }
304
305                if (!isValidSize) {
306                    hasTooLargeFilesError = true;
307                }
308
309                if (isAlreadyAdded) {
310                    fileAlreadyAddedError = true;
311                }
312
313                return isAllowed && isValidSize && !isAlreadyAdded;
314            });
315
316            var allFilesArr = oldFilesArr.concat(newFilesArray);
317
318            if (allFilesArr.length > maxFiles) {
319                hasTooManyFiles = true;
320
321                allFilesArr = allFilesArr.slice(0, maxFiles);
322            }
323
324            var allFiles = createDataTransferObject();
325
326            allFilesArr.forEach(function(file) {
327                allFiles.items.add(file);
328            });
329
330            if (allFiles.files.length) {
331                $fileStore.prop('files', allFiles.files);
332            } else {
333                clearFiles();
334            }
335
336            handleAddFileErrors();
337            updatePreview(allFiles.files);
338
339            console.log('[DEBUG]: Files in storage', $fileStore.prop('files'));
340        }
341
342        /**
343         * Process and display file add errors
344         */
345        function handleAddFileErrors() {
346            clearErrors();
347
348            var addFileErrors = [];
349
350            if (hasNotAllowedFilesError) {
351                // TODO: translations
352                addFileErrors.push('Some of the added files are not allowed');
353            }
354
355            if (hasTooLargeFilesError) {
356                // TODO: translations
357                addFileErrors.push('Some of the added files are too large');
358            }
359
360            if (hasTooManyFiles) {
361                // TODO: translations
362                addFileErrors.push('Maximum file limit is reached, some files were not added');
363            }
364
365            if (fileAlreadyAddedError) {
366                // TODO: translations
367                addFileErrors.push('Some of the files are already added');
368            }
369
370            if (addFileErrors.length) {
371                $dropErrors.html(
372                    addFileErrors.reduce(function(html, error) {
373                        return html + '<div>' + error + '</div>'
374                    }, '')
375                );
376
377                // TODO: clear errors somehow, or highlight when they change but look the same
378            }
379        }
380
381        function clearErrors() {
382            $dropErrors.html('');
383        }
384
385        /**
386         * Preview update
387         * @param files
388         */
389        function updatePreview(files) {
390            $preview.html('');
391
392            // not an array usually
393            files = makeArray(files);
394
395            files.forEach(function addFilePreview(file) {
396                var isImage = /^image\//.test(file.type);
397                var $item = $(
398                    '<div class="js-mkb-attachment-upload-preview-item mkb-attachment-upload-preview-item">' +
399                        '<a href="#" class="js-mkb-attachment-preview-remove mkb-attachment-preview-remove fa fa-times-circle"></a>' +
400                    '</div>'
401                );
402
403                if (isImage) {
404                    var reader = new FileReader();
405                    reader.readAsDataURL(file);
406
407                    reader.onloadend = function() {
408                        var img = document.createElement('img');
409                        img.src = reader.result;
410                        $item.append(img);
411                    };
412
413                    $item.addClass('type--image');
414                } else {
415                    // non-image files
416                    $item.append('<span>' + file.name + ' (' + humanFileSize(file.size, true) + ')' + '</span>');
417                    $item.addClass('type--file');
418                }
419
420                $preview.append($item);
421            });
422        }
423
424        /**
425         * Remove file by icon click
426         */
427        $preview.on('click', '.js-mkb-attachment-preview-remove', function(e) {
428            e.preventDefault();
429
430            var indexToRemove = getElementIndex(e.currentTarget.parentNode);
431            var currentFilesArray = makeArray($fileStore.prop('files'));
432            var updatedFiles = createDataTransferObject();
433
434            currentFilesArray.filter(function(file, index) {
435                return index !== indexToRemove;
436            }
436).forEach(function(file, index) {
437                updatedFiles.items.add(file);
438            });
439
440            if (updatedFiles.files.length) {
441                $fileStore.prop('files', updatedFiles.files);
442                updatePreview(updatedFiles.files);
443
444                console.log('[DEBUG]: Files in storage', $fileStore.prop('files'));
445            } else {
446                clearFiles();
447            }
448
449            clearErrors();
450        });
451
452        function clearFiles() {
453            $fileStore.val(null); // if causes issues across browser, replace with clone
454            updatePreview([]);
455
456            console.log('[DEBUG]: Files in storage', $fileStore.prop('files'));
457        }
458
459        $clearFiles.on('click', function(e) {
460            e.preventDefault();
461            e.stopImmediatePropagation();
462
463            clearFiles();
464            clearErrors();
465        });
466    }
467
468    /**
469     *
470     * @param el
471     * @param options
472     * @constructor
473     */
474    function Form(el, options) {
475        options = options || {};
476
477        // state
478        this._isLoading = false;
479
480        // options
481        this.options = options;
482
483        // controls data
484        this.controls = {};
485        this.quillEditors = this.options.quillEditors || [];
486
487        // DOM
488        this.el = el;
489        this.$el = $(el);
490        this.$messagesEl = this.$el.find('.js-mkb-form-messages');
491        this.$submit = this.$el.find('.js-mkb-form-submit');
492
493        // labels
494        this.submitLabel = this.$submit.val();
495        this.submitLoadingLabel = this.$submit.data('progress-label');
496
497        // setup
498        this.findControls();
499    }
500
501    Form.prototype.findControls = function() {
502        var $controls = this.$el.find('input:not(.js-mkb-form-submit), select, textarea');
503
504        this.controls = makeArray($controls).map(function(el) {
505            return {
506                id: el.getAttribute('name'),
507                el: el,
508                $el: $(el)
509            };
510        });
511    };
512
513    Form.prototype.isLoading = function() {
514        return this._isLoading;
515    };
516
517    Form.prototype.startLoading = function() {
518        this._isLoading = true;
519
520        this.disableControls();
521        this.disableControl(this.$submit);
522        this.$submit.val(this.submitLoadingLabel);
523        this.$el.addClass('state--loading');
524
525        this.$el.append('<div class="js-mkb-form-overlay mkb-form-overlay"></div>')
526    };
527
528    Form.prototype.endLoading = function() {
529        this._isLoading = false;
530
531        this.enableControls();
532        this.enableControl(this.$submit);
533        this.$submit.val(this.submitLabel);
534
535        this.$el.find('.js-mkb-form-overlay').remove();
536        this.$el.removeClass('state--loading');
537    };
538
539    Form.prototype.disableControls = function() {
540        this.controls.forEach(function(control) {
541            this.disableControl(control.$el);
542        }.bind(this));
543
544        this.quillEditors.forEach(function(quillEditor) {
545            quillEditor.enable(false);
546        });
547    };
548
549    Form.prototype.lock = function() {
550        // TODO: maybe use separate state var
551        this._isLoading = true;
552
553        this.disableControls();
554        this.disableControl(this.$submit);
555    };
556
557    Form.prototype.enableControls = function() {
558        this.controls.forEach(function(control) {
559            this.enableControl(control.$el);
560        }.bind(this));
561
562        this.quillEditors.forEach(function(quillEditor) {
563            quillEditor.enable(true);
564        });
565    };
566
567    Form.prototype.disableControl = function($control) {
568        $control.attr('disabled', 'disabled');
569    };
570
571    Form.prototype.enableControl = function($control) {
572        $control.attr('disabled', false);
573    };
574
575    Form.prototype.showMessages = function(messages, messagesType) {
576        messagesType = messagesType || 'error';
577
578        this.clearMessages();
579
580        this.$messagesEl
581            .addClass('mkb-form-messages--' + messagesType)
582            .html(messages); // TODO: handle array
583
584        $([document.documentElement, document.body]).animate({
585            scrollTop: this.$messagesEl.offset().top - parseInt(settings['global_scroll_offset']['size'], 10) // TODO: scroll offset setting
586        }, 100);
587    };
588
589    Form.prototype.clearMessages = function() {
590        this.$messagesEl.removeClass('mkb-form-messages--error mkb-form-messages--success').html('')
591    };
592
593    Form.prototype.serialize = function() {
594        return $(this.el).serializeArray().reduce(function(store, kv) {
595            store[kv.name] = kv.value;
596            return store;
597        }, {});
598    };
599
600    /**
601     * Exports
602     */
603    window.MinervaCommonUI = {
604        Form: Form,
605        setupQuillEditor: setupQuillEditor,
606        setupFileUpload: setupFileUpload
607    };
608})(window.jQuery);

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.