summaryrefslogtreecommitdiff
path: root/addons/web/static/src/js
diff options
context:
space:
mode:
authorqsm-odoo <qsm@odoo.com>2018-12-11 16:48:06 +0000
committerqsm-odoo <qsm@odoo.com>2019-02-26 17:02:59 +0000
commit7b960c66bb6cfdcd835f127fff4161e72c324380 (patch)
treed1ad7e4a13f7b5d8705e244a8abeb0761e5bc3ab /addons/web/static/src/js
parentdd2ba3c4f3928adc760a1f0c4f11016723504d76 (diff)
[REF] website, web, *: move part of website into web
* im_livechat, survey, website_forum When website is installed, the JavaScript code has access to utilities to define improved widgets which are automatically attached to existing DOM elements on page load. Those mechanics are more and more needed in portal which does not depend on website (as it is website which depends on portal). This commit moves part of website JS to web, so that it can be used by all 'frontend' apps whose only common dependency is web (portal, survey, auth_signup, ...). This PR also questioned several other similar issues which will be handled later like: - website should maybe not depends on portal but only on http_routing - part of portal should be moved to http_routing and http_routing should be renamed (especially for frontend layouts, etc) so that apps like survey can reuse some common code Part of https://github.com/odoo/odoo/pull/29442 task-1932066
Diffstat (limited to 'addons/web/static/src/js')
-rw-r--r--addons/web/static/src/js/public/public_root.js289
-rw-r--r--addons/web/static/src/js/public/public_root_instance.js18
-rw-r--r--addons/web/static/src/js/public/public_widget.js223
3 files changed, 515 insertions, 15 deletions
diff --git a/addons/web/static/src/js/public/public_root.js b/addons/web/static/src/js/public/public_root.js
new file mode 100644
index 00000000000..10a2d20efc3
--- /dev/null
+++ b/addons/web/static/src/js/public/public_root.js
@@ -0,0 +1,289 @@
+odoo.define('web.public.root', function (require) {
+'use strict';
+
+var ajax = require('web.ajax');
+var ServiceProviderMixin = require('web.ServiceProviderMixin');
+var session = require('web.session');
+var utils = require('web.utils');
+var publicWidget = require('web.public.widget');
+
+var publicRootRegistry = new publicWidget.RootWidgetRegistry();
+
+// Load localizations outside the PublicRoot to not wait for DOM ready (but
+// wait for them in PublicRoot)
+function getLang() {
+ var html = document.documentElement;
+ return (html.getAttribute('lang') || 'en_US').replace('-', '_');
+}
+var lang = utils.get_cookie('frontend_lang') || getLang(); // FIXME the cookie value should maybe be in the ctx?
+var localeDef = ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
+
+/**
+ * Element which is designed to be unique and that will be the top-most element
+ * in the widget hierarchy. So, all other widgets will be indirectly linked to
+ * this Class instance. Its main role will be to retrieve RPC demands from its
+ * children and handle them.
+ */
+var PublicRoot = publicWidget.RootWidget.extend(ServiceProviderMixin, {
+ events: _.extend({}, publicWidget.RootWidget.prototype.events || {}, {
+ 'submit .js_website_submit_form': '_onWebsiteFormSubmit',
+ 'click .js_disable_on_click': '_onDisableOnClick',
+ }),
+ custom_events: _.extend({}, publicWidget.RootWidget.prototype.custom_events || {}, {
+ 'animation_start_demand': '_onAnimationStartDemand',
+ 'animation_stop_demand': '_onAnimationStopDemand',
+ 'context_get': '_onContextGet',
+ 'main_object_request': '_onMainObjectRequest',
+ }),
+
+ /**
+ * @constructor
+ */
+ init: function () {
+ this._super.apply(this, arguments);
+ ServiceProviderMixin.init.call(this);
+ this.animations = [];
+ },
+ /**
+ * @override
+ */
+ willStart: function () {
+ // TODO would be even greater to wait for localeDef only when necessary
+ return $.when(
+ this._super.apply(this, arguments),
+ session.is_bound,
+ localeDef
+ );
+ },
+ /**
+ * @override
+ */
+ start: function () {
+ var defs = [this._super.apply(this, arguments)];
+
+ // Animations
+ defs.push(this._startAnimations());
+
+ // Display image thumbnail
+ this.$(".o_image[data-mimetype^='image']").each(function () {
+ var $img = $(this);
+ if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
+ $img.css('background-image', "url('" + $img.data('src') + "')");
+ }
+ });
+
+ // Auto scroll
+ if (window.location.hash.indexOf("scrollTop=") > -1) {
+ this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1];
+ }
+
+ // Fix for IE:
+ if ($.fn.placeholder) {
+ $('input, textarea').placeholder();
+ }
+
+ return $.when.apply($, defs);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * Automatically adds the global context to RPCs.
+ *
+ * @override
+ */
+ _call_service: function (ev) {
+ if (ev.data.service === 'ajax' && ev.data.method === 'rpc') {
+ var route = ev.data.args[0];
+ if (_.str.startsWith(route, '/web/dataset/call_kw/')) {
+ var params = ev.data.args[1];
+ var options = ev.data.args[2];
+ params.kwargs.context = _.extend({}, this._getContext(), params.kwargs.context || {});
+ if (options) {
+ params.kwargs.context = _.omit(params.kwargs.context, options.noContextKeys);
+ ev.data.args[2] = _.omit(options, 'noContextKeys');
+ }
+ params.kwargs.context = JSON.parse(JSON.stringify(params.kwargs.context));
+ }
+ }
+ return ServiceProviderMixin._call_service.apply(this, arguments);
+ },
+ /**
+ * Retrieves the global context of the public environment. This is the
+ * context which is automatically added to each RPC.
+ *
+ * @private
+ * @param {Object} [context]
+ * @returns {Object}
+ */
+ _getContext: function (context) {
+ return _.extend({
+ 'lang': getLang(),
+ }, context || {});
+ },
+ /**
+ * Retrieves the global context of the public environment (as
+ * @see _getContext) but with extra informations that would be useless to
+ * send with each RPC.
+ *
+ * @private
+ * @param {Object} [context]
+ * @returns {Object}
+ */
+ _getExtraContext: function (context) {
+ return this._getContext(context);
+ },
+ /**
+ * As the root instance is designed to be unique, the associated
+ * registry has been instantiated outside of the class and is simply
+ * returned here.
+ *
+ * @private
+ * @override
+ */
+ _getRegistry: function () {
+ return publicRootRegistry;
+ },
+ /**
+ * Creates an PublicWidget instance for each DOM element which matches the
+ * `selector` key of one of the registered widgets
+ * (@see PublicWidget.selector).
+ *
+ * @private
+ * @param {jQuery} [$from]
+ * only initialize the animations whose `selector` matches the
+ * element or one of its descendant (default to the wrapwrap element)
+ * @param {Object} [options]
+ * @returns {Deferred}
+ */
+ _startAnimations: function ($from, options) {
+ var self = this;
+
+ if ($from === undefined) {
+ $from = this.$('#wrapwrap');
+ }
+ if (options === undefined) {
+ options = {};
+ }
+
+ this._stopAnimations($from);
+
+ var defs = _.map(publicWidget.registry, function (WebsiteWidget) {
+ var selector = WebsiteWidget.prototype.selector || '';
+ var $target = $from.find(selector).addBack(selector);
+
+ var defs = _.map($target, function (el) {
+ var animation = new WebsiteWidget(self, options);
+ self.animations.push(animation);
+ return animation.attachTo($(el));
+ });
+ return $.when.apply($, defs);
+ });
+ return $.when.apply($, defs);
+ },
+ /**
+ * Destroys all registered widget instances. Website would need this before
+ * saving while in edition mode for example.
+ *
+ * @private
+ * @param {jQuery} [$from]
+ * only stop the animations linked to the given element(s) or one of
+ * its descendants
+ */
+ _stopAnimations: function ($from) {
+ var removedAnimations = _.map(this.animations, function (animation) {
+ if (!$from
+ || $from.filter(animation.el).length
+ || $from.find(animation.el).length) {
+ animation.destroy();
+ return animation;
+ }
+ return null;
+ });
+ this.animations = _.difference(this.animations, removedAnimations);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when the root is notified that the animations have to be
+ * (re)started.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onAnimationStartDemand: function (ev) {
+ this._startAnimations(ev.data.$target, ev.data.options)
+ .done(ev.data.onSuccess)
+ .fail(ev.data.onFailure);
+ },
+ /**
+ * Called when the root is notified that the animations have to be
+ * stopped.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onAnimationStopDemand: function (ev) {
+ this._stopAnimations(ev.data.$target);
+ },
+ /**
+ * Called when someone asked for the global public context.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onContextGet: function (ev) {
+ if (ev.data.extra) {
+ ev.data.callback(this._getExtraContext(ev.data.context));
+ } else {
+ ev.data.callback(this._getContext(ev.data.context));
+ }
+ },
+ /**
+ * Checks information about the page main object.
+ *
+ * @private
+ * @param {OdooEvent} ev
+ */
+ _onMainObjectRequest: function (ev) {
+ var repr = $('html').data('main-object');
+ var m = repr.match(/(.+)\((\d+),(.*)\)/);
+ ev.data.callback({
+ model: m[1],
+ id: m[2] | 0,
+ });
+ },
+ /**
+ * @todo review
+ * @private
+ */
+ _onWebsiteFormSubmit: function (ev) {
+ var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit');
+ _.each($buttons, function (btn) {
+ var $btn = $(btn);
+ $btn.attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text());
+ $btn.button('loading');
+ });
+ },
+ /**
+ * Called when the root is notified that the button should be
+ * disabled after the first click.
+ *
+ * @private
+ * @param {Event} ev
+ */
+ _onDisableOnClick: function (ev) {
+ $(ev.currentTarget).addClass('disabled');
+ },
+});
+
+return {
+ PublicRoot: PublicRoot,
+ publicRootRegistry: publicRootRegistry,
+};
+});
diff --git a/addons/web/static/src/js/public/public_root_instance.js b/addons/web/static/src/js/public/public_root_instance.js
index ce0cfa61bc6..8202e011cc3 100644
--- a/addons/web/static/src/js/public/public_root_instance.js
+++ b/addons/web/static/src/js/public/public_root_instance.js
@@ -1,23 +1,17 @@
odoo.define('root.widget', function (require) {
-"use strict";
+'use strict';
-var ServiceProviderMixin = require('web.ServiceProviderMixin');
-var Widget = require('web.Widget');
+require('web.dom_ready');
+var rootData = require('web.public.root');
/**
- * This is the root widget in the frontend bundle when website is not installed.
- *
* This widget is important, because the tour manager needs a root widget in
* order to work. The root widget must be a service provider with the ajax
* service, so that the tour manager can let the server know when tours have
* been consumed.
*/
-var PublicRootWidget = Widget.extend(ServiceProviderMixin, {
- init: function () {
- this._super.apply(this, arguments);
- ServiceProviderMixin.init.call(this);
- }
+var publicRoot = new rootData.PublicRoot(null);
+return publicRoot.attachTo(document.body).then(function () {
+ return publicRoot;
});
-
-return new PublicRootWidget();
});
diff --git a/addons/web/static/src/js/public/public_widget.js b/addons/web/static/src/js/public/public_widget.js
index 462dcd9bae9..2f720c019f1 100644
--- a/addons/web/static/src/js/public/public_widget.js
+++ b/addons/web/static/src/js/public/public_widget.js
@@ -1,6 +1,10 @@
-odoo.define('web_editor.root_widget', function (require) {
+odoo.define('web.public.widget', function (require) {
'use strict';
+/**
+ * Provides a way to start JS code for snippets' initialization and animations.
+ */
+
var Class = require('web.Class');
var dom = require('web.dom');
var mixins = require('web.mixins');
@@ -12,11 +16,13 @@ var Widget = require('web.Widget');
* to internal DOM elements once it is started. The widgets to instantiate are
* known thanks to a linked registry which contains info about the widget
* classes and jQuery selectors to use to find the elements to attach them to.
+ *
+ * @todo Merge with 'PublicWidget' ?
*/
var RootWidget = Widget.extend({
custom_events: _.extend({}, Widget.prototype.custom_events || {}, {
- registry_update: '_onRegistryUpdate',
- get_session: '_onGetSession',
+ 'registry_update': '_onRegistryUpdate',
+ 'get_session': '_onGetSession',
}),
/**
* @constructor
@@ -160,8 +166,219 @@ var RootWidgetRegistry = Class.extend(mixins.EventDispatcherMixin, {
},
});
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+/**
+ * Provides a way for executing code once a website DOM element is loaded in the
+ * dom.
+ */
+var PublicWidget = Widget.extend({
+ /**
+ * The selector attribute, if defined, allows to automatically create an
+ * instance of this animation on page load for each DOM element which
+ * matches this selector. The `Animation.$target` element will then be that
+ * particular DOM element. This should be the main way of instantiating
+ * `Animation` elements.
+ *
+ * @todo do not make this part of the Widget but rather an info to give when
+ * registering the widget.
+ */
+ selector: false,
+ /**
+ * Extension of @see Widget.events
+ *
+ * A description of the event handlers to bind/delegate once the widget
+ * has been rendered::
+ *
+ * 'click .hello .world': 'async _onHelloWorldClick',
+ * _^_ _^_ _^_ _^_
+ * | | | |
+ * | (Optional) jQuery | Handler method name
+ * | delegate selector |
+ * | |_ (Optional) space separated options
+ * | * async: use the automatic system
+ * |_ Event name with making handlers promise-ready (see
+ * potential jQuery makeButtonHandler, makeAsyncHandler)
+ * namespaces
+ *
+ * Note: the values may be replaced by a function declaration. This is
+ * however a deprecated behavior.
+ *
+ * @type {Object}
+ */
+ events: {},
+ /**
+ * Acts as @see Widget.events except that the events are only binded if the
+ * Animation instance is instanciated in edit mode.
+ */
+ edit_events: {},
+ /**
+ * Acts as @see Widget.events except that the events are only binded if the
+ * Animation instance is instanciated in readonly mode.
+ */
+ read_events: {},
+
+ /**
+ * Initializes the events that will need to be binded according to the
+ * given mode.
+ *
+ * @constructor
+ * @param {Object} parent
+ * @param {Object} options
+ * @param {boolean} options.editableMode - true if the page is in edition mode
+ */
+ init: function (parent, options) {
+ this._super.apply(this, arguments);
+
+ // Note that the notion of editable mode is kept here for now even
+ // though nothing can be edited here. This allows to have a readable
+ // code for frontend widgets and mainly to prevent having loading
+ // problems an "include" could have caused.
+ this.options = options || {};
+ this.editableMode = this.options.editableMode || false;
+
+ if (this.editableMode) {
+ this.events = _.extend({}, this.events || {}, this.edit_events || {});
+ } else {
+ this.events = _.extend({}, this.events || {}, this.read_events || {});
+ }
+ },
+ /**
+ * Destroys the animation and basically restores the target to the state it
+ * was before the start method was called (unlike standard widget, the
+ * associated $el DOM is not removed).
+ *
+ * Also stops animation effects and destroys them if any.
+ */
+ destroy: function () {
+ if (this.selector) {
+ var $oldel = this.$el;
+ // The difference with the default behavior is that we unset the
+ // associated element first so that:
+ // 1) its events are unbinded
+ // 2) it is not removed from the DOM
+ this.setElement(null);
+ }
+
+ this._super.apply(this, arguments);
+
+ if (this.selector) {
+ // Reassign the variables afterwards to allow extensions to use them
+ // after calling the _super method
+ this.$el = $oldel;
+ this.el = $oldel[0];
+ this.$target = this.$el;
+ this.target = this.el;
+ }
+ },
+ /**
+ * @override
+ */
+ setElement: function () {
+ this._super.apply(this, arguments);
+ if (this.selector) {
+ this.$target = this.$el;
+ this.target = this.el;
+ }
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @see this.events
+ * @override
+ */
+ _delegateEvents: function () {
+ var self = this;
+ var originalEvents = this.events;
+
+ var events = {};
+ _.each(this.events, function (method, event) {
+ // If the method is a function, use the default Widget system
+ if (typeof method !== 'string') {
+ events[event] = method;
+ return;
+ }
+ // If the method is only a function name without options, use the
+ // default Widget system
+ var methodOptions = method.split(' ');
+ if (methodOptions.length <= 1) {
+ events[event] = method;
+ return;
+ }
+ // If the method has no meaningful options, use the default Widget
+ // system
+ var isAsync = _.contains(methodOptions, 'async');
+ if (!isAsync) {
+ events[event] = method;
+ return;
+ }
+
+ method = self.proxy(methodOptions[methodOptions.length - 1]);
+ if (_.str.startsWith(event, 'click')) {
+ // Protect click handler to be called multiple times by
+ // mistake by the user and add a visual disabling effect
+ // for buttons.
+ method = dom.makeButtonHandler(method);
+ } else {
+ // Protect all handlers to be recalled while the previous
+ // async handler call is not finished.
+ method = dom.makeAsyncHandler(method);
+ }
+ events[event] = method;
+ });
+
+ this.events = events;
+ this._super.apply(this, arguments);
+ this.events = originalEvents;
+ },
+ /**
+ * @private
+ * @param {boolean} [extra=false]
+ * @param {Object} [extraContext]
+ * @returns {Object}
+ */
+ _getContext: function (extra, extraContext) {
+ var context;
+ this.trigger_up('context_get', {
+ extra: extra || false,
+ context: extraContext,
+ callback: function (ctx) {
+ context = ctx;
+ },
+ });
+ return context;
+ },
+});
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+/**
+ * The registry object contains the list of available animations.
+ */
+var registry = {};
+
+/**
+ * This is a fix for apple device (<= IPhone 4, IPad 2)
+ * Standard bootstrap requires data-toggle='collapse' element to be <a/> tags.
+ * Unfortunatly one snippet uses a <div/> tag instead. The fix forces an empty
+ * click handler on these div, which allows standard bootstrap to work.
+ *
+ * This should be removed in a future odoo snippets refactoring.
+ */
+registry._fixAppleCollapse = PublicWidget.extend({
+ selector: 'div[data-toggle="collapse"]',
+ events: {
+ 'click': function () {},
+ },
+});
+
return {
RootWidget: RootWidget,
RootWidgetRegistry: RootWidgetRegistry,
+ Widget: PublicWidget,
+ registry: registry,
};
});