flectra/addons/web/static/src/js/chrome/abstract_web_client.js

371 lines
15 KiB
JavaScript

flectra.define('web.AbstractWebClient', function (require) {
"use strict";
/**
* AbstractWebClient
*
* This class defines a simple, basic web client. It is mostly functional.
* The WebClient is in some way the most important class for the web framework:
* - this is the class that instantiate everything else,
* - it is the top of the component tree,
* - it coordinates many events bubbling up
*/
var ActionManager = require('web.ActionManager');
var concurrency = require('web.concurrency');
var core = require('web.core');
var config = require('web.config');
var crash_manager = require('web.crash_manager');
var data_manager = require('web.data_manager');
var Dialog = require('web.Dialog');
var Loading = require('web.Loading');
var mixins = require('web.mixins');
var NotificationManager = require('web.notification').NotificationManager;
var RainbowMan = require('web.rainbow_man');
var session = require('web.session');
var Widget = require('web.Widget');
var _t = core._t;
var qweb = core.qweb;
var AbstractWebClient = Widget.extend(mixins.ServiceProvider, {
custom_events: {
clear_uncommitted_changes: function (e) {
this.clear_uncommitted_changes().then(e.data.callback);
},
toggle_fullscreen: function (event) {
this.toggle_fullscreen(event.data.fullscreen);
},
current_action_updated: function (e) {
this.current_action_updated(e.data.action);
},
// GENERIC SERVICES
// the next events are dedicated to generic services required by
// downstream widgets. Mainly side effects, such as rpcs, notifications
// or cache.
// notifications, warnings and effects
notification: function (e) {
if(this.notification_manager) {
this.notification_manager.notify(e.data.title, e.data.message, e.data.sticky);
}
},
warning: '_onDisplayWarning',
load_views: function (event) {
var params = {
model: event.data.modelName,
context: event.data.context,
views_descr: event.data.views,
};
return data_manager
.load_views(params, event.data.options || {})
.then(event.data.on_success);
},
load_filters: function (event) {
return data_manager
.load_filters(event.data.dataset, event.data.action_id)
.then(event.data.on_success);
},
show_effect: '_onShowEffect',
// session
get_session: function (event) {
if (event.data.callback) {
event.data.callback(session);
}
},
do_action: function (event) {
this.do_action(event.data.action, event.data.options || {}).then(function (result) {
if (event.data.on_success) {
event.data.on_success(result);
}
}).fail(function (result) {
if (event.data.on_fail) {
event.data.on_fail(result);
}
});
},
},
init: function (parent) {
this.client_options = {};
mixins.ServiceProvider.init.call(this);
this._super(parent);
this.origin = undefined;
this._current_state = null;
this.menu_dm = new concurrency.DropMisordered();
this.action_mutex = new concurrency.Mutex();
this.set('title_part', {"zopenerp": "Flectra"});
},
start: function () {
var self = this;
// we add the o_touch_device css class to allow CSS to target touch
// devices. This is only for styling purpose, if you need javascript
// specific behaviour for touch device, just use the config object
// exported by web.config
this.$el.toggleClass('o_touch_device', config.device.touch);
this.on("change:title_part", this, this._title_changed);
this._title_changed();
return session.is_bound
.then(function () {
self.bind_events();
return $.when(
self.set_action_manager(),
self.set_notification_manager(),
self.set_loading()
);
}).then(function () {
if (session.session_is_valid()) {
return self.show_application();
} else {
// database manager needs the webclient to keep going even
// though it has no valid session
return $.when();
}
}).then(function () {
// Listen to 'scroll' event and propagate it on main bus
self.action_manager.$el.on('scroll', core.bus.trigger.bind(core.bus, 'scroll'));
core.bus.trigger('web_client_ready');
});
},
bind_events: function () {
var self = this;
$('.oe_systray').show();
this.$el.on('mouseenter', '.oe_systray > div:not([data-toggle=tooltip])', function () {
$(this).attr('data-toggle', 'tooltip').tooltip().trigger('mouseenter');
});
this.$el.on('click', '.oe_dropdown_toggle', function (ev) {
ev.preventDefault();
var $toggle = $(this);
var doc_width = $(document).width();
var $menu = $toggle.siblings('.oe_dropdown_menu');
$menu = $menu.size() >= 1 ? $menu : $toggle.find('.oe_dropdown_menu');
var state = $menu.is('.oe_opened');
setTimeout(function () {
// Do not alter propagation
$toggle.add($menu).toggleClass('oe_opened', !state);
if (!state) {
// Move $menu if outside window's edge
var offset = $menu.offset();
var menu_width = $menu.width();
var x = doc_width - offset.left - menu_width - 2;
if (x < 0) {
$menu.offset({ left: offset.left + x }).width(menu_width);
}
}
}, 0);
});
core.bus.on('click', this, function (ev) {
$('.tooltip').remove();
if (!$(ev.target).is('input[type=file]')) {
self.$('.oe_dropdown_menu.oe_opened, .oe_dropdown_toggle.oe_opened').removeClass('oe_opened');
}
});
core.bus.on('connection_lost', this, this.on_connection_lost);
core.bus.on('connection_restored', this, this.on_connection_restored);
// crash manager integration
core.bus.on('rpc_error', crash_manager, crash_manager.rpc_error);
window.onerror = function (message, file, line, col, error) {
// Scripts injected in DOM (eg: google API's js files) won't return a clean error on window.onerror.
// The browser will just give you a 'Script error.' as message and nothing else for security issue.
// To enable onerror to work properly with CORS file, you should:
// 1. add crossorigin="anonymous" to your <script> tag loading the file
// 2. enabling 'Access-Control-Allow-Origin' on the server serving the file.
// Since in some case it wont be possible to to this, this handle should have the possibility to be
// handled by the script manipulating the injected file. For this, you will use window.onOriginError
// If it is not handled, we should display something clearer than the common crash_manager error dialog
// since it won't show anything except "Script error."
// This link will probably explain it better: https://blog.sentry.io/2016/05/17/what-is-script-error.html
if (message === "Script error." && !file && !line && !col && !error) {
if (window.onOriginError) {
window.onOriginError();
delete window.onOriginError;
} else {
crash_manager.show_error({
type: _t("Flectra Client Error"),
message: _t("Unknown CORS error"),
data: {debug: _t("An unknown CORS error occured. The error probably originates from a JavaScript file served from a different origin.")},
});
}
} else {
var traceback = error ? error.stack : '';
crash_manager.show_error({
type: _t("Flectra Client Error"),
message: message,
data: {debug: file + ':' + line + "\n" + _t('Traceback:') + "\n" + traceback},
});
}
};
},
set_action_manager: function () {
this.action_manager = new ActionManager(this, {webclient: this});
return this.action_manager.appendTo(this.$('.o_main_content'));
},
set_notification_manager: function () {
this.notification_manager = new NotificationManager(this);
return this.notification_manager.appendTo(this.$el);
},
set_loading: function () {
this.loading = new Loading(this);
return this.loading.appendTo(this.$el);
},
show_application: function () {
},
clear_uncommitted_changes: function () {
var def = $.Deferred().resolve();
core.bus.trigger('clear_uncommitted_changes', function chain_callbacks(callback) {
def = def.then(callback);
});
return def;
},
destroy_content: function () {
_.each(_.clone(this.getChildren()), function (el) {
el.destroy();
});
this.$el.children().remove();
},
// --------------------------------------------------------------
// Window title handling
// --------------------------------------------------------------
/**
* Sets the first part of the title of the window, dedicated to the current action.
*/
set_title: function (title) {
this.set_title_part("action", title);
},
/**
* Sets an arbitrary part of the title of the window. Title parts are
* identified by strings. Each time a title part is changed, all parts
* are gathered, ordered by alphabetical order and displayed in the title
* of the window separated by ``-``.
*/
set_title_part: function (part, title) {
var tmp = _.clone(this.get("title_part"));
tmp[part] = title;
this.set("title_part", tmp);
},
_title_changed: function () {
var parts = _.sortBy(_.keys(this.get("title_part")), function (x) { return x; });
var tmp = "";
_.each(parts, function (part) {
var str = this.get("title_part")[part];
if (str) {
tmp = tmp ? tmp + " - " + str : str;
}
}, this);
document.title = tmp;
},
// --------------------------------------------------------------
// do_*
// --------------------------------------------------------------
/**
* When do_action is performed on the WebClient, forward it to the main ActionManager
* This allows to widgets that are not inside the ActionManager to perform do_action
*/
do_action: function () {
return this.action_manager.do_action.apply(this, arguments);
},
do_reload: function () {
var self = this;
return session.session_reload().then(function () {
session.load_modules(true).then(
self.menu.proxy('do_reload'));
});
},
do_push_state: function (state) {
if ('title' in state) {
this.set_title(state.title);
delete state.title;
}
var url = '#' + $.param(state);
this._current_state = $.deparam($.param(state), false); // stringify all values
$.bbq.pushState(url);
this.trigger('state_pushed', state);
},
// --------------------------------------------------------------
// Connection notifications
// --------------------------------------------------------------
on_connection_lost: function () {
this.connection_notification = this.notification_manager.notify(
_t('Connection lost'),
_t('Trying to reconnect...'),
true
);
},
on_connection_restored: function () {
if (this.connection_notification) {
this.connection_notification.destroy();
this.notification_manager.notify(
_t('Connection restored'),
_t('You are back online'),
false
);
this.connection_notification = false;
}
},
// Handler to be overwritten
current_action_updated: function () {
},
// --------------------------------------------------------------
// Scrolltop handling
// --------------------------------------------------------------
getScrollTop: function () {
},
//--------------------------------------------------------------
// Misc.
//--------------------------------------------------------------
toggle_fullscreen: function (fullscreen) {
this.$el.toggleClass('o_fullscreen', fullscreen);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Displays a warning in a dialog of with the NotificationManager
*
* @private
* @param {FlectraEvent} e
* @param {string} e.data.message the warning's message
* @param {string} e.data.title the warning's title
* @param {string} [e.data.type] 'dialog' to display in a dialog
* @param {boolean} [e.data.sticky] whether or not the warning should be
* sticky (if displayed with the NotificationManager)
*/
_onDisplayWarning: function (e) {
var data = e.data;
if (data.type === 'dialog') {
new Dialog(this, {
size: 'medium',
title: data.title,
$content: qweb.render("CrashManager.warning", data),
}).open();
} else if (this.notification_manager) {
this.notification_manager.warn(e.data.title, e.data.message, e.data.sticky);
}
},
/**
* Displays a visual effect (for example, a rainbowman0
*
* @private
* @param {FlectraEvent} e
* @param {Object} [e.data] - key-value options to decide rainbowman
* behavior / appearance
*/
_onShowEffect: function (e) {
var data = e.data || {};
var type = data.type || 'rainbow_man';
if (type === 'rainbow_man') {
new RainbowMan(data).appendTo(this.$el);
} else {
throw new Error('Unknown effect type: ' + type);
}
},
});
return AbstractWebClient;
});