egroupware/api/js/jsapi/egw_message.js

464 lines
15 KiB
JavaScript

/**
* EGroupware clientside API object
*
* @license http://opensource.org/licenses/gpl-license.php GPL - GNU General Public License
* @package etemplate
* @subpackage api
* @link http://www.egroupware.org
* @author Ralf Becker <RalfBecker@outdoor-training.de>
* @version $Id$
*/
/*egw:uses
egw_core;
*/
/**
* Methods to display a success or error message and the app-header
*
* @augments Class
* @param {string} _app application name object is instanciated for
* @param {object} _wnd window object is instanciated for
*/
egw.extend('message', egw.MODULE_WND_LOCAL, function(_app, _wnd)
{
"use strict";
_app; // not used, but required by function signature
var error_reg_exp;
var a_href_reg = /<a href="([^"]+)">([^<]+)<\/a>/img;
var new_line_reg = /<\/?(p|br)\s*\/?>\n?/ig;
// keeps alive messages stored
var alive_messages = [];
// Register an 'error' plugin, displaying using the message system
this.registerJSONPlugin(function(type, res, req) {
if (typeof res.data == 'string')
{
egw.message(res.data,'error');
return true;
}
throw 'Invalid parameters';
}, null, 'error');
/**
* Decode html entities so they can be added via .text(_str), eg. html_entity_decode('&amp;') === '&'
*
* @param {string} _str
* @returns {string}
*/
function html_entity_decode(_str)
{
return _str && _str.indexOf('&') != -1 ? jQuery('<span>'+_str+'</span>').text() : _str;
}
return {
/**
* Display an error or regular message
*
* Alle messages but type "success" are displayed 'til next message or user clicks on it.
*
* @param {string} _msg message to show or empty to remove previous message
* @param {string} _type 'help', 'info', 'error', 'warning' or 'success' (default)
* @param {string} _discardID unique string id (appname:id) in order to register
* the message as discardable. If no appname given, the id will be prefixed with
* current app. The discardID will be stored in local storage.
*
* @return {object} returns an object containing data and methods related to the message
*/
message: function(_msg, _type, _discardID)
{
var jQuery = _wnd.jQuery;
var wrapper = jQuery('.egw_message_wrapper').length > 0 ? jQuery('.egw_message_wrapper')
: jQuery(_wnd.document.createElement('div')).addClass('egw_message_wrapper noPrint').css('position', 'absolute');
if (_msg && !_type)
{
if (typeof error_reg_exp == 'undefined') error_reg_exp = new RegExp('(error|'+egw.lang('error')+')', 'i');
_type = _msg.match(error_reg_exp) ? 'error' : 'success';
}
// if we are NOT in a popup then call the message on top window
if (!this.is_popup() && _wnd !== egw.top)
{
egw(egw.top).message(_msg, _type);
return;
}
var parent = jQuery('div#divAppboxHeader');
// popup has no app-header (idots) or it is hidden by onlyPrint class (jdots) --> use body
if (!parent.length || parent.hasClass('onlyPrint'))
{
parent = jQuery('body');
}
for (var m in alive_messages)
{
// Do not add a same message twice if it's still not dismissed
if (alive_messages[m] == _msg) return;
}
if (_msg) // empty _msg just removes pervious message
{
// keeps alive messages
alive_messages.push(_msg);
// message index in stack
var msg_index = alive_messages.length-1;
// replace p and br-tags with newlines
_msg = _msg.replace(new_line_reg, "\n");
var msg_div = jQuery(_wnd.document.createElement('div'))
.attr('id','egw_message')
.text(_msg)
.addClass(_type+'_message')
.click(function(){
if (_type == 'success')
{
delete(alive_messages[msg_index]);
jQuery(msg_div).remove();
}
})
.prependTo(wrapper);
var msg_close = jQuery(_wnd.document.createElement('span'))
.click(function() {
//check if the messeage should be discarded forever
if (_type == 'info' && _discardID
&& msg_chkbox && msg_chkbox.is(':checked'))
{
var discarded = egw.getLocalStorageItem(discardAppName,'discardedMsgs');
if (!isDiscarded(_discardID))
{
if (!discarded)
{
discarded = [_discardID];
}
else
{
if (jQuery.isArray(discarded = JSON.parse(discarded))) discarded.push(_discardID);
}
egw.setLocalStorageItem(discardAppName,'discardedMsgs',JSON.stringify(discarded));
}
}
delete(alive_messages[msg_index]);
jQuery(msg_div).remove();
})
.addClass('close')
.appendTo(msg_div);
if (_type == 'success') msg_close.hide();
// discard checkbox implementation
if (_discardID && _type === 'info')
{
var discardID = _discardID.split(':');
if (discardID.length<2)
{
_discardID = egw.app_name() +":"+_discardID;
}
var discardAppName = discardID.length>1? discardID[0]: egw.app_name();
// helper function to check if the messaege is discarded
var isDiscarded = function (_id)
{
var discarded = JSON.parse(egw.getLocalStorageItem(discardAppName,'discardedMsgs'));
if (jQuery.isArray(discarded))
{
for(var i=0; i< discarded.length; i++)
{
if (discarded[i] === _id) return true;
}
}
return false;
};
//discard div container
var msg_discard =jQuery(_wnd.document.createElement('div')).addClass('discard');
// checkbox
var msg_chkbox = jQuery(_wnd.document.createElement('input'))
.attr({type:"checkbox",name:"msgChkbox"})
.click(function(e){e.stopImmediatePropagation();})
.appendTo(msg_discard);
// Label
jQuery(_wnd.document.createElement('label'))
.text(egw.lang("Don't show this again"))
.css({"font-weight":"bold"})
.attr({for:'msgChkbox'})
.appendTo(msg_discard);
if (isDiscarded(_discardID)) return;
msg_div.append(msg_discard);
}
parent.prepend(wrapper);
// replace simple a href (NO other attribute, to gard agains XSS!)
var matches = a_href_reg.exec(_msg);
if (matches)
{
var parts = _msg.split(matches[0]);
var href = html_entity_decode(matches[1]);
msg_div.text(parts[0]);
msg_div.append(jQuery(_wnd.document.createElement('a'))
.attr({href: href, target: href.indexOf(egw.webserverUrl) != 0 ? '_blank' : '_self'})
.text(matches[2]));
msg_div.append(jQuery(_wnd.document.createElement('span')).text(parts[1]));
}
// center the message
wrapper.css('right', ((jQuery(_wnd).innerWidth()-msg_div.width())/2)+'px');
if (_type == 'success') // clear message again after some time, if no error
{
_wnd.setTimeout(function() {
msg_div.remove();
delete(alive_messages[msg_index]);
}, 5000);
}
}
return {
node: msg_div,
message: _msg,
index: msg_index,
close: function(){msg_close.click();}
};
},
/**
* Are we running in a popup
*
* @returns {boolean} true: popup, false: main window
*/
is_popup: function ()
{
var popup = false;
try {
if (_wnd.opener && _wnd.opener != _wnd && typeof _wnd.opener.top.egw == 'function')
{
popup = true;
}
}
catch(e) {
// ignore SecurityError exception if opener is different security context / cross-origin
}
return popup;
},
/**
* Active app independent if we are using a framed template-set or not
*
* @returns {string}
*/
app_name: function()
{
return !this.is_popup() && _wnd.framework && _wnd.framework.activeApp ? _wnd.framework.activeApp.appName : _wnd.egw_appName;
},
/**
* Update app-header and website-title
*
* @param {string} _header
* @param {string} _app Application name, if not for the current app
*/
app_header: function(_header,_app)
{
// not for popups and only for framed templates
if (!this.is_popup() && _wnd.framework && _wnd.framework.setWebsiteTitle)
{
var app = _app || this.app_name();
var title = _wnd.document.title.replace(/[.*]$/, '['+_header+']');
_wnd.framework.setWebsiteTitle.call(_wnd.framework, app, title, _header);
return;
}
_wnd.jQuery('div#divAppboxHeader').text(_header);
_wnd.document.title = _wnd.document.title.replace(/[.*]$/, '['+_header+']');
},
/**
* Loading prompt is for building a loading animation and show it to user
* while a request is under progress.
*
* @param {string} _id a unique id to be able to distinguish loading-prompts
* @param {boolean} _stat true to show the loading and false to remove it
* @param {string} _msg a message to show while loading
* @param {string|jQuery _node} _node DOM selector id or jquery DOM object, default is body
* @param {string} _mode defines the animation mode, default mode is spinner
* animation modes:
* - spinner: a sphere with a spinning bar inside
* - horizental: a horizental bar
*
* @returns {jquery dom object|null} returns jQuery DOM object or null in case of hiding
*/
loading_prompt: function(_id,_stat,_msg,_node, _mode)
{
var $container = '';
var jQuery = _wnd.jQuery;
var id = _id? 'egw-loadin-prompt_'+_id: 'egw-loading-prompt_1';
var mode = _mode || 'spinner';
if (_stat)
{
var $node = _node? jQuery(_node): jQuery('body');
var $container = jQuery(_wnd.document.createElement('div'))
.attr('id', id)
.addClass('egw-loading-prompt-container ui-front');
var $text = jQuery(_wnd.document.createElement('span'))
.addClass('egw-loading-prompt-'+mode+'-msg')
.text(_msg)
.appendTo($container);
var $animator = jQuery(_wnd.document.createElement('div'))
.addClass('egw-loading-prompt-'+mode+'-animator')
.appendTo($container);
if (!_wnd.document.getElementById(id)) $container.insertBefore($node);
return $container;
}
else
{
$container = jQuery(_wnd.document.getElementById(id));
if ($container.length > 0) $container.remove();
return null;
}
},
/**
* Refresh given application _targetapp display of entry _app _id, incl. outputting _msg
*
* Default implementation here only reloads window with it's current url with an added msg=_msg attached
*
* @param {string} _msg message (already translated) to show, eg. 'Entry deleted'
* @param {string} _app application name
* @param {(string|number)} _id id of entry to refresh or null
* @param {string} _type either 'update', 'edit', 'delete', 'add' or null
* - update: request just modified data from given rows. Sorting is not considered,
* so if the sort field is changed, the row will not be moved.
* - update-in-place: update row, but do NOT move it, or refresh if uid does not exist
* - edit: rows changed, but sorting may be affected. Requires full reload.
* - delete: just delete the given rows clientside (no server interaction neccessary)
* - add: requires full reload for proper sorting
* @param {string} _targetapp which app's window should be refreshed, default current
* @param {(string|RegExp)} _replace regular expression to replace in url
* @param {string} _with
* @param {string} _msg_type 'error', 'warning' or 'success' (default)
* @param {object|null} _links app => array of ids of linked entries
* or null, if not triggered on server-side, which adds that info
*/
refresh: function(_msg, _app, _id, _type, _targetapp, _replace, _with, _msg_type, _links)
{
// Log for debugging purposes
this.debug("log", "egw_refresh(%s, %s, %s, %o, %s, %s)", _msg, _app, _id, _type, _targetapp, _replace, _with, _msg_type, _links);
var win = typeof _targetapp != 'undefined' ? _wnd.egw_appWindow(_targetapp) : _wnd;
this.message(_msg, _msg_type);
if(typeof _links == "undefined")
{
_links = [];
}
// notify app observers: if observer for _app itself returns false, no regular refresh will take place
// app's own observer can replace current app_refresh functionality
var no_regular_refresh = false;
for(var app_obj of _wnd.egw.window.EgwApp) // run observers in main window (eg. not iframe, which might be opener!)
{
if (typeof app_obj.observer == 'function' &&
app_obj.observer(_msg, _app, _id, _type, _msg_type, _links) === false && app_obj.appname === _app)
{
no_regular_refresh = true;
}
}
if (no_regular_refresh) return;
// if we have a framework template, let it deal with refresh, unless it returns a DOMwindow for us to refresh
if (win.framework && win.framework.refresh &&
!(win = win.framework.refresh(_msg, _app, _id, _type, _targetapp, _replace, _with, _msg_type)))
{
return;
}
// if window registered an app_refresh method or overwritten app_refresh, just call it
if(typeof win.app_refresh == "function" && typeof win.app_refresh.registered == "undefined" ||
typeof win.app_refresh != "undefined" && win.app_refresh.registered(_app))
{
win.app_refresh(_msg, _app, _id, _type);
return;
}
// etemplate2 specific to avoid reloading whole page
if(typeof win.etemplate2 != "undefined" && win.etemplate2.app_refresh)
{
var refresh_done = win.etemplate2.app_refresh(_msg, _app, _id, _type);
// Refresh target or current app too
if ((_targetapp || this.app_name()) != _app)
{
refresh_done = win.etemplate2.app_refresh(_msg, _targetapp || this.app_name()) || refresh_done;
}
//In case that we have etemplate2 ready but it's empty and refresh is not done
if (refresh_done) return;
}
// fallback refresh by reloading window
var href = win.location.href;
if (typeof _replace != 'undefined')
{
href = href.replace(typeof _replace == 'string' ? new RegExp(_replace) : _replace, (typeof _with != 'undefined' && _with != null) ? _with : '');
}
if (href.indexOf('msg=') != -1)
{
href = href.replace(/msg=[^&]*/,'msg='+encodeURIComponent(_msg));
}
else if (_msg)
{
href += (href.indexOf('?') != -1 ? '&' : '?') + 'msg=' + encodeURIComponent(_msg);
}
//alert('egw_refresh() about to call '+href);
win.location.href = href;
},
/**
* Handle a push notification about entry changes from the websocket
*
* @param pushData
* @param {string} pushData.app application name
* @param {(string|number)} pushData.id id of entry to refresh or null
* @param {string} pushData.type either 'update', 'edit', 'delete', 'add' or null
* - update: request just modified data from given rows. Sorting is not considered,
* so if the sort field is changed, the row will not be moved.
* - edit: rows changed, but sorting may be affected. Requires full reload.
* - delete: just delete the given rows clientside (no server interaction neccessary)
* - add: requires full reload for proper sorting
* @param {object|null} pushData.acl Extra data for determining relevance. eg: owner or responsible to decide if update is necessary
* @param {number} pushData.account_id User that caused the notification
*/
push: function(pushData)
{
// Log for debugging purposes
this.debug("log", "push(%o)", pushData);
if (typeof pushData == "undefined")
{
this.debug('warn', "Push sent nothing");
return;
}
// notify app observers
for (var app_obj of _wnd.egw.window.EgwApp) // run observers in main window (eg. not iframe, which might be opener!)
{
if (typeof app_obj.push == 'function')
{
app_obj.push(pushData);
}
}
}
};
});