Passed
Push — master ( 550b4f...70c8b4 )
by Jan
05:05
created

AjaxUI.hideProgressBar   A

Complexity

Conditions 1

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 8
c 0
b 0
f 0
dl 0
loc 12
rs 10
1
/*
2
 * This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
3
 *
4
 * Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
5
 *
6
 * This program is free software: you can redistribute it and/or modify
7
 * it under the terms of the GNU Affero General Public License as published
8
 * by the Free Software Foundation, either version 3 of the License, or
9
 * (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14
 * GNU Affero General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Affero General Public License
17
 * along with this program.  If not, see <https://www.gnu.org/licenses/>.
18
 */
19
20
/**
21
 * Extract the title (The name between the <title> tags) of a HTML snippet.
22
 * @param {string} html The HTML code which should be searched.
23
 * @returns {string} The title extracted from the html.
24
 */
25
function extractTitle(html : string) : string {
26
    let title : string = "";
27
    let regex = /<title>(.*?)<\/title>/gi;
28
    if (regex.test(html)) {
29
        let matches = html.match(regex);
30
        for(let match in matches) {
31
            title = $(matches[match]).text();
32
        }
33
    }
34
    return title;
35
}
36
37
38
class AjaxUI {
39
40
    protected BASE = "/";
41
42
    private trees_filled : boolean = false;
43
44
    private statePopped : boolean = false;
45
46
    public xhr : XMLHttpRequest;
47
48
    public constructor()
49
    {
50
        //Make back in the browser go back in history
51
        window.onpopstate = this.onPopState;
52
        $(document).ajaxError(this.onAjaxError.bind(this));
53
        //$(document).ajaxComplete(this.onAjaxComplete.bind(this));
54
    }
55
56
    /**
57
     * Starts the ajax ui und execute handlers registered in addStartAction().
58
     * Should be called in a document.ready, after handlers are set.
59
     */
60
    public start(disabled : boolean = false)
61
    {
62
        if(disabled) {
63
            return;
64
        }
65
66
        console.info("AjaxUI started!");
67
68
        this.BASE = $("body").data("base-url");
69
        //If path doesn't end with slash, add it.
70
        if(this.BASE[this.BASE.length - 1] !== '/') {
71
            this.BASE = this.BASE + '/';
72
        }
73
        console.info("Base path is " + this.BASE);
74
75
        //Show flash messages
76
        $(".toast").toast('show');
77
78
79
        /**
80
         * Save the XMLHttpRequest that jQuery used, to the class, so we can acess the responseURL property.
81
         * This is a work-around as long jQuery does not implement this property in its jQXHR objects.
82
         */
83
        //@ts-ignore
84
        jQuery.ajaxSettings.xhr = function () {
85
            //@ts-ignore
86
            let xhr = new window.XMLHttpRequest();
87
            //Save the XMLHttpRequest to the class.
88
            ajaxUI.xhr = xhr;
89
            return xhr;
90
        };
91
92
93
        this.registerLinks();
94
        this.registerForm();
95
        this.fillTrees();
96
97
        this.initDataTables();
98
99
        //Trigger start event
100
        $(document).trigger("ajaxUI:start");
101
    }
102
103
    /**
104
     * Fill the trees with the given data.
105
     */
106
    public fillTrees()
107
    {
108
        let categories =  localStorage.getItem("tree_datasource_tree-categories");
109
        let devices =  localStorage.getItem("tree_datasource_tree-devices");
110
        let tools =  localStorage.getItem("tree_datasource_tree-tools");
111
112
        if(categories == null) {
113
            categories = "categories";
114
        }
115
116
        if(devices == null) {
117
            devices = "devices";
118
        }
119
120
        if(tools == null) {
121
            tools = "tools";
122
        }
123
124
        this.treeLoadDataSource("tree-categories", categories);
125
        this.treeLoadDataSource("tree-devices", devices);
126
        this.treeLoadDataSource("tree-tools", tools);
127
128
        this.trees_filled = true;
129
130
        //Register tree btns to expand all, or to switch datasource.
131
        $(".tree-btns").click(function (event) {
132
            event.preventDefault();
133
            $(this).parents("div.dropdown").removeClass('show');
134
            //$(this).closest(".dropdown-menu").removeClass('show');
135
            $(".dropdown-menu.show").removeClass("show");
136
            let mode = $(this).data("mode");
137
            let target = $(this).data("target");
138
            let text = $(this).text() + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
139
140
            if (mode==="collapse") {
141
                // @ts-ignore
142
                $('#' + target).treeview('collapseAll', { silent: true });
143
            }
144
            else if(mode==="expand") {
145
                // @ts-ignore
146
                $('#' + target).treeview('expandAll', { silent: true });
147
            } else {
148
                localStorage.setItem("tree_datasource_" + target, mode);
149
                ajaxUI.treeLoadDataSource(target, mode);
150
            }
151
152
            return false;
153
        });
154
    }
155
156
    /**
157
     * Load the given url into the tree with the given id.
158
     * @param target_id
159
     * @param datasource
160
     */
161
    protected treeLoadDataSource(target_id, datasource) {
162
        let text : string = $(".tree-btns[data-mode='" + datasource + "']").html();
163
        text = text + " \n<span class='caret'></span>"; //Add caret or it will be removed, when written into title
164
        switch(datasource) {
165
            case "categories":
166
                ajaxUI.initTree("#" + target_id, 'tree/categories');
167
                break;
168
            case "locations":
169
                ajaxUI.initTree("#" + target_id, 'tree/locations');
170
                break;
171
            case "footprints":
172
                ajaxUI.initTree("#" + target_id, 'tree/footprints');
173
                break;
174
            case "manufacturers":
175
                ajaxUI.initTree("#" + target_id, 'tree/manufacturers');
176
                break;
177
            case "suppliers":
178
                ajaxUI.initTree("#" + target_id, 'tree/suppliers');
179
                break;
180
            case "tools":
181
                ajaxUI.initTree("#" + target_id, 'tree/tools');
182
                break;
183
            case "devices":
184
                ajaxUI.initTree("#" + target_id, 'tree/devices');
185
                break;
186
        }
187
188
        $( "#" + target_id + "-title").html(text);
189
    }
190
191
    /**
192
     * Fill a treeview with data from the given url.
193
     * @param tree The Jquery selector for the tree (e.g. "#tree-tools")
194
     * @param url The url from where the data should be loaded
195
     */
196
    public initTree(tree, url) {
197
        //let contextmenu_handler = this.onNodeContextmenu;
198
        $.getJSON(ajaxUI.BASE + url, function (data) {
199
            // @ts-ignore
200
            $(tree).treeview({
201
                data: data,
202
                enableLinks: false,
203
                showIcon: false,
204
                showBorder: true,
205
                searchResultBackColor: '#ffc107',
206
                searchResultColor: '#000',
207
                onNodeSelected: function(event, data) {
208
                    if(data.href) {
209
                        ajaxUI.navigateTo(data.href);
210
                    }
211
                },
212
                //onNodeContextmenu: contextmenu_handler,
213
                expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview"})
214
                .on('initialized', function() {
215
                    $(this).treeview('collapseAll', { silent: true });
216
217
                    //Implement searching if needed.
218
                    if($(this).data('treeSearch')) {
219
                        let _this = this;
220
                        let $search = $($(this).data('treeSearch'));
221
                        $search.on( 'input', function() {
222
                            $(_this).treeview('collapseAll', { silent: true });
223
                            $(_this).treeview('search', [$search.val()]);
224
                        });
225
                    }
226
                });
227
        });
228
    }
229
230
231
    /**
232
     * Register all links, for loading via ajax.
233
     */
234
    public registerLinks()
235
    {
236
        // Unbind all old handlers, so the things are not executed multiple times.
237
        $('a').not(".link-external, [data-no-ajax], .page-link, [href^='javascript'], [href^='#']").unbind('click').click(function (event) {
238
                let a = $(this);
239
                let href = $.trim(a.attr("href"));
240
                //Ignore links without href attr and nav links ('they only have a #)
241
                if(href != null && href != "" && href.charAt(0) !== '#') {
242
                    event.preventDefault();
243
                    ajaxUI.navigateTo(href);
244
                }
245
            }
246
        );
247
        console.debug('Links registered!');
248
    }
249
250
    protected getFormOptions() : JQueryFormOptions
251
    {
252
        return  {
253
            success: this.onAjaxComplete,
254
            beforeSerialize: function($form, options) : boolean {
255
256
                //Update the content of textarea fields using CKEDITOR before submitting.
257
                //@ts-ignore
258
                if(typeof CKEDITOR !== 'undefined') {
259
                    //@ts-ignore
260
                    for (let name in CKEDITOR.instances) {
261
                        //@ts-ignore
262
                        CKEDITOR.instances[name].updateElement();
263
                    }
264
                }
265
266
                //Check every checkbox field, so that it will be submitted (only valid fields are submitted)
267
                $form.find("input[type=checkbox].tristate").prop('checked', true);
268
269
                return true;
270
            },
271
            beforeSubmit: function (arr, $form, options) : boolean {
272
                //When data-with-progbar is specified, then show progressbar.
273
                if($form.data("with-progbar") != undefined) {
274
                    ajaxUI.showProgressBar();
275
                }
276
                return true;
277
            }
278
        };
279
    }
280
281
    /**
282
     * Register all forms for loading via ajax.
283
     */
284
    public registerForm()
285
    {
286
287
        let options = this.getFormOptions();
288
289
        $('form').not('[data-no-ajax]').ajaxForm(options);
290
291
        console.debug('Forms registered!');
292
    }
293
294
295
    /**
296
     * Submits the given form via ajax.
297
     * @param form The form that will be submmitted.
298
     * @param btn The btn via which the form is submitted
299
     */
300
    public submitForm(form, btn = null)
301
    {
302
        let options = ajaxUI.getFormOptions();
303
304
        if(btn) {
305
            options.data = {};
306
            options.data[$(btn).attr('name')] = $(btn).attr('value');
307
        }
308
309
        $(form).ajaxSubmit(options);
310
    }
311
312
313
    /**
314
     * Show the progressbar
315
     */
316
    public showProgressBar()
317
    {
318
        //Blur content background
319
        $('#content').addClass('loading-content');
320
321
        // @ts-ignore
322
        $('#progressModal').modal({
323
            keyboard: false,
324
            backdrop: false,
325
            show: true
326
        });
327
    }
328
329
    /**
330
     * Hides the progressbar.
331
     */
332
    public hideProgressBar()
333
    {
334
        // @ts-ignore
335
        $('#progressModal').modal('hide');
336
        //Remove the remaining things of the modal
337
        $('.modal-backdrop').remove();
338
        $('body').removeClass('modal-open');
339
        $('body, .navbar').css('padding-right', "");
340
341
    }
342
343
344
    /**
345
     * Navigates to the given URL
346
     * @param url The url which should be opened.
347
     * @param show_loading Show the loading bar during loading.
348
     */
349
    public navigateTo(url : string, show_loading : boolean = true)
350
    {
351
        if(show_loading) {
352
            this.showProgressBar();
353
        }
354
        $.ajax(url, {
355
            success: this.onAjaxComplete
356
        });
357
        //$.ajax(url).promise().done(this.onAjaxComplete);
358
    }
359
360
    /**
361
     * Called when an error occurs on loading ajax. Outputs the message to the console.
362
     */
363
    private onAjaxError (event, request, settings) {
364
        'use strict';
365
        //Ignore aborted requests.
366
        if (request.statusText =='abort' || request.status == 0) {
367
            return;
368
        }
369
370
        //Ignore ajax errors with 200 code (like the ones during 2FA authentication)
371
        if(request.status == 200) {
372
            return;
373
        }
374
375
        console.error("Error getting the ajax data from server!");
376
        console.log(event);
377
        console.log(request);
378
        console.log(settings);
379
380
        ajaxUI.hideProgressBar();
381
382
        //Create error text
383
        let title = request.statusText;
384
385
        switch(request.status) {
386
            case 500:
387
                title =  'Internal Server Error!';
388
                break;
389
            case 404:
390
                title = "Site not found!";
391
                break;
392
            case 403:
393
                title = "Permission denied!";
394
                break;
395
        }
396
397
        var alert = bootbox.alert(
398
            {
399
                size: 'large',
400
                message: function() {
401
                    let msg = "Error getting data from Server! <b>Status Code: " + request.status + "</b>";
402
403
                    msg += '<br><br><a class=\"btn btn-link\" data-toggle=\"collapse\" href=\"#iframe_div\" >' + 'Show response' + "</a>";
404
                    msg += "<div class=\" collapse\" id='iframe_div'><iframe height='512' width='100%' id='iframe'></iframe></div>";
405
406
                    return msg;
407
                },
408
                title: title,
409
                callback: function () {
410
                    //Remove blur
411
                    $('#content').removeClass('loading-content');
412
                }
413
414
            });
415
416
        //@ts-ignore
417
        alert.init(function (){
418
            var dstFrame = document.getElementById('iframe');
419
            //@ts-ignore
420
            var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
421
            dstDoc.write(request.responseText);
422
            dstDoc.close();
423
        });
424
425
426
427
        //If it was a server error and response is not empty, show it to user.
428
        if(request.status == 500 && request.responseText !== "")
429
        {
430
            console.log("Response:" + request.responseText);
431
        }
432
    }
433
434
    /**
435
     * This function gets called every time, the "back" button in the browser is pressed.
436
     * We use it to load the content from history stack via ajax and to rewrite url, so we only have
437
     * to load #content-data
438
     * @param event
439
     */
440
    private onPopState(event)
441
    {
442
        let page : string = location.href;
443
        ajaxUI.statePopped = true;
444
        ajaxUI.navigateTo(page);
445
    }
446
447
    /**
448
     * This function takes the response of an ajax requests, and does the things we need to do for our AjaxUI.
449
     * This includes inserting the content and pushing history.
450
     * @param responseText
451
     * @param textStatus
452
     * @param jqXHR
453
     */
454
    private onAjaxComplete(responseText: string, textStatus: string, jqXHR: any)
455
    {
456
        console.debug("Ajax load completed!");
457
458
459
        ajaxUI.hideProgressBar();
460
461
        /* We need to do the url checking before the parseHTML, so that we dont get wrong url name, caused by scripts
462
           in the new content */
463
        // @ts-ignore
464
        let url = this.url;
465
        //Check if we were redirect to a new url, then we should use that as new url.
466
        if(ajaxUI.xhr.responseURL) {
467
            url = ajaxUI.xhr.responseURL;
468
        }
469
470
471
        //Parse response to DOM structure
472
        //We need to preserve javascript, so the table ca
473
        let dom = $.parseHTML(responseText, document, true);
474
        //And replace the content container
475
        $("#content").replaceWith($("#content", dom));
476
        //Replace login menu too (so everything is up to date)
477
        $("#login-content").replaceWith($('#login-content', dom));
478
479
        //Replace flash messages and show them
480
        $("#message-container").replaceWith($('#message-container', dom));
481
        $(".toast").toast('show');
482
483
        //Set new title
484
        let title  = extractTitle(responseText);
485
        document.title = title;
486
487
        //Push to history, if we currently arent poping an old value.
488
        if(!ajaxUI.statePopped) {
489
            history.pushState(null, title, url);
490
        } else {
491
            //Clear pop state
492
            ajaxUI.statePopped = false;
493
        }
494
495
        //Do things on the new dom
496
        ajaxUI.registerLinks();
497
        ajaxUI.registerForm();
498
        ajaxUI.initDataTables();
499
500
        //Trigger reload event
501
        $(document).trigger("ajaxUI:reload");
502
    }
503
504
    /**
505
     * Init all datatables marked with data-datatable based on their data-settings attribute.
506
     */
507
    protected initDataTables()
508
    {
509
        //@ts-ignore
510
        $($.fn.DataTable.tables()).DataTable().fixedHeader.disable();
511
        //@ts-ignore
512
        $($.fn.DataTable.tables()).DataTable().destroy();
513
514
        //Find all datatables and init it.
515
        let $tables = $('[data-datatable]');
516
        $.each($tables, function(index, table) {
517
            let $table = $(table);
518
            let settings = $table.data('settings');
519
520
            //@ts-ignore
521
            var promise = $('#part_list').initDataTables(settings,
522
                {
523
                    colReorder: true,
524
                    responsive: true,
525
                    "fixedHeader": { header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
526
                        headerOffset: $("#navbar").height()},
527
                    "buttons": [ {
528
                        "extend": 'colvis',
529
                        'className': 'mr-2 btn-light',
530
                        "text": "<i class='fa fa-cog'></i>"
531
                    }],
532
                    "select": $table.data('select') ?? false,
533
                    "rowCallback": function( row, data, index ) {
534
                        //Check if we have a level, then change color of this row
535
                        if (data.level) {
536
                            let style = "";
537
                            switch(data.level) {
538
                                case "emergency":
539
                                case "alert":
540
                                case "critical":
541
                                case "error":
542
                                    style = "table-danger";
543
                                    break;
544
                                case "warning":
545
                                    style = "table-warning";
546
                                    break;
547
                                case "notice":
548
                                    style = "table-info";
549
                                    break;
550
                            }
551
552
                            if (style){
553
                                $(row).addClass(style);
554
                            }
555
                        }
556
                    }
557
                });
558
559
            //Register links.
560
            promise.then(function() {
561
                ajaxUI.registerLinks();
562
563
                //Set the correct title in the table.
564
                let title = $('#part-card-header-src');
565
                $('#part-card-header').html(title.html());
566
                $(document).trigger('ajaxUI:dt_loaded');
567
568
569
                if($table.data('part_table')) {
570
                    //@ts-ignore
571
                    $('#dt').on( 'select.dt deselect.dt', function ( e, dt, items ) {
572
                        let selected_elements = dt.rows({selected: true});
573
                        let count = selected_elements.count();
574
575
                        if(count > 0) {
576
                            $('#select_panel').removeClass('d-none');
577
                        } else {
578
                            $('#select_panel').addClass('d-none');
579
                        }
580
581
                        $('#select_count').text(count);
582
583
                        let selected_ids_string = selected_elements.data().map(function(value, index) {
584
                            return value['id']; }
585
                            ).join(",");
586
587
                        $('#select_ids').val(selected_ids_string);
588
589
                    } );
590
                }
591
592
                //Attach event listener to update links after new page selection:
593
                $('#dt').on('draw.dt column-visibility.dt', function() {
594
                    ajaxUI.registerLinks();
595
                    $(document).trigger('ajaxUI:dt_loaded');
596
                });
597
            });
598
        });
599
600
        console.debug('Datatables inited.');
601
    }
602
}
603
604
export let ajaxUI = new AjaxUI();