/*
* This file is part of Part-DB (https://github.com/Part-DB/Part-DB-symfony).
*
* Copyright (C) 2019 - 2020 Jan Böhmer (https://github.com/jbtronics)
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published
* by the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
/**
* Extract the title (The name between the
tags) of a HTML snippet.
* @param {string} html The HTML code which should be searched.
* @returns {string} The title extracted from the html.
*/
function extractTitle(html : string) : string {
let title : string = "";
let regex = /(.*?)<\/title>/gi;
if (regex.test(html)) {
let matches = html.match(regex);
for(let match in matches) {
title = $(matches[match]).text();
}
}
return title;
}
class AjaxUI {
protected BASE = "/";
private trees_filled : boolean = false;
private statePopped : boolean = false;
public xhr : XMLHttpRequest;
public constructor()
{
//Make back in the browser go back in history
window.onpopstate = this.onPopState;
$(document).ajaxError(this.onAjaxError.bind(this));
//$(document).ajaxComplete(this.onAjaxComplete.bind(this));
}
/**
* Starts the ajax ui und execute handlers registered in addStartAction().
* Should be called in a document.ready, after handlers are set.
*/
public start(disabled : boolean = false)
{
if(disabled) {
return;
}
console.info("AjaxUI started!");
this.BASE = $("body").data("base-url");
//If path doesn't end with slash, add it.
if(this.BASE[this.BASE.length - 1] !== '/') {
this.BASE = this.BASE + '/';
}
console.info("Base path is " + this.BASE);
//Show flash messages
$(".toast").toast('show');
/**
* Save the XMLHttpRequest that jQuery used, to the class, so we can acess the responseURL property.
* This is a work-around as long jQuery does not implement this property in its jQXHR objects.
*/
//@ts-ignore
jQuery.ajaxSettings.xhr = function () {
//@ts-ignore
let xhr = new window.XMLHttpRequest();
//Save the XMLHttpRequest to the class.
ajaxUI.xhr = xhr;
return xhr;
};
this.registerLinks();
this.registerForm();
this.fillTrees();
this.initDataTables();
//Trigger start event
$(document).trigger("ajaxUI:start");
}
/**
* Fill the trees with the given data.
*/
public fillTrees()
{
let categories = localStorage.getItem("tree_datasource_tree-categories");
let devices = localStorage.getItem("tree_datasource_tree-devices");
let tools = localStorage.getItem("tree_datasource_tree-tools");
if(categories == null) {
categories = "categories";
}
if(devices == null) {
devices = "devices";
}
if(tools == null) {
tools = "tools";
}
this.treeLoadDataSource("tree-categories", categories);
this.treeLoadDataSource("tree-devices", devices);
this.treeLoadDataSource("tree-tools", tools);
this.trees_filled = true;
//Register tree btns to expand all, or to switch datasource.
$(".tree-btns").click(function (event) {
event.preventDefault();
$(this).parents("div.dropdown").removeClass('show');
//$(this).closest(".dropdown-menu").removeClass('show');
$(".dropdown-menu.show").removeClass("show");
let mode = $(this).data("mode");
let target = $(this).data("target");
let text = $(this).text() + " \n"; //Add caret or it will be removed, when written into title
if (mode==="collapse") {
// @ts-ignore
$('#' + target).treeview('collapseAll', { silent: true });
}
else if(mode==="expand") {
// @ts-ignore
$('#' + target).treeview('expandAll', { silent: true });
} else {
localStorage.setItem("tree_datasource_" + target, mode);
ajaxUI.treeLoadDataSource(target, mode);
}
return false;
});
}
/**
* Load the given url into the tree with the given id.
* @param target_id
* @param datasource
*/
protected treeLoadDataSource(target_id, datasource) {
let text : string = $(".tree-btns[data-mode='" + datasource + "']").html();
text = text + " \n"; //Add caret or it will be removed, when written into title
switch(datasource) {
case "categories":
ajaxUI.initTree("#" + target_id, 'tree/categories');
break;
case "locations":
ajaxUI.initTree("#" + target_id, 'tree/locations');
break;
case "footprints":
ajaxUI.initTree("#" + target_id, 'tree/footprints');
break;
case "manufacturers":
ajaxUI.initTree("#" + target_id, 'tree/manufacturers');
break;
case "suppliers":
ajaxUI.initTree("#" + target_id, 'tree/suppliers');
break;
case "tools":
ajaxUI.initTree("#" + target_id, 'tree/tools');
break;
case "devices":
ajaxUI.initTree("#" + target_id, 'tree/devices');
break;
}
$( "#" + target_id + "-title").html(text);
}
/**
* Fill a treeview with data from the given url.
* @param tree The Jquery selector for the tree (e.g. "#tree-tools")
* @param url The url from where the data should be loaded
*/
public initTree(tree, url) {
//let contextmenu_handler = this.onNodeContextmenu;
$.getJSON(ajaxUI.BASE + url, function (data) {
// @ts-ignore
$(tree).treeview({
data: data,
enableLinks: false,
showIcon: false,
showBorder: true,
searchResultBackColor: '#ffc107',
searchResultColor: '#000',
onNodeSelected: function(event, data) {
if(data.href) {
ajaxUI.navigateTo(data.href);
}
},
//onNodeContextmenu: contextmenu_handler,
expandIcon: "fas fa-plus fa-fw fa-treeview", collapseIcon: "fas fa-minus fa-fw fa-treeview"})
.on('initialized', function() {
$(this).treeview('collapseAll', { silent: true });
//Implement searching if needed.
if($(this).data('treeSearch')) {
let _this = this;
let $search = $($(this).data('treeSearch'));
$search.on( 'input', function() {
$(_this).treeview('collapseAll', { silent: true });
$(_this).treeview('search', [$search.val()]);
});
}
});
});
}
/**
* Register all links, for loading via ajax.
*/
public registerLinks()
{
// Unbind all old handlers, so the things are not executed multiple times.
$('a').not(".link-external, [data-no-ajax], .page-link, [href^='javascript'], [href^='#']").unbind('click').click(function (event) {
let a = $(this);
let href = $.trim(a.attr("href"));
//Ignore links without href attr and nav links ('they only have a #)
if(href != null && href != "" && href.charAt(0) !== '#') {
event.preventDefault();
ajaxUI.navigateTo(href);
}
}
);
console.debug('Links registered!');
}
protected getFormOptions() : JQueryFormOptions
{
return {
success: this.onAjaxComplete,
beforeSerialize: function($form, options) : boolean {
//Update the content of textarea fields using CKEDITOR before submitting.
//@ts-ignore
if(typeof CKEDITOR !== 'undefined') {
//@ts-ignore
for (let name in CKEDITOR.instances) {
//@ts-ignore
CKEDITOR.instances[name].updateElement();
}
}
//Check every checkbox field, so that it will be submitted (only valid fields are submitted)
$form.find("input[type=checkbox].tristate").prop('checked', true);
return true;
},
beforeSubmit: function (arr, $form, options) : boolean {
//When data-with-progbar is specified, then show progressbar.
if($form.data("with-progbar") != undefined) {
ajaxUI.showProgressBar();
}
return true;
}
};
}
/**
* Register all forms for loading via ajax.
*/
public registerForm()
{
let options = this.getFormOptions();
$('form').not('[data-no-ajax]').ajaxForm(options);
console.debug('Forms registered!');
}
/**
* Submits the given form via ajax.
* @param form The form that will be submmitted.
* @param btn The btn via which the form is submitted
*/
public submitForm(form, btn = null)
{
let options = ajaxUI.getFormOptions();
if(btn) {
options.data = {};
options.data[$(btn).attr('name')] = $(btn).attr('value');
}
$(form).ajaxSubmit(options);
}
/**
* Show the progressbar
*/
public showProgressBar()
{
//Blur content background
$('#content').addClass('loading-content');
// @ts-ignore
$('#progressModal').modal({
keyboard: false,
backdrop: false,
show: true
});
}
/**
* Hides the progressbar.
*/
public hideProgressBar()
{
// @ts-ignore
$('#progressModal').modal('hide');
//Remove the remaining things of the modal
$('.modal-backdrop').remove();
$('body').removeClass('modal-open');
$('body, .navbar').css('padding-right', "");
}
/**
* Navigates to the given URL
* @param url The url which should be opened.
* @param show_loading Show the loading bar during loading.
*/
public navigateTo(url : string, show_loading : boolean = true)
{
if(show_loading) {
this.showProgressBar();
}
$.ajax(url, {
success: this.onAjaxComplete
});
//$.ajax(url).promise().done(this.onAjaxComplete);
}
/**
* Called when an error occurs on loading ajax. Outputs the message to the console.
*/
private onAjaxError (event, request, settings) {
'use strict';
//Ignore aborted requests.
if (request.statusText =='abort') {
return;
}
//Ignore ajax errors with 200 code (like the ones during 2FA authentication)
if(request.status == 200) {
return;
}
console.error("Error getting the ajax data from server!");
console.log(event);
console.log(request);
console.log(settings);
ajaxUI.hideProgressBar();
//Create error text
let title = "";
switch(request.status) {
case 500:
title = 'Internal Server Error!';
break;
case 404:
title = "Site not found!";
break;
case 403:
title = "Permission denied!";
break;
}
var alert = bootbox.alert(
{
size: 'large',
message: function() {
let msg = "Error getting data from Server! Status Code: " + request.status + "";
msg += '
' + 'Show response' + "";
msg += "";
return msg;
},
title: title,
callback: function () {
//Remove blur
$('#content').removeClass('loading-content');
}
});
//@ts-ignore
alert.init(function (){
var dstFrame = document.getElementById('iframe');
//@ts-ignore
var dstDoc = dstFrame.contentDocument || dstFrame.contentWindow.document;
dstDoc.write(request.responseText);
dstDoc.close();
});
//If it was a server error and response is not empty, show it to user.
if(request.status == 500 && request.responseText !== "")
{
console.log("Response:" + request.responseText);
}
}
/**
* This function gets called every time, the "back" button in the browser is pressed.
* We use it to load the content from history stack via ajax and to rewrite url, so we only have
* to load #content-data
* @param event
*/
private onPopState(event)
{
let page : string = location.href;
ajaxUI.statePopped = true;
ajaxUI.navigateTo(page);
}
/**
* This function takes the response of an ajax requests, and does the things we need to do for our AjaxUI.
* This includes inserting the content and pushing history.
* @param responseText
* @param textStatus
* @param jqXHR
*/
private onAjaxComplete(responseText: string, textStatus: string, jqXHR: any)
{
console.debug("Ajax load completed!");
ajaxUI.hideProgressBar();
/* We need to do the url checking before the parseHTML, so that we dont get wrong url name, caused by scripts
in the new content */
// @ts-ignore
let url = this.url;
//Check if we were redirect to a new url, then we should use that as new url.
if(ajaxUI.xhr.responseURL) {
url = ajaxUI.xhr.responseURL;
}
//Parse response to DOM structure
//We need to preserve javascript, so the table ca
let dom = $.parseHTML(responseText, document, true);
//And replace the content container
$("#content").replaceWith($("#content", dom));
//Replace login menu too (so everything is up to date)
$("#login-content").replaceWith($('#login-content', dom));
//Replace flash messages and show them
$("#message-container").replaceWith($('#message-container', dom));
$(".toast").toast('show');
//Set new title
let title = extractTitle(responseText);
document.title = title;
//Push to history, if we currently arent poping an old value.
if(!ajaxUI.statePopped) {
history.pushState(null, title, url);
} else {
//Clear pop state
ajaxUI.statePopped = false;
}
//Do things on the new dom
ajaxUI.registerLinks();
ajaxUI.registerForm();
ajaxUI.initDataTables();
//Trigger reload event
$(document).trigger("ajaxUI:reload");
}
/**
* Init all datatables marked with data-datatable based on their data-settings attribute.
*/
protected initDataTables()
{
//@ts-ignore
$($.fn.DataTable.tables()).DataTable().fixedHeader.disable();
//@ts-ignore
$($.fn.DataTable.tables()).DataTable().destroy();
//Find all datatables and init it.
let $tables = $('[data-datatable]');
$.each($tables, function(index, table) {
let $table = $(table);
let settings = $table.data('settings');
//@ts-ignore
var promise = $('#part_list').initDataTables(settings,
{
colReorder: true,
responsive: true,
"fixedHeader": { header: $(window).width() >= 768, //Only enable fixedHeaders on devices with big screen. Fixes scrolling issues on smartphones.
headerOffset: $("#navbar").height()},
"buttons": [ {
"extend": 'colvis',
'className': 'mr-2 btn-light',
"text": ""
}],
"rowCallback": function( row, data, index ) {
//Check if we have a level, then change color of this row
if (data.level) {
let style = "";
switch(data.level) {
case "emergency":
case "alert":
case "critical":
case "error":
style = "table-danger";
break;
case "warning":
style = "table-warning";
break;
case "notice":
style = "table-info";
break;
}
if (style){
$(row).addClass(style);
}
}
}
});
//Register links.
promise.then(function() {
ajaxUI.registerLinks();
//Set the correct title in the table.
let title = $('#part-card-header-src');
$('#part-card-header').html(title.html());
$(document).trigger('ajaxUI:dt_loaded');
//Attach event listener to update links after new page selection:
$('#dt').on('draw.dt column-visibility.dt', function() {
ajaxUI.registerLinks();
$(document).trigger('ajaxUI:dt_loaded');
});
});
});
console.debug('Datatables inited.');
}
}
export let ajaxUI = new AjaxUI();