You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

529 lines
13 KiB

/*
** Zabbix
** Copyright (C) 2001-2023 Zabbix SIA
**
** This program is free software; you can redistribute it and/or modify
** it under the terms of the GNU General Public License as published by
** the Free Software Foundation; either version 2 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 General Public License for more details.
**
** You should have received a copy of the GNU General Public License
** along with this program; if not, write to the Free Software
** Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
**/
/**
* Overlay object DOM node to be mounted before document body closing tag.
*
* @param {string} type
* @param {string} (optional) dialogueid
*
* @prop {jQuery} $dialogue
* @prop {jQuery} $backdrop
* @prop {string} type
* @prop {string} headerid
*/
function Overlay(type, dialogueid) {
this.type = type;
this.dialogueid = dialogueid || overlays_stack.getNextId();
this.headerid = 'dashboard-widget-head-title-' + this.dialogueid;
this.$backdrop = jQuery('<div>', {
'class': 'overlay-bg',
'data-dialogueid': this.dialogueid
});
this.$dialogue = jQuery('<div>', {
'class': 'overlay-dialogue modal',
'data-dialogueid': this.dialogueid,
'role': 'dialog',
'aria-modal': 'true',
'aria-labelledby': this.headerid
});
this.$dialogue.$header = jQuery('<h4>', {id: this.headerid});
const $close_btn = jQuery('<button>', {
class: 'btn-overlay-close',
title: t('S_CLOSE')
}).click(function(e) {
overlayDialogueDestroy(this.dialogueid);
e.preventDefault();
}.bind(this));
this.$dialogue.$controls = jQuery('<div>', {class: 'overlay-dialogue-controls'});
this.$dialogue.$head = jQuery('<div>', {class: 'dashboard-widget-head'});
this.$dialogue.$body = jQuery('<div>', {class: 'overlay-dialogue-body'});
this.$dialogue.$debug = jQuery('<pre>', {class: 'debug-output'});
this.$dialogue.$footer = jQuery('<div>', {class: 'overlay-dialogue-footer'});
this.$dialogue.$script = jQuery('<script>');
this.$dialogue.$head.append(this.$dialogue.$header, $close_btn);
this.$dialogue.append(this.$dialogue.$head);
this.$dialogue.append(this.$dialogue.$body);
this.$dialogue.append(this.$dialogue.$footer);
this.$dialogue.$body.on('submit', 'form', function(e) {
if (this.$btn_submit) {
e.preventDefault();
this.$btn_submit.trigger('click');
}
}.bind(this));
this.center_dialog_animation_frame = null;
this.center_dialog_function = () => {
if (this.center_dialog_animation_frame !== null) {
cancelAnimationFrame(this.center_dialog_animation_frame);
}
this.center_dialog_animation_frame = requestAnimationFrame(() => {
this.center_dialog_animation_frame = null;
this.centerDialog();
});
};
var body_mutation_observer = window.MutationObserver || window.WebKitMutationObserver;
this.body_mutation_observer = new body_mutation_observer(this.center_dialog_function);
jQuery(window).resize(function() {
this.$dialogue.is(':visible') && this.centerDialog();
}.bind(this));
this.setProperties({
content: jQuery('<div>', {'height': '68px', class: 'is-loading'})
});
}
/**
* Centers the $dialog.
*/
Overlay.prototype.centerDialog = function() {
var body_scroll_height = this.$dialogue.$body[0].scrollHeight,
body_height = this.$dialogue.$body.innerHeight();
if (body_height != Math.floor(body_height)) {
// The body height is often about a half pixel less than the height.
body_height = Math.floor(body_height) + 1;
}
// A fix for IE and Edge to stop popup width flickering when having vertical scrollbar.
this.$dialogue.$body.css('overflow-y', body_scroll_height > body_height ? 'scroll' : 'hidden');
// Allow full width to determine actual width taken by the contents.
this.$dialogue.css({
'left': 0,
'top': 0
});
this.$dialogue.css({
'left': Math.max(0, Math.floor((jQuery(window).width() - this.$dialogue.outerWidth(true)) / 2)) + 'px',
'top': this.$dialogue.hasClass('position-middle')
? Math.max(0, Math.floor((jQuery(window).height() - this.$dialogue.outerHeight(true)) / 2)) + 'px'
: ''
});
var size = {
width: this.$dialogue.$body[0].scrollWidth,
height: this.$dialogue.$body[0].scrollHeight
},
size_saved = this.$dialogue.data('size') || size;
if (JSON.stringify(size) !== JSON.stringify(size_saved)) {
this.$dialogue.trigger('overlay-dialogue-resize', [size, size_saved]);
}
this.$dialogue.data('size', size);
};
/**
* Determines element to place focus on and focuses it if found.
*/
Overlay.prototype.recoverFocus = function() {
if (this.$btn_focus) {
this.$btn_focus.focus();
return;
}
if (jQuery('[autofocus=autofocus]', this.$dialogue).length) {
jQuery('[autofocus=autofocus]', this.$dialogue).first().focus();
}
else if (jQuery('.overlay-dialogue-body form :focusable', this.$dialogue).length) {
jQuery('.overlay-dialogue-body form :focusable', this.$dialogue).first().focus();
}
else {
jQuery(':focusable:first', this.$dialogue).focus();
}
};
/**
* Binds keyboard events to contain focus within dialogue window.
*/
Overlay.prototype.containFocus = function() {
var focusable = jQuery(':focusable', this.$dialogue);
focusable.off('keydown.containFocus');
if (focusable.length > 1) {
var first_focusable = focusable.filter(':first'),
last_focusable = focusable.filter(':last');
first_focusable
.on('keydown.containFocus', function(e) {
// TAB and SHIFT
if (e.which == 9 && e.shiftKey) {
last_focusable.focus();
return false;
}
});
last_focusable
.on('keydown.containFocus', function(e) {
// TAB and not SHIFT
if (e.which == 9 && !e.shiftKey) {
first_focusable.focus();
return false;
}
});
}
else {
focusable
.on('keydown.containFocus', function(e) {
if (e.which == 9) {
return false;
}
});
}
};
/**
* Sets dialogue in loading state.
*/
Overlay.prototype.setLoading = function() {
this.$dialogue.$body.addClass('is-loading is-loading-fadein');
this.$dialogue.$controls.find('z-select, button').prop('disabled', true);
this.$dialogue.$footer.find('button:not(.js-cancel)').each(function() {
$(this).prop('disabled', true);
});
};
/**
* Sets dialogue in idle state.
*/
Overlay.prototype.unsetLoading = function() {
this.$dialogue.$body.removeClass('is-loading is-loading-fadein');
this.$dialogue.$footer.find('button:not(.js-cancel)').each(function() {
if ($(this).data('disabled') !== true) {
$(this).removeClass('is-loading').prop('disabled', false);
}
});
};
/**
* @param {string} action
* @param {array|object} options (optional)
*
* @return {jQuery.XHR}
*/
Overlay.prototype.load = function(action, options) {
var url = new Curl('zabbix.php');
url.setArgument('action', action);
// Properties 'action' and 'options' are stored to enable popup reload. This may be done outside the class.
this.action = action;
this.options = options;
if (this.xhr) {
this.xhr.abort();
}
this.setLoading();
this.xhr = jQuery.ajax({
url: url.getUrl(),
type: 'post',
dataType: 'json',
data: options
});
this.xhr.always(function() {
this.unsetLoading();
}.bind(this));
return this.xhr;
};
/**
* Removes associated nodes from DOM.
*/
Overlay.prototype.unmount = function() {
this.cancel_action && this.cancel_action();
jQuery.unsubscribe('debug.click', this.center_dialog_function);
this.unsetProperty('prevent_navigation');
this.$backdrop.remove();
this.$dialogue.remove();
this.body_mutation_observer.disconnect();
if (this.center_dialog_animation_frame !== null) {
cancelAnimationFrame(this.center_dialog_animation_frame);
}
var $wrapper = jQuery('.wrapper');
if (!jQuery('[data-dialogueid]').length) {
$wrapper.css('overflow', $wrapper.data('overflow'));
$wrapper.removeData('overflow');
}
};
/**
* Appends associated nodes to document body.
*/
Overlay.prototype.mount = function() {
var $wrapper = jQuery('.wrapper');
if (!jQuery('[data-dialogueid]').length) {
$wrapper.data('overflow', $wrapper.css('overflow'));
$wrapper.css('overflow', 'hidden');
}
this.$backdrop.appendTo($wrapper);
this.$dialogue.appendTo($wrapper);
for (const dialog_part of ['$header', '$controls', '$body', '$footer']) {
this.body_mutation_observer.observe(this.$dialogue[dialog_part][0], {
childList: true,
subtree: true,
attributeFilter: ['style', 'class']
});
}
this.centerDialog();
jQuery.subscribe('debug.click', this.center_dialog_function);
};
/**
* @param {object} obj
*
* @return {jQuery}
*/
Overlay.prototype.makeButton = function(obj) {
var $button = jQuery('<button>', {
type: 'button',
text: obj.title
});
$button.on('click', function(e) {
if (('confirmation' in obj) && !confirm(obj.confirmation)) {
e.preventDefault();
return;
}
if (!('cancel' in obj) || !obj.cancel) {
$(e.target)
.blur()
.addClass('is-loading')
.prop('disabled', true)
.siblings(':not(.js-cancel)')
.prop('disabled', true);
}
if (obj.action && obj.action(this) !== false) {
this.cancel_action = null;
if (!obj.keepOpen) {
overlayDialogueDestroy(this.dialogueid);
}
}
e.preventDefault();
}.bind(this));
if (obj.class) {
$button.addClass(obj.class);
}
if (obj.enabled === false) {
$button
.prop('disabled', true)
.data('disabled', true);
}
return $button;
};
/**
* @param {array} arr
*
* @return {array}
*/
Overlay.prototype.makeButtons = function(arr) {
var buttons = [];
this.$btn_submit = null;
this.$btn_focus = null;
arr.forEach(function(obj) {
if (typeof obj.action === 'string') {
obj.action = new Function('overlay', obj.action);
}
var $button = this.makeButton(obj);
if (obj.cancel) {
this.cancel_action = obj.action;
}
if (obj.isSubmit) {
this.$btn_submit = $button;
}
if (obj.focused) {
this.$btn_focus = $button;
}
buttons.push($button);
}.bind(this));
return buttons;
};
Overlay.prototype.preventNavigation = function(event) {
event.preventDefault();
event.returnValue = '';
};
/**
* @param {string} key
*/
Overlay.prototype.unsetProperty = function(key) {
switch (key) {
case 'title':
this.$dialogue.$header.text('');
break;
case 'doc_url':
const doc_link = this.$dialogue.$head[0].querySelector('.' + ZBX_ICON_HELP_SMALL);
if (doc_link !== null) {
doc_link.remove();
}
break;
case 'buttons':
this.$dialogue.$footer.find('button').remove();
break;
case 'content':
this.$dialogue.$body.html('');
if (this.$dialogue.$debug.html().length) {
this.$dialogue.$body.append(this.$dialogue.$debug);
}
break;
case 'controls':
this.$dialogue.$controls.remove();
break;
case 'debug':
this.$dialogue.$debug.remove();
break;
case 'script_inline':
this.$dialogue.$script.remove();
break;
case 'prevent_navigation':
window.removeEventListener('beforeunload', this.preventNavigation);
break;
}
};
/**
* Evaluates and applies properties.
*
* @param {object} obj
*/
Overlay.prototype.setProperties = function(obj) {
for (var key in obj) {
if (!obj[key]) {
this.unsetProperty(key);
continue;
}
switch (key) {
case 'class':
this.$dialogue.addClass(obj[key]);
break;
case 'title':
this.$dialogue.$header.text(obj[key]);
break;
case 'doc_url':
this.unsetProperty(key);
this.$dialogue.$header[0].insertAdjacentHTML('afterend', `
<a class="${ZBX_STYLE_BTN_ICON} ${ZBX_ICON_HELP_SMALL}" target="_blank" title="${t('Help')}" href="${obj[key]}"></a>
`);
break;
case 'buttons':
this.unsetProperty(key);
this.$dialogue.$footer.append(this.makeButtons(obj[key]));
break;
case 'footer':
this.unsetProperty(key);
this.$dialogue.$footer.append(obj[key]);
break;
case 'content':
this.$dialogue.$body.html(obj[key]);
if (this.$dialogue.$debug.html().length) {
this.$dialogue.$body.append(this.$dialogue.$debug);
}
break;
case 'controls':
this.$dialogue.$controls.html(obj[key]);
this.$dialogue.$body.before(this.$dialogue.$controls);
break;
case 'debug':
this.$dialogue.$debug.html(jQuery(obj[key]).html());
this.$dialogue.$body.append(this.$dialogue.$debug);
break;
case 'script_inline':
this.unsetProperty(key);
// See: jQuery.html() rnoInnerhtml = /<script|<style|<link/i
// If content matches this regex it will be parsed in jQuery.buildFragment as HTML, but here we have JS.
this.$dialogue.$script.get(0).innerHTML = obj[key];
this.$dialogue.$footer.prepend(this.$dialogue.$script);
break;
case 'prevent_navigation':
this.unsetProperty(key);
window.addEventListener('beforeunload', this.preventNavigation, {passive: false});
break;
case 'element':
this.element = obj[key];
break;
case 'data':
this.data = obj[key];
break;
}
}
};