From 4f925af06fd3cebfcfb56eb947ab68c8a2ca4490 Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Wed, 7 Jun 2017 17:45:15 +0200 Subject: [MOV] web_editor, website: reorganize files --- addons/website/static/src/js/backend/button.js | 51 ++ addons/website/static/src/js/backend/dashboard.js | 603 +++++++++++++++++++++ addons/website/static/src/js/content/share.js | 87 +++ .../static/src/js/content/snippets.animation.js | 593 ++++++++++++++++++++ addons/website/static/src/js/content/zoomodoo.js | 292 ++++++++++ addons/website/static/src/js/editor/editor.js | 151 ++++++ .../static/src/js/editor/snippets.options.js | 538 ++++++++++++++++++ addons/website/static/src/js/menu/content.js | 361 ++++++++++++ addons/website/static/src/js/menu/mobile_view.js | 49 ++ addons/website/static/src/js/menu/planner.js | 41 ++ addons/website/static/src/js/menu/seo.js | 582 ++++++++++++++++++++ addons/website/static/src/js/menu/translate.js | 104 ++++ addons/website/static/src/js/tours/banner.js | 81 +++ addons/website/static/src/js/tours/rte.js | 159 ++++++ .../website/static/src/js/web_planner_website.js | 41 -- addons/website/static/src/js/website.ace.js | 96 ---- addons/website/static/src/js/website.backend.js | 51 -- .../static/src/js/website.backendDashboard.js | 603 --------------------- .../website/static/src/js/website.contentMenu.js | 361 ------------ addons/website/static/src/js/website.editor.js | 151 ------ addons/website/static/src/js/website.mobile.js | 49 -- addons/website/static/src/js/website.seo.js | 582 -------------------- addons/website/static/src/js/website.share.js | 87 --- .../static/src/js/website.snippets.animation.js | 593 -------------------- .../static/src/js/website.snippets.editor.js | 538 ------------------ addons/website/static/src/js/website.theme.js | 311 ----------- .../website/static/src/js/website.tour.banner.js | 81 --- addons/website/static/src/js/website.tour.rte.js | 159 ------ addons/website/static/src/js/website.translator.js | 104 ---- addons/website/static/src/js/website.zoomodoo.js | 292 ---------- addons/website/static/src/js/widgets/ace.js | 96 ++++ addons/website/static/src/js/widgets/theme.js | 311 +++++++++++ 32 files changed, 4099 insertions(+), 4099 deletions(-) create mode 100644 addons/website/static/src/js/backend/button.js create mode 100644 addons/website/static/src/js/backend/dashboard.js create mode 100644 addons/website/static/src/js/content/share.js create mode 100644 addons/website/static/src/js/content/snippets.animation.js create mode 100644 addons/website/static/src/js/content/zoomodoo.js create mode 100644 addons/website/static/src/js/editor/editor.js create mode 100644 addons/website/static/src/js/editor/snippets.options.js create mode 100644 addons/website/static/src/js/menu/content.js create mode 100644 addons/website/static/src/js/menu/mobile_view.js create mode 100644 addons/website/static/src/js/menu/planner.js create mode 100644 addons/website/static/src/js/menu/seo.js create mode 100644 addons/website/static/src/js/menu/translate.js create mode 100644 addons/website/static/src/js/tours/banner.js create mode 100644 addons/website/static/src/js/tours/rte.js delete mode 100644 addons/website/static/src/js/web_planner_website.js delete mode 100644 addons/website/static/src/js/website.ace.js delete mode 100644 addons/website/static/src/js/website.backend.js delete mode 100644 addons/website/static/src/js/website.backendDashboard.js delete mode 100644 addons/website/static/src/js/website.contentMenu.js delete mode 100644 addons/website/static/src/js/website.editor.js delete mode 100644 addons/website/static/src/js/website.mobile.js delete mode 100644 addons/website/static/src/js/website.seo.js delete mode 100644 addons/website/static/src/js/website.share.js delete mode 100644 addons/website/static/src/js/website.snippets.animation.js delete mode 100644 addons/website/static/src/js/website.snippets.editor.js delete mode 100644 addons/website/static/src/js/website.theme.js delete mode 100644 addons/website/static/src/js/website.tour.banner.js delete mode 100644 addons/website/static/src/js/website.tour.rte.js delete mode 100644 addons/website/static/src/js/website.translator.js delete mode 100644 addons/website/static/src/js/website.zoomodoo.js create mode 100644 addons/website/static/src/js/widgets/ace.js create mode 100644 addons/website/static/src/js/widgets/theme.js (limited to 'addons/website/static/src/js') diff --git a/addons/website/static/src/js/backend/button.js b/addons/website/static/src/js/backend/button.js new file mode 100644 index 00000000000..5033d9a690c --- /dev/null +++ b/addons/website/static/src/js/backend/button.js @@ -0,0 +1,51 @@ +odoo.define('website.backend', function (require) { +"use strict"; + +var AbstractField = require('web.AbstractField'); +var core = require('web.core'); +var field_registry = require('web.field_registry'); + +var _t = core._t; + +var WidgetWebsiteButton = AbstractField.extend({ + template: 'WidgetWebsiteButton', + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + isSet: function () { + return true; + }, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + * @private + */ + _render: function () { + this._super.apply(this, arguments); + + var $value = this.$('.o_value'); + + if (this.value === true) { + $value.html(_t('Published')) + .removeClass('text-danger') + .addClass('text-success'); + } else { + $value.html(_t('Unpublished')) + .removeClass('text-success') + .addClass('text-danger'); + } + }, +}); + +field_registry.add('website_button', WidgetWebsiteButton); + +}); diff --git a/addons/website/static/src/js/backend/dashboard.js b/addons/website/static/src/js/backend/dashboard.js new file mode 100644 index 00000000000..16b1281f222 --- /dev/null +++ b/addons/website/static/src/js/backend/dashboard.js @@ -0,0 +1,603 @@ +odoo.define('website.backendDashboard', function (require) { +"use strict"; + + +var ajax = require('web.ajax'); +var ControlPanelMixin = require('web.ControlPanelMixin'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var field_utils = require('web.field_utils'); +var session = require('web.session'); +var web_client = require('web.web_client'); +var Widget = require('web.Widget'); + +var local_storage = require('web.local_storage'); + +var _t = core._t; +var QWeb = core.qweb; + +var Dashboard = Widget.extend(ControlPanelMixin, { + template: "website.WebsiteDashboardMain", + events: { + 'click .js_link_analytics_settings': 'on_link_analytics_settings', + 'click .o_dashboard_action': 'on_dashboard_action', + 'click .o_dashboard_action_form': 'on_dashboard_action_form', + 'click .o_dashboard_hide_panel': 'on_dashboard_hide_panel', + }, + + init: function(parent, context) { + this._super(parent, context); + + this.date_range = 'week'; // possible values : 'week', 'month', year' + this.date_from = moment().subtract(1, 'week'); + this.date_to = moment(); + this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); + + this.dashboards_templates = ['website.dashboard_visits']; + this.graphs = []; + }, + + willStart: function() { + var self = this; + return this._super().then(function() { + return $.when( + self.fetch_data() + ); + }); + }, + + start: function() { + var self = this; + return this._super().then(function() { + self.update_cp(); + self.render_dashboards(); + self.render_graphs(); + self.$el.parent().addClass('oe_background_grey'); + }); + }, + + fetch_data: function() { + var self = this; + return ajax.jsonRpc('/website/fetch_dashboard_data', 'call', { + 'date_from': this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(), + 'date_to': this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(), + }).done(function(result) { + self.data = result; + self.dashboards_data = result.dashboards; + self.currency_id = result.currency_id; + self.groups = result.groups; + }); + }, + + on_link_analytics_settings: function(ev) { + ev.preventDefault(); + + var self = this; + var dialog = new Dialog(this, { + size: 'medium', + title: _t('Google Analytics'), + $content: QWeb.render('website.ga_dialog_content', { + ga_key: this.dashboards_data.visits.ga_client_id, + ga_analytics_key: this.dashboards_data.visits.ga_analytics_key, + }), + buttons: [ + { + text: _t("Save"), + classes: 'btn-primary', + close: true, + click: function() { + var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val(); + var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val(); + self.on_save_ga_client_id(ga_client_id, ga_analytics_key); + }, + }, + { + text: _t("Cancel"), + close: true, + }, + ], + }).open(); + }, + + on_save_ga_client_id: function(ga_client_id, ga_analytics_key) { + var self = this; + return this._rpc({ + route: '/website/dashboard/set_ga_data', + params: { + 'ga_client_id': ga_client_id, + 'ga_analytics_key': ga_analytics_key, + }, + }).then(function (result) { + if (result.error) { + self.do_warn(result.error.title, result.error.message); + return; + } + self.on_date_range_button('week'); + }); + }, + + render_dashboards: function() { + var self = this; + _.each(this.dashboards_templates, function(template) { + self.$('.o_website_dashboard_content').append(QWeb.render(template, {widget: self})); + }); + }, + + render_graph: function(div_to_display, chart_values) { + this.$(div_to_display).empty(); + + var self = this; + nv.addGraph(function() { + var chart = nv.models.lineChart() + .x(function(d) { return self.getDate(d); }) + .y(function(d) { return self.getValue(d); }) + .forceY([0]); + chart + .useInteractiveGuideline(true) + .showLegend(false) + .showYAxis(true) + .showXAxis(true); + + var tick_values = self.getPrunedTickValues(chart_values[0].values, 5); + + chart.xAxis + .tickFormat(function(d) { return d3.time.format("%m/%d/%y")(new Date(d)); }) + .tickValues(_.map(tick_values, function(d) { return self.getDate(d); })) + .rotateLabels(-45); + + chart.yAxis + .tickFormat(d3.format('.02f')); + + var svg = d3.select(div_to_display) + .append("svg"); + + svg + .attr("height", '24em') + .datum(chart_values) + .call(chart); + + nv.utils.windowResize(chart.update); + return chart; + }); + }, + + render_graphs: function() { + var self = this; + _.each(this.graphs, function(e) { + if (self.groups[e.group]) { + self.render_graph('#o_graph_' + e.name, self.dashboards_data[e.name].graph); + } + }); + this.render_graph_analytics(this.dashboards_data.visits.ga_client_id); + }, + + render_graph_analytics: function(client_id) { + if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) { + return; + } + + this.load_analytics_api(); + + var $analytics_components = this.$('.js_analytics_components'); + this.addLoader($analytics_components); + + var self = this; + gapi.analytics.ready(function() { + + $analytics_components.empty(); + // 1. Authorize component + var $analytics_auth = $('
').addClass('col-md-12'); + gapi.analytics.auth.authorize({ + container: $analytics_auth[0], + clientid: client_id + }); + $analytics_auth.appendTo($analytics_components); + + self.handle_analytics_auth($analytics_components); + gapi.analytics.auth.on('signIn', function() { + self.handle_analytics_auth($analytics_components); + }); + + }); + }, + + on_date_range_button: function(date_range) { + if (date_range === 'week') { + this.date_range = 'week'; + this.date_from = moment().subtract(1, 'weeks'); + } else if (date_range === 'month') { + this.date_range = 'month'; + this.date_from = moment().subtract(1, 'months'); + } else if (date_range === 'year') { + this.date_range = 'year'; + this.date_from = moment().subtract(1, 'years'); + } else { + console.log('Unknown date range. Choose between [week, month, year]'); + return; + } + + var self = this; + $.when(this.fetch_data()).then(function() { + self.$('.o_website_dashboard_content').empty(); + self.render_dashboards(); + self.render_graphs(); + }); + + }, + + on_reverse_breadcrumb: function() { + web_client.do_push_state({}); + this.update_cp(); + }, + + on_dashboard_action: function (ev) { + ev.preventDefault(); + var $action = $(ev.currentTarget); + var additional_context = {}; + if (this.date_range === 'week') { + additional_context = {'search_default_week': true} + } else if (this.date_range === 'month') { + additional_context = {'search_default_month': true} + } else if (this.date_range === 'year') { + additional_context = {'search_default_year': true} + } + this.do_action($action.attr('name'), { + additional_context: additional_context, + on_reverse_breadcrumb: this.on_reverse_breadcrumb + }); + }, + + on_dashboard_action_form: function (ev) { + ev.preventDefault(); + var $action = $(ev.currentTarget); + this.do_action({ + name: $action.attr('name'), + res_model: $action.data('res_model'), + res_id: $action.data('res_id'), + views: [[false, 'form']], + type: 'ir.actions.act_window', + }, { + on_reverse_breadcrumb: this.on_reverse_breadcrumb + }); + }, + + on_dashboard_hide_panel: function (ev) { + ev.preventDefault(); + ev.stopPropagation(); + var $action = $(ev.currentTarget); + // update hidden module list + this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); + this.hidden_apps.push(JSON.parse($action.data('module_id'))); + local_storage.setItem('website_dashboard_hidden_app_ids', JSON.stringify(this.hidden_apps)); + // remove box + $action.closest(".o_box_item").remove(); + }, + + update_cp: function() { + var self = this; + if (!this.$searchview) { + this.$searchview = $(QWeb.render("website.DateRangeButtons", { + widget: this, + })); + this.$searchview.click('button.js_date_range', function(ev) { + self.on_date_range_button($(ev.target).data('date')); + $(this).find('button.js_date_range.active').removeClass('active'); + $(ev.target).addClass('active'); + }); + } + this.update_control_panel({ + cp_content: { + $searchview: this.$searchview, + }, + breadcrumbs: this.getParent().get_breadcrumbs(), + }); + }, + + // Loads Analytics API + load_analytics_api: function() { + var self = this; + if (!("gapi" in window)) { + (function(w,d,s,g,js,fjs){ + g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}}; + js=d.createElement(s);fjs=d.getElementsByTagName(s)[0]; + js.src='https://apis.google.com/js/platform.js'; + fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');}; + }(window,document,'script')); + gapi.analytics.ready(function() { + self.analytics_create_components(); + }); + } + }, + + handle_analytics_auth: function($analytics_components) { + $analytics_components.find('.js_unauthorized_message').remove(); + + // Check if the user is authenticated and has the right to make API calls + if (!gapi.analytics.auth.getAuthResponse()) { + this.display_unauthorized_message($analytics_components, 'not_connected'); + } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) { + this.display_unauthorized_message($analytics_components, 'no_right'); + } else { + this.make_analytics_calls($analytics_components); + } + }, + + display_unauthorized_message: function($analytics_components, reason) { + $analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason}))); + }, + + make_analytics_calls: function($analytics_components) { + // 2. ActiveUsers component + var $analytics_users = $('
'); + var activeUsers = new gapi.analytics.ext.ActiveUsers({ + container: $analytics_users[0], + pollingInterval: 10, + }); + $analytics_users.appendTo($analytics_components); + + // 3. View Selector + var $analytics_view_selector = $('
').addClass('col-md-12 o_properties_selection'); + var viewSelector = new gapi.analytics.ViewSelector({ + container: $analytics_view_selector[0], + }); + viewSelector.execute(); + $analytics_view_selector.appendTo($analytics_components); + + // 4. Chart graph + var start_date = '7daysAgo'; + if (this.date_range === 'month') { + start_date = '30daysAgo'; + } else if (this.date_range === 'year') { + start_date = '365daysAgo'; + } + var $analytics_chart_2 = $('
').addClass('col-md-6 col-xs-12'); + var breakdownChart = new gapi.analytics.googleCharts.DataChart({ + query: { + 'dimensions': 'ga:date', + 'metrics': 'ga:sessions', + 'start-date': start_date, + 'end-date': 'yesterday' + }, + chart: { + type: 'LINE', + container: $analytics_chart_2[0], + options: { + title: 'All', + width: '100%' + } + } + }); + $analytics_chart_2.appendTo($analytics_components); + + // 5. Chart table + var $analytics_chart_1 = $('
').addClass('col-md-6 col-xs-12'); + var mainChart = new gapi.analytics.googleCharts.DataChart({ + query: { + 'dimensions': 'ga:medium', + 'metrics': 'ga:sessions', + 'sort': '-ga:sessions', + 'max-results': '6' + }, + chart: { + type: 'TABLE', + container: $analytics_chart_1[0], + options: { + width: '100%' + } + } + }); + $analytics_chart_1.appendTo($analytics_components); + + // Events handling & animations + + var table_row_listener; + + viewSelector.on('change', function(ids) { + var options = {query: {ids: ids}}; + activeUsers.set({ids: ids}).execute(); + mainChart.set(options).execute(); + breakdownChart.set(options).execute(); + + if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); } + }); + + mainChart.on('success', function(response) { + var chart = response.chart; + var dataTable = response.dataTable; + + table_row_listener = google.visualization.events.addListener(chart, 'select', function() { + var options; + if (chart.getSelection().length) { + var row = chart.getSelection()[0].row; + var medium = dataTable.getValue(row, 0); + options = { + query: { + filters: 'ga:medium==' + medium, + }, + chart: { + options: { + title: medium, + } + } + }; + } else { + options = { + chart: { + options: { + title: 'All', + } + } + }; + delete breakdownChart.get().query.filters; + } + breakdownChart.set(options).execute(); + }); + }); + + // Add CSS animation to visually show the when users come and go. + activeUsers.once('success', function() { + var element = this.container.firstChild; + var timeout; + + this.on('change', function(data) { + element = this.container.firstChild; + var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing'; + element.className += (' ' + animationClass); + + clearTimeout(timeout); + timeout = setTimeout(function() { + element.className = element.className.replace(/ is-(increasing|decreasing)/g, ''); + }, 3000); + }); + }); + }, + + /* + * Credits to https://github.com/googleanalytics/ga-dev-tools + * This is the Active Users component that polls + * the number of active users on Analytics each 5 secs + */ + analytics_create_components: function() { + + gapi.analytics.createComponent('ActiveUsers', { + + initialize: function() { + this.activeUsers = 0; + gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); + }, + + execute: function() { + // Stop any polling currently going on. + if (this.polling_) { + this.stop(); + } + + this.render_(); + + // Wait until the user is authorized. + if (gapi.analytics.auth.isAuthorized()) { + this.pollActiveUsers_(); + } else { + gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this)); + } + }, + + stop: function() { + clearTimeout(this.timeout_); + this.polling_ = false; + this.emit('stop', {activeUsers: this.activeUsers}); + }, + + render_: function() { + var opts = this.get(); + + // Render the component inside the container. + this.container = typeof opts.container === 'string' ? + document.getElementById(opts.container) : opts.container; + + this.container.innerHTML = opts.template || this.template; + this.container.querySelector('b').innerHTML = this.activeUsers; + }, + + pollActiveUsers_: function() { + var options = this.get(); + var pollingInterval = (options.pollingInterval || 5) * 1000; + + if (isNaN(pollingInterval) || pollingInterval < 5000) { + throw new Error('Frequency must be 5 seconds or more.'); + } + + this.polling_ = true; + gapi.client.analytics.data.realtime + .get({ids:options.ids, metrics:'rt:activeUsers'}) + .then(function(response) { + var result = response.result; + var newValue = result.totalResults ? +result.rows[0][0] : 0; + var oldValue = this.activeUsers; + + this.emit('success', {activeUsers: this.activeUsers}); + + if (newValue !== oldValue) { + this.activeUsers = newValue; + this.onChange_(newValue - oldValue); + } + + if (this.polling_) { + this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval); + } + }.bind(this)); + }, + + onChange_: function(delta) { + var valueContainer = this.container.querySelector('b'); + if (valueContainer) { valueContainer.innerHTML = this.activeUsers; } + + this.emit('change', {activeUsers: this.activeUsers, delta: delta}); + if (delta > 0) { + this.emit('increase', {activeUsers: this.activeUsers, delta: delta}); + } else { + this.emit('decrease', {activeUsers: this.activeUsers, delta: delta}); + } + }, + + handleSignOut_: function() { + this.stop(); + gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this)); + }, + + handleSignIn_: function() { + this.pollActiveUsers_(); + gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); + }, + + template: + '
' + + 'Active Users: ' + + '
' + + }); + }, + + // Utility functions + addLoader: function(selector) { + var loader = ''; + selector.html("
" + loader + "
"); + }, + getDate: function(d) { return new Date(d[0]); }, + getValue: function(d) { return d[1]; }, + getPrunedTickValues: function(ticks, nb_desired_ticks) { + var nb_values = ticks.length; + var keep_one_of = Math.max(1, Math.floor(nb_values / nb_desired_ticks)); + + return _.filter(ticks, function(d, i) { + return i % keep_one_of === 0; + }); + }, + format_number: function(value, type, digits, symbol) { + if (type === 'currency') { + return this.render_monetary_field(value, this.currency_id); + } else { + return field_utils.format[type](value || 0, {digits: digits}) + ' ' + symbol; + } + }, + render_monetary_field: function(value, currency_id) { + var currency = session.get_currency(currency_id); + var formatted_value = field_utils.format.float(value || 0, {digits: currency && currency.digits}); + if (currency) { + if (currency.position === "after") { + formatted_value += currency.symbol; + } else { + formatted_value = currency.symbol + formatted_value; + } + } + return formatted_value; + }, + +}); + +core.action_registry.add('backend_dashboard', Dashboard); + + +return Dashboard; + +}); diff --git a/addons/website/static/src/js/content/share.js b/addons/website/static/src/js/content/share.js new file mode 100644 index 00000000000..4de4dd65117 --- /dev/null +++ b/addons/website/static/src/js/content/share.js @@ -0,0 +1,87 @@ +odoo.define('website.share', function (require) { +"use strict"; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); + +var _t = core._t; +var qweb = core.qweb; + +ajax.loadXML('/website/static/src/xml/website.share.xml', qweb); + +var SocialShare = Widget.extend({ + template: 'website.social_hover', + /** + :param element: element to bind popover + :param social_list: list of social media strings available from + ['facebook','twitter', 'linkedin', 'google-plus'] + :param hashtags: string of hashtags for twitter + */ + init: function (parent) { + this._super.apply(this, arguments); + this.element = parent; + if (parent.data('social')) { + this.social_list = (parent.data('social')).split(); + } else { + this.social_list = ['facebook','twitter', 'linkedin', 'google-plus']; + } + this.hashtags = parent.data('hashtags') || ''; + this.renderElement(); + this.bind_events(); + }, + bind_events: function () { + $('.oe_social_facebook').click($.proxy(this.renderSocial, this, 'facebook')); + $('.oe_social_twitter').click($.proxy(this.renderSocial, this, 'twitter')); + $('.oe_social_linkedin').click($.proxy(this.renderSocial, this, 'linkedin')); + $('.oe_social_google-plus').click($.proxy(this.renderSocial, this, 'google-plus')); + }, + renderElement: function () { + this.$el.append( + qweb.render('website.social_hover', {medias: this.social_list})); + //we need to re-render the element on each hover; popover has the nasty habit of not hiding but completely removing its + // code from the page + //so the binding is lost if we simply trigger on hover. + this.element.popover({ + 'content': this.$el.html(), + 'placement': 'bottom', + 'container': this.element, + 'html': true, + 'trigger': 'manual', + 'animation': false, + }).popover("show").on("mouseleave", function () { + var self = this; + setTimeout(function () { + if (! $(".popover:hover").length) { + $(self).popover("destroy"); + } + }, 200); + }); + }, + renderSocial: function (social) { + var url = document.URL.split(/[?#]/)[0]; // get current url without query string + var title = document.title.split(" | ")[0]; // get the page title without the company name + var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag) + var social_network = { + 'facebook':'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url), + 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url), + 'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title), + 'google-plus': 'https://plus.google.com/share?url=' + encodeURIComponent(url) + }; + if (! _.contains(_.keys(social_network), social)) return; + var window_height = 500, window_width = 500; + window.open(social_network[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + window_height + ',width=' + window_width); + }, +}); + +// Initialize all social_share links when ready +base.ready().done(function () { + $('.oe_social_share').mouseenter(function () { + new SocialShare($(this)); + }); +}); + +return SocialShare; + +}); diff --git a/addons/website/static/src/js/content/snippets.animation.js b/addons/website/static/src/js/content/snippets.animation.js new file mode 100644 index 00000000000..234a43463dd --- /dev/null +++ b/addons/website/static/src/js/content/snippets.animation.js @@ -0,0 +1,593 @@ +odoo.define('website.snippets.animation', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var Class = require('web.Class'); +var core = require('web.core'); +var base = require('web_editor.base'); +var animation = require('web_editor.snippets.animation'); + +var qweb = core.qweb; + +// Initialize fallbacks for the use of requestAnimationFrame, cancelAnimationFrame and performance.now() +window.requestAnimationFrame = window.requestAnimationFrame + || window.webkitRequestAnimationFrame + || window.mozRequestAnimationFrame + || window.msRequestAnimationFrame + || window.oRequestAnimationFrame + || function (callback) { window.setTimeout(callback, 10); }; + +window.cancelAnimationFrame = window.cancelAnimationFrame + || window.webkitCancelAnimationFrame + || window.mozCancelAnimationFrame + || window.msCancelAnimationFrame + || window.oCancelAnimationFrame + || function (id) { window.clearTimeout(id); }; + +if (!window.performance || !window.performance.now) window.performance = {now: function () { return Date.now(); }}; + +/** + * The AnimationComponent class in in charge of handling one animation loop using the requestAnimationFrame + * feature. This is used by the Animation class below and should not be called directly by an end developer. + * The component contains a simple API, it can be started, stopped, played and paused. + */ +var AnimationComponent = Class.extend({ + init: function (parent, updateCallback, startEvents, $startTarget, options) { + this.parent = parent; // The Animation class instance which is using this component + + options = options || {}; + this._minFrameTime = 1000 / (options.maxFPS || 100); + + // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks + this._updateCallback = updateCallback || function () {}; + this.startEvents = startEvents || "scroll"; + this.$startTarget = $($startTarget || window); + this._getStateCallback = options.getStateCallback + || ((this.startEvents === "scroll" && this.$startTarget[0] === window) ? function () { return window.pageYOffset; } : false) + || ((this.startEvents === "resize" && this.$startTarget[0] === window) ? function () { return {width: window.innerWidth, height: window.innerHeight}; } : false) + || function () { return undefined; }; + this.endEvents = options.endEvents || false; + this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget; + + // Add a namespace to events using the generated uid + this._uid = "_animationComponent" + _.uniqueId(); + this.startEvents = _processEvents(this.startEvents, this._uid); + if (this.endEvents) { + this.endEvents = _processEvents(this.endEvents, this._uid); + } + + function _processEvents(events, namespace) { + events = events.split(" "); + return _.each(events, function (e, index) { + events[index] += ("." + namespace); + }).join(" "); + } + }, + /** + * The start method initializes when the animation must be played and paused and initializes the + * animation first frame. + */ + start: function () { + // Initialize the animation first frame + this._paused = false; + this._rafID = window.requestAnimationFrame((function (t) { + this._update(t); + this._paused = true; + }).bind(this)); + + // Initialize the animation play/pause events + if (this.endEvents) { + /** + * If there are endEvents, the animation should begin playing when the startEvents are triggered + * on the startTarget and pause when the endEvents are triggered on the endTarget. + */ + this.$startTarget.on(this.startEvents, (function (e) { + if (this._paused) { + _.defer(this.play.bind(this, e)); + } + }).bind(this)); + this.$endTarget.on(this.endEvents, (function () { + if (!this._paused) { + _.defer(this.pause.bind(this)); + } + }).bind(this)); + } else { + /** + * Else, if there is no endEvent, the animation should begin playing when the startEvents are + * *continuously* triggered on the startTarget or fully played once. To achieve this, the animation + * begins playing and is scheduled to pause after 2 seconds. If the startEvents are triggered during + * that time, the schedule is delayed for another 2 seconds. This allows to describe an "effect" + * animation (which lasts less than 2 seconds) or an animation which must be playing *during* an event + * (scroll, mousemove, resize, repeated clicks, ...). + */ + var pauseTimer = null; + this.$startTarget.on(this.startEvents, _.throttle((function (e) { + this.play(e); + + clearTimeout(pauseTimer); + pauseTimer = _.delay((function () { + this.pause(); + pauseTimer = null; + }).bind(this), 2000); + }).bind(this), 250, {trailing: false})); + } + }, + /** + * The stop method pauses the animation and destroys the attached events which trigger the + * animation to be played or paused. + */ + stop: function () { + this.$startTarget.off(this.startEvents); + if (this.endEvents) { + this.$endTarget.off(this.endEvents); + } + this.pause(); + }, + /** + * The play method forces the requestAnimationFrame loop to start. + * @param e -> the event which triggered the animation to play + */ + play: function (e) { + this._newEvent = e; + if (!this._paused) return; + this._paused = false; + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + this._lastUpdateTimestamp = undefined; + }, + /** + * The pause method forces the requestAnimationFrame loop to stop. + */ + pause: function () { + if (this._paused) return; + this._paused = true; + window.cancelAnimationFrame(this._rafID); + this._lastUpdateTimestamp = undefined; + }, + /** + * The private _update method is the callback which is repeatedly called by the requestAnimationFrame + * loop. It controls the max fps at which the animation is running and initializes the values that + * the update callback needs to describe the animation (state, elapsedTime, triggered event). + * @param timestamp -> the DOMHighResTimeStamp timestamp at which the function is called + */ + _update: function (timestamp) { + if (this._paused) return; + this._rafID = window.requestAnimationFrame(this._update.bind(this)); + + // Check the elapsed time since the last update callback call. + // Consider it 0 if there is no info of last timestamp and leave this _update call if it was + // called too soon (would overflow the set max FPS). + var elapsedTime = 0; + if (this._lastUpdateTimestamp) { + elapsedTime = timestamp - this._lastUpdateTimestamp; + if (elapsedTime < this._minFrameTime) return; + } + + // Check the new animation state thanks to the get state callback and store its new value. + // If the state is the same as the previous one, leave this _update call, except if there + // is an event which triggered the "play" method again. + var animationState = this._getStateCallback.call(this.parent, elapsedTime, this._newEvent); + if (!this._newEvent && animationState !== undefined && _.isEqual(animationState, this._animationLastState)) return; + this._animationLastState = animationState; + + // Call the update callback with frame parameters + this._updateCallback.call(this.parent, this._animationLastState, elapsedTime, this._newEvent); + this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called + this._newEvent = undefined; // Forget the event which triggered the last "play" call + }, +}); + +/** + * Extend the Animation class to be able to register AnimationComponent's automatically according to + * the methods it defines. Also allows to register them in more conventional ways by extending the + * _prepareComponents method and calling the _addComponent method. + */ +animation.Class.include({ + maxFPS: 100, // The max FPS at which all the automatic animation components will be running by default + + /** + * Extend the start method to prepare animation components and start them. + */ + start: function () { + this._super.apply(this, arguments); + + this._prepareComponents(); + _.each(this._animationComponents, function (component) { + component.start(); + }); + }, + /** + * Extend the start method to stop animation components and destroy them. + */ + stop: function () { + this._super.apply(this, arguments); + + _.each(this._animationComponents, function (component) { + component.stop(); + }); + this._animationComponents = []; + }, + /** + * The private _prepareComponents method is the place to register AnimationComponent instances. + * This can be done by extending this method and calling the _addComponent method in it or by + * defining particulary-named methods in the specialized Animation class. + * + * The automatic creation of components according to method names is working like this: + * - Search for methods which match the pattern: on_startEvent[_startTarget[_off_endEvent[_endTarget]]] where: + * - startEvent: the event name which triggers the animation to begin playing + * - startTarget (optional): the selector (see description below) to find the target where to listen + * for startEvent (if no selector, the window target will be used) + * - endEvent (optional): the event name which triggers the end of the animation (if none is defined, the + * animation will stop after a while, @see AnimationComponent.start) + * - endTarget (optional): the selector (see description below) to find the target where to listen + * for endEvent (if no selector, the startTarget will be used) + * - For all of these methods, register the appropriate animation component with the method implementation used + * as the update callback. The method receives 3 arguments: the animation state, the elapsedTime since last + * update and the event which triggered the animation (undefined if just a new update call without trigger). + * The animation state is undefined by default, the scroll offset for the particular "on_scroll" method and + * and object with width and height for the particular "on_resize" method. There is the possibility to define + * the getState callback of the animation component with this method. For the component created by the definition + * of the "on_abc_def", define the "get_state_for_on_abc_def" (so "get_state_for_" followed by the method name). + * This allows to improve performance even further in some cases. + * + * Selectors mentioned above can be: + * - the "selector" string: this tells the system to use the Animation instance $target element as target for the + * animation component + * - an underscore-separated list of classnames: for example "leftPanel_playBtn", the system will search inside + * the Animation instance $target element for a DOM which matches the selector ".leftPanel .playBtn" + */ + _prepareComponents: function () { + this._animationComponents = []; + + var self = this; + _.each(this.__proto__, function (callback, key) { + if (!_.isFunction(callback)) return; + var m = key.match(/^on_([^_]+)(?:_(.+?))?(?:_off_([^_]+)(?:_(.+))?)?$/); // match the pattern described above + if (!m) return; + + self._addComponent(callback, m[1], _target_from_ugly_selector(m[2]), { + getStateCallback: self["get_state_for_" + key] || undefined, + endEvents: m[3] || undefined, + $endTarget: _target_from_ugly_selector(m[4]), + maxFPS: self.maxFPS, + }); + + // Return the DOM element matching the selector in the form described above. + function _target_from_ugly_selector(selector) { + if (selector) { + if (selector === "selector") { + return self.$target; + } else { + return self.$(_.map(selector.split("_"), function (v) { + return "." + v; + }).join(" ")); + } + } + return undefined; + } + }); + }, + /** + * The private _addComponent method registers a new AnimationComponent according to given parameters + * @see AnimationComponent.init + */ + _addComponent: function (updateCallback, startEvents, $startTarget, options) { + this._animationComponents.push(new AnimationComponent(this, updateCallback, startEvents, $startTarget, options)); + }, +}); + +function load_called_template () { + var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get()); + if (ids_or_xml_ids.length) { + ajax.jsonRpc('/website/multi_render', 'call', { + 'ids_or_xml_ids': ids_or_xml_ids + }).then(function (data) { + for (var k in data) { + var $data = $(data[k]).addClass('o_block_'+k); + $("[data-oe-call='"+k+"']").each(function () { + $(this).replaceWith($data.clone()); + }); + } + }); + } +} + +base.ready().then(function () { + load_called_template(); + if ($(".o_gallery:not(.oe_slideshow)").size()) { + // load gallery modal template + ajax.loadXML('/website/static/src/xml/website.gallery.xml', qweb); + } +}); + +animation.registry.slider = animation.Class.extend({ + selector: ".carousel", + start: function () { + this.$target.carousel(); + return this._super.apply(this, arguments); + }, + stop: function () { + this.$target.carousel('pause'); + this.$target.removeData("bs.carousel"); + return this._super.apply(this, arguments); + }, +}); + +animation.registry.parallax = animation.Class.extend({ + selector: ".parallax", + start: function () { + this._super.apply(this, arguments); + this._rebuild(); + $(window).on("resize.animation_parallax", _.debounce(this._rebuild.bind(this), 500)); + }, + stop: function () { + this._super.apply(this, arguments); + $(window).off(".animation_parallax"); + }, + _rebuild: function () { + // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax) + if (!this.$bg || !this.$bg.length) { + this.$bg = this.$("> .s_parallax_bg"); + if (!this.$bg.length) { + this.$bg = $('', { + class: 's_parallax_bg' + (this.$target.hasClass('oe_custom_bg') ? ' oe_custom_bg' : ''), + }).prependTo(this.$target); + } + } + var urlTarget = this.$target.css("background-image"); + if (urlTarget !== "none") { + this.$bg.css("background-image", urlTarget); + } + this.$target.css("background-image", "none"); + + // Get parallax speed + this.speed = parseFloat(this.$target.attr("data-scroll-background-ratio") || 0); + + // Reset offset if parallax effect will not be performed and leave + this.$target.toggleClass("s_parallax_is_fixed", this.speed === 1); + if (this.speed === 0 || this.speed === 1) { + this.$bg.css({ + transform: "", + top: "", + bottom: "" + }); + return; + } + + // Initialize parallax data according to snippet and viewport dimensions + this.viewport = document.body.clientHeight - $('#wrapwrap').position().top; + this.visible_area = [this.$target.offset().top]; + this.visible_area.push(this.visible_area[0] + this.$target.innerHeight() + this.viewport); + this.ratio = this.speed * (this.viewport / 10); + + // Provide a "safe-area" to limit parallax + this.$bg.css({ + top: -this.ratio, + bottom: -this.ratio + }); + }, + on_scroll: function (scrollOffset) { + if (this.speed === 0 || this.speed === 1) return; + + // Perform translate if the element is visible only + var vpEndOffset = scrollOffset + this.viewport; + if (vpEndOffset >= this.visible_area[0] && vpEndOffset <= this.visible_area[1]) { + this.$bg.css("transform", "translateY(" + _getNormalizedPosition.call(this, vpEndOffset) + "px)"); + } + + function _getNormalizedPosition(pos) { + // Normalize scroll in a 1 to 0 range + var r = (pos - this.visible_area[1]) / (this.visible_area[0] - this.visible_area[1]); + // Normalize accordingly to current options + return Math.round(this.ratio * (2 * r - 1)); + } + } +}); + +animation.registry.share = animation.Class.extend({ + selector: ".oe_share", + start: function () { + var url_regex = /(\?(?:|.*&)(?:u|url|body)=)(.*?)(&|#|$)/; + var title_regex = /(\?(?:|.*&)(?:title|text|subject)=)(.*?)(&|#|$)/; + var url = encodeURIComponent(window.location.href); + var title = encodeURIComponent($("title").text()); + this.$("a").each(function () { + var $a = $(this); + $a.attr("href", function(i, href) { + return href.replace(url_regex, function (match, a, b, c) { + return a + url + c; + }).replace(title_regex, function (match, a, b, c) { + return a + title + c; + }); + }); + if ($a.attr("target") && $a.attr("target").match(/_blank/i) && !$a.closest('.o_editable').length) { + $a.on('click', function () { + window.open(this.href,'','menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600'); + return false; + }); + } + }); + return this._super.apply(this, arguments); + } +}); + +animation.registry.media_video = animation.Class.extend({ + selector: ".media_iframe_video", + start: function () { + if (!this.$target.has('> iframe').length) { + var editor = '
 
'; + var size = '
 
'; + this.$target.html(editor+size); + } + this.$target.html(this.$target.html()+''); + return this._super.apply(this, arguments); + }, +}); + +animation.registry.ul = animation.Class.extend({ + selector: "ul.o_ul_folded, ol.o_ul_folded", + start: function (editable_mode) { + this.$('.o_ul_toggle_self').off('click').on('click', function (event) { + $(this).toggleClass('o_open'); + $(this).closest('li').find('ul,ol').toggleClass('o_close'); + event.preventDefault(); + }); + this.$('.o_ul_toggle_next').off('click').on('click', function (event) { + $(this).toggleClass('o_open'); + $(this).closest('li').next().toggleClass('o_close'); + event.preventDefault(); + }); + return this._super.apply(this, arguments); + }, +}); + +/** + * This is a fix for apple device (<= IPhone 4, IPad 2) + * Standard bootstrap requires data-toggle='collapse' element to be tags. Unfortunatly one snippet uses a + *
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. + */ +animation.registry._fix_apple_collapse = animation.Class.extend({ + selector: ".s_faq_collapse [data-toggle='collapse']", + start: function () { + this.$target.off("click._fix_apple_collapse").on("click._fix_apple_collapse", function () {}); + return this._super.apply(this, arguments); + }, +}); + +/* ------------------------------------------------------------------------- +Gallery Animation + +This ads a Modal window containing a slider when an image is clicked +inside a gallery +-------------------------------------------------------------------------*/ +animation.registry.gallery = animation.Class.extend({ + selector: ".o_gallery:not(.o_slideshow)", + start: function () { + this.$el.on("click", "img", this.click_handler); + return this._super.apply(this, arguments); + }, + click_handler : function (event) { + var $cur = $(event.currentTarget); + var edition_mode = ($cur.closest("[contenteditable='true']").size() !== 0); + + // show it only if not in edition mode + if (!edition_mode) { + var urls = [], + idx = undefined, + milliseconds = undefined, + params = undefined, + $images = $cur.closest(".o_gallery").find("img"), + size = 0.8, + dimensions = { + min_width : Math.round( window.innerWidth * size*0.9), + min_height : Math.round( window.innerHeight * size), + max_width : Math.round( window.innerWidth * size*0.9), + max_height : Math.round( window.innerHeight * size), + width : Math.round( window.innerWidth * size*0.9), + height : Math.round( window.innerHeight * size) + }; + + $images.each(function () { + urls.push($(this).attr("src")); + }); + var $img = ($cur.is("img") === true) ? $cur : $cur.closest("img"); + idx = urls.indexOf($img.attr("src")); + + milliseconds = $cur.closest(".o_gallery").data("interval") || false; + params = { + srcs : urls, + index: idx, + dim : dimensions, + interval : milliseconds, + id: _.uniqueId("slideshow_") + }; + var $modal = $(qweb.render('website.gallery.slideshow.lightbox', params)); + $modal.modal({ + keyboard : true, + backdrop : true + }); + $modal.on('hidden.bs.modal', function () { + $(this).hide(); + $(this).siblings().filter(".modal-backdrop").remove(); // bootstrap leaves a modal-backdrop + $(this).remove(); + + }); + $modal.find(".modal-content, .modal-body.o_slideshow").css("height", "100%"); + $modal.appendTo(document.body); + + this.carousel = new animation.registry.gallery_slider($modal.find(".carousel").carousel()); + } + } // click_handler +}); + +animation.registry.gallery_slider = animation.Class.extend({ + selector: ".o_slideshow", + start: function (editable_mode) { + var self = this; + this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel"); + this.$indicator = this.$carousel.find('.carousel-indicators'); + this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden + this.$next = this.$indicator.find('li.fa:last').css('visibility', ''); + var $lis = this.$indicator.find('li:not(.fa)'); + var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space + var realNbPerPage = nbPerPage || 1; + var nbPages = Math.ceil($lis.length / realNbPerPage); + + var index; + var page; + update(); + + function hide() { + $lis.each(function (i) { + $(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage)); + }); + if (editable_mode) { // do not remove DOM in edit mode + return; + } + if (page <= 0) { + self.$prev.detach(); + } else { + self.$prev.prependTo(self.$indicator); + } + if (page >= nbPages - 1) { + self.$next.detach(); + } else { + self.$next.appendTo(self.$indicator); + } + } + + function update() { + index = $lis.index($lis.filter('.active')) || 0; + page = Math.floor(index / realNbPerPage); + hide(); + } + + this.$carousel.on('slide.bs.carousel.gallery_slider', function () { + setTimeout(function () { + var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next'); + var index = $item.index(); + $lis.removeClass("active") + .filter('[data-slide-to="'+index+'"]') + .addClass("active"); + }, 0); + }); + this.$indicator.on('click.gallery_slider', '> li.fa', function () { + page += ($(this).hasClass('o_indicators_left') ? -1 : 1); + page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary + self.$carousel.carousel(page * realNbPerPage); + hide(); + }); + this.$carousel.on('slid.bs.carousel.gallery_slider', update); + }, + stop: function () { + this._super.apply(this, arguments); + + this.$prev.prependTo(this.$indicator); + this.$next.appendTo(this.$indicator); + this.$carousel.off('.gallery_slider'); + this.$indicator.off('.gallery_slider'); + }, +}); + +}); diff --git a/addons/website/static/src/js/content/zoomodoo.js b/addons/website/static/src/js/content/zoomodoo.js new file mode 100644 index 00000000000..0e152d0e6de --- /dev/null +++ b/addons/website/static/src/js/content/zoomodoo.js @@ -0,0 +1,292 @@ +(function ($) { + /** + This code has been more that widely inspired by easyZoom library. + + Copyright 2013 Matt Hinchliffe + + Permission is hereby granted, free of charge, to any person obtaining + a copy of this software and associated documentation files (the + "Software"), to deal in the Software without restriction, including + without limitation the rights to use, copy, modify, merge, publish, + distribute, sublicense, and/or sell copies of the Software, and to + permit persons to whom the Software is furnished to do so, subject to + the following conditions: + + The above copyright notice and this permission notice shall be + included in all copies or substantial portions of the Software. + **/ + + 'use strict'; + + var dw, dh, rw, rh, lx, ly; + + var defaults = { + + // Attribute to retrieve the zoom image URL from. + linkTag: 'a', + linkAttribute: 'data-zoom-image', + + // event to trigger zoom + event: 'click', //or mousenter + + // Prevent clicks on the zoom image link. + preventClicks: true, + + // disable on mobile + disabledOnMobile: true, + + // Callback function to execute before the flyout is displayed. + beforeShow: $.noop, + + // Callback function to execute before the flyout is removed. + beforeHide: $.noop, + + // Callback function to execute when the flyout is displayed. + onShow: $.noop, + + // Callback function to execute when the flyout is removed. + onHide: $.noop, + + // Callback function to execute when the cursor is moved while over the image. + onMove: $.noop + + }; + + /** + * ZoomOdoo + * @constructor + * @param {Object} target + * @param {Object} options (Optional) + */ + function ZoomOdoo(target, options) { + this.$target = $(target); + this.opts = $.extend({}, defaults, options, this.$target.data()); + + if (this.isOpen === undefined) { + this._init(); + } + } + + /** + * Init + * @private + */ + ZoomOdoo.prototype._init = function() { + if (window.outerWidth > 467 || !this.opts.disabledOnMobile) { + this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target; + this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target; + this.$flyout = $('
'); + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { + $attach = this.$target.parents(this.opts.attach); + } + $attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this)); + $attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this)); + this.$target.parent().on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this)); + + if (this.opts.preventClicks) { + this.$target.on('click.zoomodoo', function(e) { e.preventDefault(); }); + } + } + }; + + /** + * Show + * @param {MouseEvent|TouchEvent} e + * @param {Boolean} testMouseOver (Optional) + */ + ZoomOdoo.prototype.show = function(e, testMouseOver) { + var w1, h1, w2, h2; + var self = this; + + if (this.opts.beforeShow.call(this) === false) return; + + if (!this.isReady) { + return this._loadImage(this.$link.attr(this.opts.linkAttribute), function() { + if (self.isMouseOver || !testMouseOver) { + self.show(e); + } + }); + } + + var $attach = this.$target; + if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { + $attach = this.$target.parents(this.opts.attach); + } + $attach.parent().append(this.$flyout); + + w1 = this.$target.width(); + h1 = this.$target.height(); + + w2 = this.$flyout.width(); + h2 = this.$flyout.height(); + + dw = this.$zoom.width() - w2; + dh = this.$zoom.height() - h2; + + // For the case where the zoom image is actually smaller than + // the flyout. + if (dw < 0) dw = 0; + if (dh < 0) dh = 0; + + rw = dw / w1; + rh = dh / h1; + + this.isOpen = true; + + this.opts.onShow.call(this); + + if (e) { + this._move(e); + } + }; + + /** + * On enter + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onEnter = function(e) { + var touches = e.originalEvent.touches; + + this.isMouseOver = true; + if (!touches || touches.length === 1) { + e.preventDefault(); + this.show(e, true); + } + }; + + /** + * On move + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onMove = function(e) { + if (!this.isOpen) return; + + e.preventDefault(); + this._move(e); + }; + + /** + * On leave + * @private + */ + ZoomOdoo.prototype._onLeave = function() { + this.isMouseOver = false; + if (this.isOpen) { + this.hide(); + } + }; + + /** + * On load + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._onLoad = function(e) { + // IE may fire a load event even on error so test the image dimensions + if (!e.currentTarget.width) return; + + this.isReady = true; + + this.$flyout.html(this.$zoom); + + if (e.data.call) { + e.data(); + } + }; + + + + /** + * Load image + * @private + * @param {String} href + * @param {Function} callback + */ + ZoomOdoo.prototype._loadImage = function(href, callback) { + var zoom = new Image(); + + this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this)); + + zoom.style.position = 'absolute'; + zoom.src = href; + }; + + /** + * Move + * @private + * @param {Event} e + */ + ZoomOdoo.prototype._move = function(e) { + if (e.type.indexOf('touch') === 0) { + var touchlist = e.touches || e.originalEvent.touches; + lx = touchlist[0].pageX; + ly = touchlist[0].pageY; + } else { + lx = e.pageX || lx; + ly = e.pageY || ly; + } + + var offset = this.$target.offset(); + var pt = ly - offset.top; + var pl = lx - offset.left; + var xt = Math.ceil(pt * rh); + var xl = Math.ceil(pl * rw); + + // Close if outside + if (xl < 0 || xt < 0 || xl > dw || xt > dh) { + this.hide(); + } else { + var top = xt * -1; + var left = xl * -1; + + this.$zoom.css({ + top: top, + left: left + }); + + this.opts.onMove.call(this, top, left); + } + + }; + + /** + * Hide + */ + ZoomOdoo.prototype.hide = function() { + if (!this.isOpen) return; + if (this.opts.beforeHide.call(this) === false) return; + + this.$flyout.detach(); + this.isOpen = false; + + this.opts.onHide.call(this); + }; + + + // jQuery plugin wrapper + $.fn.zoomOdoo = function(options) { + return this.each(function() { + var api = $.data(this, 'zoomOdoo'); + + if (!api) { + $.data(this, 'zoomOdoo', new ZoomOdoo(this, options)); + } else if (api.isOpen === undefined) { + api._init(); + } + }); + }; + + // AMD and CommonJS module compatibility + if (typeof define === 'function' && define.amd){ + define(function() { + return ZoomOdoo; + }); + } else if (typeof module !== 'undefined' && module.exports) { + module.exports = ZoomOdoo; + } + +})(jQuery); diff --git a/addons/website/static/src/js/editor/editor.js b/addons/website/static/src/js/editor/editor.js new file mode 100644 index 00000000000..fb2e03f3aa8 --- /dev/null +++ b/addons/website/static/src/js/editor/editor.js @@ -0,0 +1,151 @@ +odoo.define('website.editor', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var widget = require('web_editor.widget'); +var website = require('website.website'); + +var qweb = core.qweb; +var _t = core._t; + +ajax.loadXML('/website/static/src/xml/website.editor.xml', qweb); + +/** + * Object who contains all method and bind for the top bar, the template is create server side. + */ +website.TopBar.include({ + events: { + 'click [data-action="edit"]': 'edit', + }, + start: function () { + $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]:empty") + .filter(".oe_not_editable") + .filter(".oe_no_empty") + .addClass("oe_empty"); + + if (location.search.indexOf("enable_editor") >= 0 && $('html').attr('lang').match(/en[-_]US/)) { + this.$el.addClass('editing_mode'); + this.delayed_hide(); + } + + return this._super.apply(this, arguments); + }, + edit: function () { + this.$('button[data-action=edit]').prop('disabled', true); + this.$el.addClass('editing_mode'); + editor.editor_bar = new editor.Class(this); + editor.editor_bar.prependTo(document.body); + $('.o_homepage_editor_welcome_message').remove(); + + this.delayed_hide(); + }, + delayed_hide: function () { + _.delay((function () { + this.do_hide(); + }).bind(this), 800); + }, +}); + +/* ----- Customize Template ---- */ + +website.TopBarCustomize = Widget.extend({ + events: { + 'click > ul a[data-view-id]': 'do_customize', + }, + start: function () { + this.view_name = $(document.documentElement).data('view-xmlid'); + if (!this.view_name) { + this.$el.hide(); + } + + if (this.$el.is(".open")) { + this.load_menu(); + } else { + this.$el.one("mousedown", "> a.dropdown-toggle", this.load_menu.bind(this)); + } + }, + load_menu: function () { + var $menu = this.$el.children("ul"); + ajax.jsonRpc('/website/get_switchable_related_views', 'call', { + key: this.view_name, + }).then(function (result) { + var current_group = ""; + _.each(result, function (item) { + if (current_group !== item.inherit_id[1]) { + current_group = item.inherit_id[1]; + $menu.append("
  • " + current_group + "
  • "); + } + var $li = $('
  • ', {role: 'presentation'}) + .append($('', {href: '#', 'data-view-id': item.id, role: 'menuitem'}) + .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name}))); + $li.find('input').prop('checked', !!item.active); + $menu.append($li); + }); + }); + }, + do_customize: function (e) { + e.preventDefault(); + var view_id = $(e.currentTarget).data('view-id'); + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'ir.ui.view', + method: 'toggle', + args: [[parseInt(view_id, 10)]], + kwargs: {context: base.get_context()} + }).then(function () { + window.location.reload(); + }); + }, +}); +website.TopBar.include({ + start: function () { + this.customize_menu = new website.TopBarCustomize(); + var def = this.customize_menu.attachTo($('#customize-menu')); + return $.when(this._super(), def); + } +}); + +/* ----- EDITOR: LINK & MEDIA ---- */ + +widget.LinkDialog.include({ + bind_data: function () { + this.$( "#o_link_dialog_url_input" ).autocomplete({ + source: function (request, response) { + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'search_pages', + args: [null, request.term], + kwargs: { + limit: 15, + context: base.get_context(), + }, + }).then(function (exists) { + var rs=_.map(exists, function (r) { + return r.loc; + }); + response(rs); + }); + } + }); + return this._super(); + } +}); + +/** + * Display a welcome message on the homepage when it is empty and that the user is connected. + */ +base.ready().then(function () { + if (location.search.indexOf("enable_editor") < 0 && $(".editor_enable").length === 0) { + var $wrap = $("#wrapwrap.homepage #wrap"); + if ($wrap.length && $wrap.html().trim() === "") { + var $welcome_message = $(qweb.render("website.homepage_editor_welcome_message")); + $welcome_message.css("min-height", $wrap.parent("main").height() - ($wrap.outerHeight(true) - $wrap.height())); + $wrap.empty().append($welcome_message); + } + } +}); + +}); diff --git a/addons/website/static/src/js/editor/snippets.options.js b/addons/website/static/src/js/editor/snippets.options.js new file mode 100644 index 00000000000..098e7fc94a2 --- /dev/null +++ b/addons/website/static/src/js/editor/snippets.options.js @@ -0,0 +1,538 @@ +odoo.define('website.snippets.editor', function (require) { +'use strict'; + +var ajax = require("web.ajax"); +var core = require("web.core"); +var Dialog = require("web.Dialog"); +var rpc = require("web.rpc"); +var editor = require("web_editor.editor"); +var animation = require('web_editor.snippets.animation'); +var options = require('web_editor.snippets.options'); +var snippet_editor = require('web_editor.snippet.editor'); +var website = require('website.website'); + +var _t = core._t; + +snippet_editor.Class.include({ + _get_snippet_url: function () { + return '/website/snippets'; + } +}); + +options.registry.menu_data = options.Class.extend({ + start: function () { + this._super.apply(this, arguments); + this.link = this.$target.attr("href"); + }, + + on_focus: function () { + this._super.apply(this, arguments); + + (new Dialog(null, { + title: _t("Confirmation"), + $content: $(core.qweb.render("website.leaving_current_page_edition")), + buttons: [ + {text: _t("Go to Link"), classes: "btn-primary", click: save_editor_then_go_to.bind(null, this.link)}, + {text: _t("Edit the menu"), classes: "btn-primary", click: function () { + var self = this; + website.topBar.content_menu.edit_menu(function () { + return editor.editor_bar.save_without_reload(); + }).then(function (dialog) { + self.close(); + }); + }}, + {text: _t("Stay on this page"), close: true} + ] + })).open(); + + function save_editor_then_go_to(url) { + editor.editor_bar.save_without_reload().then(function () { + window.location.href = url; + }); + } + }, +}); + +options.registry.company_data = options.Class.extend({ + start: function () { + this._super.apply(this, arguments); + + var proto = options.registry.company_data.prototype; + + if (proto.__link_deferred === undefined) { + proto.__link_deferred = $.Deferred(); + return ajax.jsonRpc("/web/session/get_session_info", "call").then(function (session) { + return rpc.query({ + model: 'res.users', + method: 'read', + args: [session.uid, ['company_id']], + }) + .then(function (res) { + proto.__link_deferred.resolve( + "/web#action=base.action_res_company_form&view_type=form&id=" + (res && res[0] && res[0].company_id[0] || 1) + ); + }); + }); + } + }, + + on_focus: function () { + this._super.apply(this, arguments); + + var proto = options.registry.company_data.prototype; + + Dialog.confirm(null, _t("Do you want to edit the company data ?"), { + confirm_callback: function () { + editor.editor_bar.save_without_reload().then(function () { + proto.__link_deferred.then(function (link) { + window.location.href = link; + }); + }); + }, + }); + }, +}); + +options.registry.slider = options.Class.extend({ + drop_and_build_snippet: function () { + this.id = "myCarousel" + new Date().getTime(); + this.$target.attr("id", this.id); + this.$target.find("[data-slide]").attr("data-cke-saved-href", "#" + this.id); + this.$target.find("[data-target]").attr("data-target", "#" + this.id); + this.rebind_event(); + }, + on_clone: function ($clone) { + var id = "myCarousel" + new Date().getTime(); + $clone.attr("id", id); + $clone.find("[data-slide]").attr("href", "#" + id); + $clone.find("[data-slide-to]").attr("data-target", "#" + id); + }, + // rebind event to active carousel on edit mode + rebind_event: function () { + var self = this; + this.$target.find('.carousel-indicators [data-slide-to]').off('click').on('click', function () { + self.$target.carousel(+$(this).data('slide-to')); }); + }, + clean_for_save: function () { + this._super(); + this.$target.find(".item").removeClass("next prev left right active") + .first().addClass("active"); + this.$target.find('.carousel-indicators').find('li').removeClass('active') + .first().addClass("active"); + }, + start : function () { + this._super.apply(this, arguments); + this.$target.carousel({interval: false}); + this.id = this.$target.attr("id"); + this.$inner = this.$target.find('.carousel-inner'); + this.$indicators = this.$target.find('.carousel-indicators'); + this.$target.carousel('pause'); + this.rebind_event(); + }, + add_slide: function (type) { + if(type !== "click") return; + + var self = this; + var cycle = this.$inner.find('.item').length; + var $active = this.$inner.find('.item.active, .item.prev, .item.next').first(); + var index = $active.index(); + this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden"); + this.$indicators.append('
  • '); + + // clone the best candidate from template to use new features + var $snippets = this.buildingBlock.$snippets; + //since saas-6, all snippets must start by s_ + var selection = this.$target.closest('[class*="s_"'); + if (_.isUndefined(selection)) { + var point = 0; + var className = _.compact(this.$target.attr("class").split(" ")); + $snippets.find('.oe_snippet_body').each(function () { + var len = _.intersection(_.compact(this.className.split(" ")), className).length; + if (len > point) { + point = len; + selection = this; + } + }); + } + else { + var s_class = selection.attr('class').split(' ').filter(function (o) { return _.str.startsWith(o, "s_"); })[0]; + selection = $snippets.find("." + s_class); + } + var $clone = $(selection).find('.item:first').clone(); + + // insert + $clone.removeClass('active').insertAfter($active); + setTimeout(function () { + self.$target.carousel().carousel(++index); + self.rebind_event(); + },0); + return $clone; + }, + remove_slide: function (type) { + if (type !== "click" || this.remove_process) return; + var self = this; + + var $items = this.$inner.find('.item'); + var cycle = $items.length - 1; + var $active = $items.filter('.active'); + var index = $active.index(); + + if (cycle > 0) { + this.remove_process = true; + this.$target.on('slid.bs.carousel.slide_removal', function (event) { + $active.remove(); + self.$indicators.find("li:last").remove(); + self.$target.off('slid.bs.carousel.slide_removal'); + self.rebind_event(); + self.remove_process = false; + if (cycle === 1) { + self.$target.find('.carousel-control, .carousel-indicators').addClass("hidden"); + } + }); + _.defer(function () { + self.$target.carousel(index > 0 ? --index : cycle); + }); + } + }, + interval : function (type, value) { + this.$target.attr("data-interval", value); + }, + set_active: function () { + this.$el.find('li[data-interval]').removeClass("active") + .filter('li[data-interval='+this.$target.attr("data-interval")+']').addClass("active"); + }, +}); + +options.registry.carousel = options.registry.slider.extend({ + getSize: function () { + this.grid = this._super(); + this.grid.size = 8; + return this.grid; + }, + clean_for_save: function () { + this._super(); + this.$target.removeClass('oe_img_bg ' + this._class).css("background-image", ""); + }, + load_style_options : function () { + this._super(); + $(".snippet-option-size li[data-value='']").remove(); + }, + start : function () { + var self = this; + this._super.apply(this, arguments); + + // set background and prepare to clean for save + this.$target.on('slid.bs.carousel', function () { + self.$target.carousel("pause"); + if (!self.editor) return; + + _.each(["background", "background_position", "colorpicker"], function (opt_name) { + var s_option = self.editor.styles[opt_name]; + if (!s_option) return; + + s_option.$target = self.$target.find(".item.active"); + s_option.set_active(); + s_option.$target.trigger("snippet-option-change", [s_option]); + if (opt_name === 'background') { + s_option.bind_bg_events(); + } + }); + }); + this.$target.trigger('slid.bs.carousel'); + }, + // rebind event to active carousel on edit mode + rebind_event: function () { + var self = this; + this.$target.find('.carousel-control').off('click').on('click', function () { + self.$target.carousel($(this).data('slide')); + }); + this._super.apply(this, arguments); + + /* Fix: backward compatibility saas-3 */ + this.$target.find('.item.text_image, .item.image_text, .item.text_only').find('.container > .carousel-caption > div, .container > img.carousel-image').attr('contentEditable', 'true'); + }, +}); + +options.registry["margin-x"] = options.registry.marginAndResize.extend({ + preventChildPropagation: true, + + getSize: function () { + this.grid = this._super(); + var width = this.$target.parents(".row:first").first().outerWidth(); + + var grid = [1,2,3,4,5,6,7,8,9,10,11,12]; + this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})]; + + grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11]; + this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12]; + + return this.grid; + }, + on_clone: function ($clone, options) { + // Below condition is added to remove offset of target element only + // and not its children to avoid design alteration of a container / block. + if (options.isCurrent) { + var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''); + $clone.attr("class", _class); + } + return false; + }, + on_resize: function (compass, beginClass, current) { + if (compass === 'w') { + // don't change the right border position when we change the offset (replace col size) + var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); + var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); + var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); + if (offset < 0) { + offset = 0; + } + var colSize = beginCol - (offset - beginOffset); + if (colSize <= 0) { + colSize = 1; + offset = beginOffset + beginCol - 1; + } + this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, '')); + + this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize)); + if (offset > 0) { + this.$target.addClass('col-md-offset-' + offset); + } + } + this._super(compass, beginClass, current); + }, +}); + +options.registry.parallax = options.Class.extend({ + getSize: function () { + this.grid = this._super.apply(this, arguments); + this.grid.size = 8; + return this.grid; + }, + start: function () { + this._super.apply(this, arguments); + if (!this.$target.data("snippet-view")) { + this.$target.data("snippet-view", new animation.registry.parallax(this.$target)); + } + this._refresh_callback = this._refresh.bind(this); + this._toggle_refresh_callback(true); + }, + on_focus: function () { + this._super.apply(this, arguments); + this._update_target_to_bg(); + }, + on_resize: function () { + this._super.apply(this, arguments); + this._refresh(); + }, + scroll: function (type, value) { + this.$target.attr("data-scroll-background-ratio", value); + this._refresh(); + }, + set_active: function () { + this._super.apply(this, arguments); + this.$el.find('[data-scroll]').removeClass("active") + .filter('[data-scroll="' + (this.$target.attr('data-scroll-background-ratio') || 0) + '"]').addClass("active"); + }, + clean_for_save: function () { + this._super.apply(this, arguments); + this._toggle_refresh_callback(false); + }, + on_move: function () { + this._super.apply(this, arguments); + this._refresh(); + }, + on_remove: function () { + this._super.apply(this, arguments); + this._toggle_refresh_callback(false); + }, + _update_target_to_bg: function () { + this.editor.styles.background.$target = this.$target.data("snippet-view").$bg; + this.editor.styles.background.set_active(); + this.editor.styles.background_position.$target = this.$target.data("snippet-view").$bg; + this.editor.styles.background_position.set_active(); + }, + _refresh: function () { + _.defer((function () { + this.$target.data("snippet-view")._rebuild(); + }).bind(this)); + }, + _toggle_refresh_callback: function (on) { + this.$target[on ? "on" : "off"]("snippet-option-change snippet-option-preview", this._refresh_callback); + this.buildingBlock.$el[on ? "on" : "off"]("snippet-dropped snippet-activated", this._refresh_callback); + }, +}); + +options.registry.ul = options.Class.extend({ + start: function () { + var self = this; + this._super(); + this.$target.data("snippet-view", new animation.registry.ul(this.$target, true)); + this.$target.on('mouseup', '.o_ul_toggle_self, .o_ul_toggle_next', function () { + setTimeout(function () { + self.buildingBlock.cover_target(self.$overlay, self.$target); + },0); + }); + }, + reset_ul: function () { + this.$target.find('.o_ul_toggle_self, .o_ul_toggle_next').remove(); + + this.$target.find('li:has(>ul,>ol)').map(function () { + // get if the li contain a text label + var texts = _.filter(_.toArray(this.childNodes), function (a) { return a.nodeType == 3;}); + if (!texts.length || !texts.reduce(function (a,b) { return a.textContent + b.textContent;}).match(/\S/)) { + return; + } + $(this).children('ul,ol').addClass('o_close'); + return $(this).children(':not(ul,ol)')[0] || this; + }) + .prepend(''); + + var $li = this.$target.find('li:has(+li:not(>.o_ul_toggle_self)>ul, +li:not(>.o_ul_toggle_self)>ol)'); + $li.map(function () { return $(this).children()[0] || this; }) + .prepend(''); + $li.removeClass('o_open').next().addClass('o_close'); + + this.$target.find("li").removeClass('o_open').css('list-style', ''); + this.$target.find("li:has(.o_ul_toggle_self, .o_ul_toggle_next), li:has(>ul,>ol):not(:has(>li))").css('list-style', 'none'); + }, + clean_for_save: function () { + this._super(); + if (!this.$target.hasClass('o_ul_folded')) { + this.$target.find(".o_close").removeClass("o_close"); + } + this.$target.find("li:not(:has(>ul))").css('list-style', ''); + }, + toggle_class: function (type, value, $li) { + this._super(type, value, $li); + this.$target.data("snippet-view").stop(); + this.reset_ul(); + this.$target.find("li:not(:has(>ul))").css('list-style', ''); + this.$target.data("snippet-view", new animation.registry.ul(this.$target, true)); + } +}); + +options.registry.collapse = options.Class.extend({ + start: function () { + var self = this; + this._super(); + this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () { + self.buildingBlock.cover_target(self.$overlay, self.$target); + }); + }, + create_ids: function ($target) { + var time = new Date().getTime(); + var $tab = $target.find('[data-toggle="collapse"]'); + + // link to the parent group + + var $tablist = $target.closest('.panel-group'); + var tablist_id = $tablist.attr("id"); + if (!tablist_id) { + tablist_id = "myCollapse" + time; + $tablist.attr("id", tablist_id); + } + $tab.attr('data-parent', "#"+tablist_id); + $tab.data('parent', "#"+tablist_id); + + // link to the collapse + + var $panel = $target.find('.panel-collapse'); + var panel_id = $panel.attr("id"); + if (!panel_id) { + while($('#'+(panel_id = "myCollapseTab" + time)).length) { + time++; + } + $panel.attr("id", panel_id); + } + $tab.attr('data-target', "#"+panel_id); + $tab.data('target', "#"+panel_id); + }, + drop_and_build_snippet: function () { + this._super(); + this.create_ids(this.$target); + }, + on_clone: function ($clone) { + this._super.apply(this, arguments); + $clone.find('[data-toggle="collapse"]').removeAttr('data-target').removeData('target'); + $clone.find('.panel-collapse').removeAttr('id'); + this.create_ids($clone); + }, + on_move: function () { + this._super(); + this.create_ids(this.$target); + var $panel = this.$target.find('.panel-collapse').removeData('bs.collapse'); + if ($panel.attr('aria-expanded') === 'true') { + $panel.closest('.panel-group').find('.panel-collapse[aria-expanded="true"]') + .filter(function () {return this !== $panel[0];}) + .collapse('hide') + .one('hidden.bs.collapse', function () { + $panel.trigger('shown.bs.collapse'); + }); + } + } +}); + +return options; +}); + + +odoo.define('website.rte.summernote', function (require) { +'use strict'; + +var core = require('web.core'); +require('web_editor.rte.summernote'); + +var eventHandler = $.summernote.eventHandler; +var renderer = $.summernote.renderer; +var tplIconButton = renderer.getTemplate().iconButton; +var _t = core._t; + +var fn_tplPopovers = renderer.tplPopovers; +renderer.tplPopovers = function (lang, options) { + var $popover = $(fn_tplPopovers.call(this, lang, options)); + $popover.find('.note-image-popover .btn-group:has([data-value="img-thumbnail"])').append( + tplIconButton('fa fa-object-ungroup', { + title: _t('Transform the picture (click twice to reset transformation)'), + event: 'transform', + })); + return $popover; +}; + + +$.summernote.pluginEvents.transform = function (event, editor, layoutInfo, sorted) { + var $selection = layoutInfo.handle().find('.note-control-selection'); + var $image = $($selection.data('target')); + + if($image.data('transfo-destroy')) { + $image.removeData('transfo-destroy'); + return; + } + + $image.transfo(); + + var mouseup = function (event) { + $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]')); + }; + $(document).on('mouseup', mouseup); + + var mousedown = function (event) { + if (!$(event.target).closest('.transfo-container').length) { + $image.transfo("destroy"); + $(document).off('mousedown', mousedown).off('mouseup', mouseup); + } + if ($(event.target).closest('.note-popover').length) { + $image.data('transfo-destroy', true).attr("style", ($image.attr("style") || '').replace(/[^;]*transform[\w:]*;?/g, '')); + } + $image.trigger('content_changed'); + }; + $(document).on('mousedown', mousedown); +}; + +var fn_boutton_update = eventHandler.modules.popover.button.update; +eventHandler.modules.popover.button.update = function ($container, oStyle) { + fn_boutton_update.call(this, $container, oStyle); + $container.find('button[data-event="transform"]') + .toggleClass('active', $(oStyle.image).is('[style*="transform"]')) + .toggleClass('hidden', !$(oStyle.image).is('img')); +}; +}); diff --git a/addons/website/static/src/js/menu/content.js b/addons/website/static/src/js/menu/content.js new file mode 100644 index 00000000000..70f35871e74 --- /dev/null +++ b/addons/website/static/src/js/menu/content.js @@ -0,0 +1,361 @@ +odoo.define('website.contentMenu', function (require) { +"use strict"; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var editor = require('web_editor.editor'); +var widget = require('web_editor.widget'); +var website = require('website.website'); + +var _t = core._t; +var qweb = core.qweb; + +ajax.loadXML('/website/static/src/xml/website.contentMenu.xml', qweb); + +var TopBarContent = Widget.extend({ + start: function () { + var self = this; + + // Add page modal + menu content event + this.$el.add($('#o_website_add_page_modal')).on('click', 'a[data-action]', function (e) { + e.preventDefault(); + self[$(this).data('action')](); + }); + + return this._super(); + }, + edit_menu: function (action_before_reload) { + var context = base.get_context(); + var def = $.Deferred(); + if ($("[data-content_menu_id]").length) { + var select = new SelectEditMenuDialog(); + select.on('save', this, function (root) { + def.resolve(root); + }); + select.open(); + } else { + def.resolve(null); + } + + return def.then(function (root_id) { + return ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website.menu', + method: 'get_tree', + args: [context.website_id, root_id], + kwargs: { + context: context + }, + }).then(function (menu) { + var dialog = new EditMenuDialog(this, {}, menu).open(); + dialog.on("save", null, function () { + $.when(action_before_reload && action_before_reload()).then(function () { + editor.reload(); + }); + }); + return dialog; + }); + }); + }, + new_page: function () { + website.prompt({ + id: "editor_new_page", + window_title: _t("New Page"), + input: _t("Page Title"), + init: function () { + var $group = this.$dialog.find("div.form-group"); + $group.removeClass("mb0"); + + var $add = $('
    ', {'class': 'form-group mb0'}) + .append($('', {'class': 'col-sm-offset-3 col-sm-9 text-left'}) + .append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add page in menu")}))); + $add.find('input').prop('checked', true); + $group.after($add); + } + }).then(function (val, field, $dialog) { + if (val) { + var url = '/website/add/' + encodeURIComponent(val); + if ($dialog.find('input[type="checkbox"]').is(':checked')) url +="?add_menu=1"; + document.location = url; + } + }); + }, + rename_page: function () { + var self = this; + var context = base.get_context(); + self.mo_id = self.getMainObject().id; + + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'page_search_dependencies', + args: [self.mo_id], + kwargs: { + context: context + }, + }).then(function (deps) { + website.prompt({ + id: "editor_rename_page", + window_title: _t("Rename This Page"), + dependencies: deps, + }, 'website.rename_page').then(function (val, field, $dialog) { + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'rename_page', + args: [ + self.mo_id, + val, + ], + kwargs: { + context: context + }, + }).then(function (new_name) { + window.location = "/page/" + encodeURIComponent(new_name); + }); + }); + }); + }, + delete_page: function () { + var self = this; + var context = base.get_context(); + self.mo_id = self.getMainObject().id; + + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'page_search_dependencies', + args: [self.mo_id], + kwargs: { + context: context, + }, + }).then(function (deps) { + website.prompt({ + id: "editor_delete_page", + window_title: _t("Delete Page"), + dependencies: deps, + init: function () { $('.btn-continue').prop("disabled", true); }, + }, 'website.delete_page').then(function (val, field, $dialog) { + + if ($dialog.find('input[type="checkbox"]').is(':checked')) { + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'delete_page', + args: [self.mo_id], + kwargs: { + context: context + }, + }).then(function () { + window.location = "/"; + }); + } + }); + }); + }, + getMainObject: function () { + var repr = $('html').data('main-object'); + var m = repr.match(/(.+)\((\d+),(.*)\)/); + if (!m) { + return null; + } else { + return { + model: m[1], + id: m[2]|0 + }; + } + } +}); + +website.TopBar.include({ + start: function () { + this.content_menu = new TopBarContent(); + var def = this.content_menu.attachTo($('.oe_content_menu')); + return $.when(this._super(), def); + } +}); + +var SelectEditMenuDialog = widget.Dialog.extend({ + template: 'website.contentMenu.dialog.select', + init: function (parent, options) { + var self = this; + self.roots = [{id: null, name: _t("Top Menu")}]; + $("[data-content_menu_id]").each(function () { + self.roots.push({id: $(this).data("content_menu_id"), name: $(this).attr("name")}); + }); + this._super(parent, _.extend({}, { + title: _t("Select a Menu"), + save_text: _t("Continue") + }, options || {})); + }, + save: function () { + this.final_data = parseInt(this.$el.find("select").val() || null); + this._super.apply(this, arguments); + } +}); + +var EditMenuDialog = widget.Dialog.extend({ + template: 'website.contentMenu.dialog.edit', + events: _.extend({}, widget.Dialog.prototype.events, { + 'click a.js_add_menu': 'add_menu', + 'click button.js_edit_menu': 'edit_menu', + 'click button.js_delete_menu': 'delete_menu', + }), + init: function (parent, options, menu) { + this.menu = menu; + this.root_menu_id = menu.id; + this.flat = this.flatenize(menu); + this.to_delete = []; + this._super(parent, _.extend({}, { + title: _t("Edit Menu"), + size: 'medium', + }, options || {})); + }, + start: function () { + var r = this._super.apply(this, arguments); + this.$('.oe_menu_editor').nestedSortable({ + listType: 'ul', + handle: 'div', + items: 'li', + maxLevels: 2, + toleranceElement: '> div', + forcePlaceholderSize: true, + opacity: 0.6, + placeholder: 'oe_menu_placeholder', + tolerance: 'pointer', + attribute: 'data-menu-id', + expression: '()(.+)', // nestedSortable takes the second match of an expression (*sigh*) + }); + return r; + }, + flatenize: function (node, dict) { + dict = dict || {}; + var self = this; + dict[node.id] = node; + node.children.forEach(function (child) { + self.flatenize(child, dict); + }); + return dict; + }, + add_menu: function () { + var self = this; + var dialog = new MenuEntryDialog(this, {menu_link_options: true}, undefined, {}); + dialog.on('save', this, function (link) { + var new_menu = { + id: _.uniqueId('new-'), + name: link.text, + url: link.url, + new_window: link.isNewWindow, + parent_id: false, + sequence: 0, + children: [], + }; + self.flat[new_menu.id] = new_menu; + self.$('.oe_menu_editor').append( + qweb.render('website.contentMenu.dialog.submenu', { submenu: new_menu })); + }); + dialog.open(); + }, + edit_menu: function (ev) { + var self = this; + var menu_id = $(ev.currentTarget).closest('[data-menu-id]').data('menu-id'); + var menu = self.flat[menu_id]; + if (menu) { + var dialog = new MenuEntryDialog(this, {}, undefined, menu); + dialog.on('save', this, function (link) { + var id = link.id; + var menu_obj = self.flat[id]; + _.extend(menu_obj, { + 'name': link.text, + 'url': link.url, + 'new_window': link.isNewWindow, + }); + var $menu = self.$('[data-menu-id="' + id + '"]'); + $menu.find('.js_menu_label').first().text(menu_obj.name); + }); + dialog.open(); + } else { + alert("Could not find menu entry"); + } + }, + delete_menu: function (ev) { + var $menu = $(ev.currentTarget).closest('[data-menu-id]'); + var mid = $menu.data('menu-id')|0; + if (mid) { + this.to_delete.push(mid); + } + $menu.remove(); + }, + save: function () { + var _super = this._super.bind(this); + var self = this; + var new_menu = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0}); + var levels = []; + var data = []; + var context = base.get_context(); + // Resequence, re-tree and remove useless data + new_menu.forEach(function (menu) { + if (menu.id) { + levels[menu.depth] = (levels[menu.depth] || 0) + 1; + var mobj = self.flat[menu.id]; + mobj.sequence = levels[menu.depth]; + mobj.parent_id = (menu.parent_id|0) || menu.parent_id || self.root_menu_id; + delete(mobj.children); + data.push(mobj); + } + }); + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website.menu', + method: 'save', + args: [[context.website_id], { data: data, to_delete: self.to_delete }], + kwargs: { + context: context + }, + }).then(function () { + return _super(); + }); + }, +}); + +var MenuEntryDialog = widget.LinkDialog.extend({ + init: function (parent, options, editor, data) { + data.text = data.name || ''; + data.isNewWindow = data.new_window; + this.data = data; + this.menu_link_options = options.menu_link_options; + return this._super.apply(this, arguments); + }, + start: function () { + var self = this; + this.$(".o_link_dialog_preview").remove(); + this.$(".window-new, .link-style").closest(".form-group").remove(); + this.$("label[for='o_link_dialog_label_input']").text(_t("Menu Label")); + if (this.menu_link_options) { // add menu link option only when adding new menu + this.$('#o_link_dialog_label_input').closest('.form-group').after(qweb.render('website.contentMenu.dialog.edit.link_menu_options')); + this.$('input[name=link_menu_options]').on('change', function() { + self.$('#o_link_dialog_url_input').closest('.form-group').toggle(); + }); + } + this.$modal.find('.modal-lg').removeClass('modal-lg') + .find('.col-md-8').removeClass('col-md-8').addClass('col-xs-12'); + + return this._super.apply(this, arguments); + }, + save: function () { + var $e = this.$('#o_link_dialog_label_input'); + if (!$e.val() || !$e[0].checkValidity()) { + $e.closest('.form-group').addClass('has-error'); + $e.focus(); + return; + } + if (this.$('input[name=link_menu_options]:checked').val() === 'new_page') { + window.location = '/website/add/' + encodeURIComponent($e.val()) + '?add_menu=1'; + return; + } + return this._super.apply(this, arguments); + } +}); + +return { + 'TopBar': TopBarContent, + 'EditMenuDialog': EditMenuDialog, +}; + +}); diff --git a/addons/website/static/src/js/menu/mobile_view.js b/addons/website/static/src/js/menu/mobile_view.js new file mode 100644 index 00000000000..791775f57b8 --- /dev/null +++ b/addons/website/static/src/js/menu/mobile_view.js @@ -0,0 +1,49 @@ +odoo.define('website.mobile', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var Dialog = require('web.Dialog'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var website = require('website.website'); + +var _t = core._t; + +var MobilePreviewDialog = Dialog.extend({ + template: 'website.mobile_preview', + + init: function () { + this._super.apply(this, arguments); + this.mobile_src = $.param.querystring(window.location.href, 'mobilepreview'); + }, + + start: function () { + var self = this; + this.$modal.addClass('oe_mobile_preview'); + this.$modal.on('click', '.modal-header', function () { + self.$el.toggleClass('o_invert_orientation'); + }); + this.$iframe = this.$('iframe'); + this.$iframe.on('load', function (e) { + self.$iframe.contents().find('body').removeClass('o_connected_user'); + self.$iframe.contents().find('#oe_main_menu_navbar, #o_website_add_page_modal').remove(); + }); + + return this._super.apply(this, arguments); + }, +}); + +website.TopBar.include({ + start: function () { + var self = this; + this.$el.on('click', 'a[data-action=show-mobile-preview]', function () { + new MobilePreviewDialog(self, { + title: _t('Mobile preview') + " ", + }).open(); + }); + return this._super(); + }, +}); + +}); diff --git a/addons/website/static/src/js/menu/planner.js b/addons/website/static/src/js/menu/planner.js new file mode 100644 index 00000000000..59adfef92a0 --- /dev/null +++ b/addons/website/static/src/js/menu/planner.js @@ -0,0 +1,41 @@ +odoo.define('web.planner.website', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var planner = require('web.planner.common'); +var rpc = require('web.rpc'); +var session = require('web.session'); +var website = require('website.website'); + +var qweb = core.qweb; + +var WebsitePlannerLauncher = planner.PlannerLauncher.extend({ + _fetch_planner_data: function () { + return rpc.query({ + model: 'web.planner', + method: 'search_read', + args: [[['planner_application', '=', 'planner_website']]], + }) + .then((function (planner) { + if (!planner.length) return; + + planner[0].data = $.parseJSON(planner[0].data) || {}; + this._setup_for_planner(planner[0]); + }).bind(this)); + }, +}); + +if (session.is_system) { + website.TopBar.include({ + start: function () { + var websitePlannerLauncher = new WebsitePlannerLauncher(); + var def = ajax.loadXML('/web_planner/static/src/xml/web_planner.xml', qweb).then((function() { + return websitePlannerLauncher.prependTo(this.$(".o_menu_systray")); + }).bind(this)); + return $.when(this._super.apply(this, arguments), def); + }, + }); +} + +}); diff --git a/addons/website/static/src/js/menu/seo.js b/addons/website/static/src/js/menu/seo.js new file mode 100644 index 00000000000..93b434a5f44 --- /dev/null +++ b/addons/website/static/src/js/menu/seo.js @@ -0,0 +1,582 @@ +odoo.define('website.seo', function (require) { +'use strict'; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var Class = require('web.Class'); +var Dialog = require('web.Dialog'); +var mixins = require('web.mixins'); +var rpc = require("web.rpc"); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var website = require('website.website'); + +var _t = core._t; + +var qweb = core.qweb; + +ajax.loadXML('/website/static/src/xml/website.seo.xml', qweb); + + // This replaces \b, because accents(e.g. à, é) are not seen as word boundaries. + // Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b + var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)'; + +function analyzeKeyword(htmlPage, keyword) { + return htmlPage.isInTitle(keyword) ? { + title: 'label label-success', + description: "This keyword is used in the page title", + } : htmlPage.isInDescription(keyword) ? { + title: 'label label-primary', + description: "This keyword is used in the page description", + } : htmlPage.isInBody(keyword) ? { + title: 'label label-info', + description: "This keyword is used in the page content." + } : { + title: 'label label-default', + description: "This keyword is not used anywhere on the page." + }; +} + +var Suggestion = Widget.extend({ + template: 'website.seo_suggestion', + events: { + 'click .js_seo_suggestion': 'select', + }, + init: function (parent, options) { + this.root = options.root; + this.keyword = options.keyword; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.htmlPage.on('title-changed', this, this.renderElement); + this.htmlPage.on('description-changed', this, this.renderElement); + }, + analyze: function () { + return analyzeKeyword(this.htmlPage, this.keyword); + }, + highlight: function () { + return this.analyze().title; + }, + tooltip: function () { + return this.analyze().description; + }, + select: function () { + this.trigger('selected', this.keyword); + }, +}); + +var SuggestionList = Widget.extend({ + template: 'website.seo_suggestion_list', + init: function (parent, options) { + this.root = options.root; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.refresh(); + }, + refresh: function () { + var self = this; + self.$el.append(_t("Loading...")); + var language = self.language || base.get_context().lang.toLowerCase(); + ajax.jsonRpc('/website/seo_suggest', 'call', { + 'keywords': self.root, + 'lang': language, + }).then(function (keyword_list) { + self.addSuggestions(JSON.parse(keyword_list)); + }); + }, + addSuggestions: function (keywords) { + var self = this; + self.$el.empty(); + // TODO Improve algorithm + Ajust based on custom user keywords + var regex = new RegExp(self.root, "gi"); + var keywords = _.map(_.uniq(keywords), function (word) { + return word.replace(regex, "").trim(); + }); + // TODO Order properly ? + _.each(keywords, function (keyword) { + if (keyword) { + var suggestion = new Suggestion(self, { + root: self.root, + language: self.language, + keyword: keyword, + page: self.htmlPage, + }); + suggestion.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + suggestion.appendTo(self.$el); + } + }); + }, +}); + +var Keyword = Widget.extend({ + template: 'website.seo_keyword', + events: { + 'click a[data-action=remove-keyword]': 'destroy', + }, + maxWordsPerKeyword: 4, // TODO Check + init: function (parent, options) { + this.keyword = options.word; + this.language = options.language; + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + this.htmlPage.on('title-changed', this, this.updateLabel); + this.htmlPage.on('description-changed', this, this.updateLabel); + this.suggestionList = new SuggestionList(this, { + root: this.keyword, + language: this.language, + page: this.htmlPage, + }); + this.suggestionList.on('selected', this, function (word, language) { + this.trigger('selected', word, language); + }); + this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion')); + }, + analyze: function () { + return analyzeKeyword(this.htmlPage, this.keyword); + }, + highlight: function () { + return this.analyze().title; + }, + tooltip: function () { + return this.analyze().description; + }, + updateLabel: function () { + var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight(); + this.$(".js_seo_keyword").attr('class', cssClass); + this.$(".js_seo_keyword").attr('title', this.tooltip()); + }, + destroy: function () { + this.trigger('removed'); + this._super(); + }, +}); + +var KeywordList = Widget.extend({ + template: 'website.seo_list', + maxKeywords: 10, + init: function (parent, options) { + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + var self = this; + var existingKeywords = self.htmlPage.keywords(); + if (existingKeywords.length > 0) { + _.each(existingKeywords, function (word) { + self.add.call(self, word); + }); + } + }, + keywords: function () { + var result = []; + this.$('.js_seo_keyword').each(function () { + result.push($(this).data('keyword')); + }); + return result; + }, + isFull: function () { + return this.keywords().length >= this.maxKeywords; + }, + exists: function (word) { + return _.contains(this.keywords(), word); + }, + add: function (candidate, language) { + var self = this; + // TODO Refine + var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : ""; + if (word && !self.isFull() && !self.exists(word)) { + var keyword = new Keyword(self, { + word: word, + language: language, + page: this.htmlPage, + }); + keyword.on('removed', self, function () { + self.trigger('list-not-full'); + self.trigger('removed', word); + self.trigger('content-updated', true); + }); + keyword.on('selected', self, function (word, language) { + self.trigger('selected', word, language); + }); + keyword.appendTo(self.$el); + } + if (self.isFull()) { + self.trigger('list-full'); + } + self.trigger('content-updated'); + }, +}); + +var Image = Widget.extend({ + template: 'website.seo_image', + init: function (parent, options) { + this.src = options.src; + this.alt = options.alt; + this._super(parent); + }, +}); + +var ImageList = Widget.extend({ + init: function (parent, options) { + this.htmlPage = options.page; + this._super(parent); + }, + start: function () { + var self = this; + this.htmlPage.images().each(function (index, image) { + new Image(self, image).appendTo(self.$el); + }); + }, + images: function () { + var result = []; + this.$('input').each(function () { + var $input = $(this); + result.push({ + src: $input.attr('src'), + alt: $input.val(), + }); + }); + return result; + }, + add: function (image) { + new Image(this, image).appendTo(this.$el); + }, +}); + +var Preview = Widget.extend({ + template: 'website.seo_preview', + init: function (parent, options) { + this.title = options.title; + this.url = options.url; + this.description = options.description || "[ The description will be generated by google unless you specify one ]"; + this._super(parent); + }, +}); + +var HtmlPage = Class.extend(mixins.PropertiesMixin, { + url: function () { + var url = window.location.href; + var hashIndex = url.indexOf('#'); + return hashIndex >= 0 ? url.substring(0, hashIndex) : url; + }, + title: function () { + var $title = $('title'); + return ($title.length > 0) && $title.text() && $title.text().trim(); + }, + changeTitle: function (title) { + // TODO create tag if missing + $('title').text(title); + this.trigger('title-changed', title); + }, + description: function () { + var $description = $('meta[name=description]'); + return ($description.length > 0) && ($description.attr('content') && $description.attr('content').trim()); + }, + changeDescription: function (description) { + // TODO create tag if missing + $('meta[name=description]').attr('content', description); + this.trigger('description-changed', description); + }, + keywords: function () { + var $keywords = $('meta[name=keywords]'); + var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(","); + return (parsed && parsed[0]) ? parsed: []; + }, + changeKeywords: function (keywords) { + // TODO create tag if missing + $('meta[name=keywords]').attr('content', keywords.join(",")); + this.trigger('keywords-changed', keywords); + }, + headers: function (tag) { + return $('#wrap '+tag).map(function () { + return $(this).text(); + }); + }, + images: function () { + return $('#wrap img').map(function () { + var $img = $(this); + return { + src: $img.attr('src'), + alt: $img.attr('alt'), + }; + }); + }, + company: function () { + return $('html').attr('data-oe-company-name'); + }, + bodyText: function () { + return $('body').children().not('.js_seo_configuration').text(); + }, + isInBody: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.bodyText()); + }, + isInTitle: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.title()); + }, + isInDescription: function (text) { + return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.description()); + }, +}); + +var Tip = Widget.extend({ + template: 'website.seo_tip', + events: { + 'closed.bs.alert': 'destroy', + }, + init: function (parent, options) { + this.message = options.message; + // cf. http://getbootstrap.com/components/#alerts + // success, info, warning or danger + this.type = options.type || 'info'; + this._super(parent); + }, +}); + +var Configurator = Dialog.extend({ + template: 'website.seo_configuration', + events: { + 'keyup input[name=seo_page_keywords]': 'confirmKeyword', + 'blur input[name=seo_page_title]': 'titleChanged', + 'blur textarea[name=seo_page_description]': 'descriptionChanged', + 'click button[data-action=add]': 'addKeyword', + }, + canEditTitle: false, + canEditDescription: false, + canEditKeywords: false, + canEditLanguage: false, + maxTitleSize: 65, + maxDescriptionSize: 160, // TODO master: remove me and add warning + + init: function (parent, options) { + options = options || {}; + _.defaults(options, { + title: _t('Promote This Page'), + subtitle: _t('Get this page efficiently referenced in Google to attract more visitors.'), + buttons: [ + {text: _t('Save'), classes: 'btn-primary', click: this.update}, + {text: _t('Discard'), close: true}, + ], + }); + + this._super(parent, options); + }, + start: function () { + var self = this; + + this.$modal.addClass('oe_seo_configuration js_seo_configuration'); + + this.htmlPage = new HtmlPage(); + this.$('.js_seo_page_url').text(this.htmlPage.url()); + this.$('input[name=seo_page_title]').val(this.htmlPage.title()); + this.$('textarea[name=seo_page_description]').val(this.htmlPage.description()); + + this.keywordList = new KeywordList(self, { page: this.htmlPage }); + this.keywordList.on('list-full', self, function () { + self.$('input[name=seo_page_keywords]').attr({ + readonly: "readonly", + placeholder: "Remove a keyword first" + }); + self.$('button[data-action=add]').prop('disabled', true).addClass('disabled'); + }); + this.keywordList.on('list-not-full', self, function () { + self.$('input[name=seo_page_keywords]').removeAttr('readonly').attr('placeholder', ""); + self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled'); + }); + this.keywordList.on('selected', self, function (word, language) { + self.keywordList.add(word, language); + }); + this.keywordList.on('content-updated', self, function (removed) { + self.updateTable(removed); + }); + this.keywordList.insertAfter(this.$('.table thead')); + this.disableUnsavableFields(); + this.renderPreview(); + this.getLanguages(); + this.updateTable(); + }, + getLanguages: function () { + var self = this; + ajax.jsonRpc('/web/dataset/call_kw', 'call', { + model: 'website', + method: 'get_languages', + args: [[base.get_context().website_id]], + kwargs: {context: base.get_context()} + }).then( function (data) { + self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { + 'language': data, + 'def_lang': base.get_context().lang + })); + }); + }, + disableUnsavableFields: function () { + var self = this; + this.loadMetaData().then(function (data) { + self.canEditTitle = data && ('website_meta_title' in data); + self.canEditDescription = data && ('website_meta_description' in data); + self.canEditKeywords = data && ('website_meta_keywords' in data); + if (!self.canEditTitle) { + self.$('input[name=seo_page_title]').attr('disabled', true); + } + if (!self.canEditDescription) { + self.$('textarea[name=seo_page_description]').attr('disabled', true); + } + if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) { + self.$footer.find('button[data-action=update]').attr('disabled', true); + } + }); + }, + suggestImprovements: function () { + var self = this; + var tips = []; + _.each(tips, function (tip) { + displayTip(tip.message, tip.type); + }); + + function displayTip(message, type) { + new Tip(self, { + message: message, + type: type, + }).appendTo(self.$('.js_seo_tips')); + } + }, + confirmKeyword: function (e) { + if (e.keyCode == 13) { + this.addKeyword(); + } + }, + addKeyword: function (word) { + var $input = this.$('input[name=seo_page_keywords]'); + var $language = this.$('select[name=seo_page_language]'); + var keyword = _.isString(word) ? word : $input.val(); + var language = $language.val().toLowerCase(); + this.keywordList.add(keyword, language); + $input.val("").focus(); + }, + update: function () { + var self = this; + var data = {}; + if (this.canEditTitle) { + data.website_meta_title = this.htmlPage.title(); + } + if (this.canEditDescription) { + data.website_meta_description = this.htmlPage.description(); + } + if (this.canEditKeywords) { + data.website_meta_keywords = this.keywordList.keywords().join(", "); + } + this.saveMetaData(data).then(function () { + self.close(); + }); + }, + getMainObject: function () { + var repr = $('html').data('main-object'); + var m = repr.match(/(.+)\((\d+),(.*)\)/); + if (!m) { + return null; + } else { + return { + model: m[1], + id: m[2]|0 + }; + } + }, + loadMetaData: function () { + var obj = this.getMainObject(); + var def = $.Deferred(); + if (!obj) { + // return $.Deferred().reject(new Error("No main_object was found.")); + def.resolve(null); + } else { + var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords']; + rpc.query({ + model: obj.model, + method: 'read', + args: [[obj.id], fields, base.get_context()], + }).then(function (data) { + if (data.length) { + var meta = data[0]; + meta.model = obj.model; + def.resolve(meta); + } else { + def.resolve(null); + } + }).fail(function () { + def.reject(); + }); + } + return def; + }, + saveMetaData: function (data) { + var obj = this.getMainObject(); + if (!obj) { + return $.Deferred().reject(); + } else { + return rpc.query({ + model: obj.model, + method: 'write', + args: [[obj.id], data, base.get_context()], + }); + } + }, + titleChanged: function () { + var self = this; + _.defer(function () { + var title = self.$('input[name=seo_page_title]').val(); + self.htmlPage.changeTitle(title); + self.renderPreview(); + }); + }, + descriptionChanged: function () { + var self = this; + _.defer(function () { + var description = self.$('textarea[name=seo_page_description]').val(); + self.htmlPage.changeDescription(description); + self.renderPreview(); + }); + }, + renderPreview: function () { + var preview = new Preview(this, { + title: this.htmlPage.title(), + description: this.htmlPage.description(), + url: this.htmlPage.url(), + }); + var $preview = this.$('.js_seo_preview'); + $preview.empty(); + preview.appendTo($preview); + }, + destroy: function () { + this.htmlPage.changeKeywords(this.keywordList.keywords()); + this._super.apply(this, arguments); + }, + updateTable : function (removed) { + var self = this, + val = removed ? (this.$el.find('tbody > tr').length - 1) : (this.$el.find('tbody > tr').length); + this.$('table').toggleClass('js_seo_has_content', val > 0 ); + this.$el.scrollTop(self.$el[0].scrollHeight); + }, +}); + +website.TopBar.include({ + start: function () { + var self = this; + this.$el.on('click', 'a[data-action=promote-current-page]', function () { + new Configurator(self).open(); + }); + return this._super(); + } +}); + +return { + Configurator: Configurator, +}; + +}); diff --git a/addons/website/static/src/js/menu/translate.js b/addons/website/static/src/js/menu/translate.js new file mode 100644 index 00000000000..c9b7de4ed8b --- /dev/null +++ b/addons/website/static/src/js/menu/translate.js @@ -0,0 +1,104 @@ +odoo.define('website.translator', function (require) { +'use strict'; + +var core = require('web.core'); +var ajax = require('web.ajax'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var translate = require('web_editor.translate'); +var website = require('website.website'); +var local_storage = require('web.local_storage'); +var utils = require('web.utils'); + +var qweb = core.qweb; + +if (!translate.translatable) { + return; +} + + +website.TopBar.include({ + events: _.extend({}, website.TopBar.prototype.events, { + 'click [data-action="edit_master"]': 'edit_master', + 'click [data-action="translate"]': 'translate', + }), + translate: function (ev) { + ev.preventDefault(); + if (translate.edit_translations) { + translate.instance.edit(); + } else { + location.search += '&edit_translations'; + } + }, + edit_master: function (ev) { + ev.preventDefault(); + + var lang = '/' + utils.get_cookie('frontend_lang'); + + var current = document.createElement('a'); + current.href = window.location.toString(); + current.search += (current.search ? '&' : '?') + 'enable_editor=1'; + if (current.pathname.indexOf(lang) === 0) { + current.pathname = current.pathname.replace(lang, ''); + } + + var link = document.createElement('a'); + link.href = '/website/lang/default'; + link.search += (link.search ? '&' : '?') + 'r=' + encodeURIComponent(current.pathname + current.search + current.hash); + + window.location = link.href; + }, +}); + + +if (!translate.edit_translations) { + return; +} + +ajax.loadXML('/website/static/src/xml/website.translator.xml', qweb); + +var nodialog = 'website_translator_nodialog'; + +var Translate = translate.Class.include({ + onTranslateReady: function () { + if(this.gengo_translate) { + this.translation_gengo_display(); + } + this._super(); + }, + edit: function () { + $("#oe_main_menu_navbar").hide(); + if (!local_storage.getItem(nodialog)) { + var dialog = new TranslatorDialog(); + dialog.appendTo($(document.body)); + dialog.on('activate', this, function () { + if (dialog.$('input[name=do_not_show]').prop('checked')) { + local_storage.removeItem(nodialog); + } else { + local_storage.setItem(nodialog, true); + } + dialog.$el.modal('hide'); + }); + } + return this._super(); + }, + cancel: function () { + $("#oe_main_menu_navbar").show(); + return this._super(); + } +}); + +var TranslatorDialog = Widget.extend({ + events: _.extend({}, website.TopBar.prototype.events, { + 'hidden.bs.modal': 'destroy', + 'click button[data-action=activate]': function (ev) { + this.trigger('activate'); + }, + }), + template: 'website.TranslatorDialog', + start: function () { + this.$el.modal(); + }, +}); + +}); diff --git a/addons/website/static/src/js/tours/banner.js b/addons/website/static/src/js/tours/banner.js new file mode 100644 index 00000000000..8c61c78a5ab --- /dev/null +++ b/addons/website/static/src/js/tours/banner.js @@ -0,0 +1,81 @@ +odoo.define("website.tour.banner", function (require) { + "use strict"; + + var core = require("web.core"); + var tour = require("web_tour.tour"); + var base = require("web_editor.base"); + + var _t = core._t; + + tour.register("banner", { + url: "/", + wait_for: base.ready(), + }, [{ + trigger: "a[data-action=edit]", + content: _t("Click Edit to start designing your homepage."), + extra_trigger: ".homepage", + position: "bottom", + }, { + trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail", + content: _t("Drag the Cover block and drop it in your page."), + position: "bottom", + run: "drag_and_drop", + }, { + trigger: "#wrapwrap .s_text_block_image_fw h2", + content: _t("Click on a text to start editing it. It's that easy to edit your content!"), + position: "left", + width: 150, + run: "text", + }, { + trigger: ".oe_overlay_options .oe_options", + extra_trigger: "#wrapwrap .s_text_block_image_fw h2:not(:containsExact(\"Headline\"))", + content: _t("Customize any block through this menu. Try to change the background of the banner."), + position: "bottom", + }, { + trigger: "#snippet_structure .oe_snippet:eq(3) .oe_snippet_thumbnail", + content: _t("Drag another block in your page, below the cover."), + position: "bottom", + run: "drag_and_drop", + }, { + trigger: "button[data-action=save]", + extra_trigger: ".oe_overlay_options .oe_options", + content: _t("Publish your page by clicking on the Save button."), + position: "bottom", + }, { + trigger: "a[data-action=show-mobile-preview]", + content: _t("Good Job! You created your first page. Let's check how this page looks like on mobile devices."), + position: "bottom", + }, { + trigger: ".modal-dialog:has(#mobile-viewport) button[data-dismiss=modal]", + content: _t("After having checked how it looks on mobile, close the preview."), + position: "right", + }, { + trigger: "#oe_main_menu_navbar a[data-action=new_page]", + content: _t("

    That's it. Your homepage is live.

    Continue adding more pages to your site or edit this page to make it even more awesome.

    "), + position: "bottom", + }]); +}); + + +odoo.define("website.tour.contact", function (require) { + "use strict"; + + var core = require("web.core"); + var tour = require("web_tour.tour"); + var base = require("web_editor.base"); + var _t = core._t; + + tour.register("contact", { + url: "/page/contactus", + wait_for: base.ready(), + }, [{ + trigger: "li#customize-menu", + content: _t("Install a contact form to improve this page."), + extra_trigger: "#o_contact_mail", + position: "bottom", + }, { + trigger: "li#install_apps", + content: _t("Install new apps to get more features. Let's install the 'Contact form' app."), + position: "bottom", + }]); +}); diff --git a/addons/website/static/src/js/tours/rte.js b/addons/website/static/src/js/tours/rte.js new file mode 100644 index 00000000000..d428c46fa7f --- /dev/null +++ b/addons/website/static/src/js/tours/rte.js @@ -0,0 +1,159 @@ +odoo.define('website.tour.rte', function (require) { +'use strict'; + +var tour = require("web_tour.tour"); +var base = require("web_editor.base"); + +tour.register('rte_translator', { + test: true, + wait_for: base.ready() +}, + [ + { + content: "click on Add a language", + trigger: '.js_language_selector a:has(i.fa)', + }, { + content: "select french", + trigger: 'select[name="lang"]', + run: 'text "fr_BE"', + }, { + content: "load french", + trigger: '.modal-footer button:first', + extra_trigger: '.modal select[name="lang"]:propValueContains(fr_BE)', + }, { + content: "go to english version", + trigger: '.js_language_selector a[data-lang="en_US"]', + extra_trigger: 'html[lang*="fr"]', + timeout: 60000, + }, { + content: "Open new page menu", + trigger: '#oe_main_menu_navbar a[data-action=new_page]', + extra_trigger: 'a[data-action="edit"]', + }, { + content: "click on new page", + trigger: 'a[data-action="new_page"]', + }, { + content: "insert page name", + trigger: '#editor_new_page input[type="text"]', + run: 'text rte_translator', + }, { + content: "create page", + trigger: 'button.btn-continue', + extra_trigger: 'input[type="text"]:propValue(rte_translator)', + }, { + content: "drop a snippet", + trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail", + run: 'drag_and_drop', + }, { + content: "change content", + trigger: '.oe_overlay_options .oe_options:visible', + run: function () { + $("#wrap p:first").replaceWith('

    Write one or two paragraphs describing your product or\ + services. To be successful your content needs to be\ + useful to your readers.

    \ +

    <b></b> is an HTML tag & is empty

    '); + $("#wrap img").attr("title", "test translate image title"); + } + }, { + content: "save", + trigger: 'button[data-action=save]', + extra_trigger: '#wrap p:first b', + }, { + content: "click on french version", + trigger: '.js_language_selector a[data-lang="fr_BE"]', + extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', + }, { + content: "translate", + trigger: 'html:not(:has(#wrap p span)) .o_menu_systray a[data-action="translate"]', + }, { + content: "close modal", + trigger: 'button[data-action="activate"]', + }, { + content: "check if translation is activate", + trigger: '[data-oe-translation-id]', + }, { + content: "translate text", + trigger: '#wrap p font:first', + run: function (action_helper) { + action_helper.text('translated french text'); + this.$anchor.trigger('keydown'); + }, + }, { + content: "translate text with special char", + trigger: '#wrap input + p span:first', + run: function (action_helper) { + action_helper.click(); + this.$anchor.prepend('<{translated}>').trigger('keydown'); + }, + }, { + content: "click on input", + trigger: 'input:first', + extra_trigger: '#wrap .o_dirty font:first:contains(translated french text)', + run: 'click', + }, { + content: "translate placeholder", + trigger: 'input:first', + run: 'text test french placeholder', + }, { + content: "close modal", + trigger: 'button.o_save_button', + extra_trigger: '.modal input:propValue(test french placeholder)', + }, { + content: "save translation", + trigger: 'button[data-action=save]', + }, { + content: "check: content is translated", + trigger: '#wrap p font:first:contains(translated french text)', + run: function () {}, // it's a check + }, { + content: "check: content with special char is translated", + trigger: "#wrap input + p:contains(<{translated}> is an HTML\xa0tag & )", + run: function () {}, // it's a check + + }, { + content: "check: placeholder translation", + trigger: 'input[placeholder="test french placeholder"]', + run: function () {}, // it's a check + }, { + content: "return to english version", + trigger: '.js_language_selector a[data-lang="en_US"]', + extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', + }, { + content: "edit english version", + trigger: 'a[data-action=edit]', + extra_trigger: 'body:not(:has(#wrap p font:first:containsExact(paragraphs describing)))', + }, { + content: "select text", + extra_trigger: 'button[data-action=save]', + trigger: '#wrap p', + run: function (action_helper) { + action_helper.click(); + var el = this.$anchor[0]; + var evt = document.createEvent("MouseEvents"); + $.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select(); + evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); + el.dispatchEvent(evt); + }, + }, { + content: "underline", + trigger: '.note-air-popover button[data-event="underline"]', + }, { + content: "save new change", + trigger: 'button[data-action=save]', + extra_trigger: '#wrap p u', + }, { + content: "return in french", + trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]', + extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', + }, { + content: "check bis: content is translated", + trigger: '#wrap p font:first:contains(translated french text)', + extra_trigger: 'body:not(:has(button[data-action=save]))', + }, { + content: "check bis: placeholder translation", + trigger: 'input[placeholder="test french placeholder"]', + }, + ] +); + +}); diff --git a/addons/website/static/src/js/web_planner_website.js b/addons/website/static/src/js/web_planner_website.js deleted file mode 100644 index 59adfef92a0..00000000000 --- a/addons/website/static/src/js/web_planner_website.js +++ /dev/null @@ -1,41 +0,0 @@ -odoo.define('web.planner.website', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var planner = require('web.planner.common'); -var rpc = require('web.rpc'); -var session = require('web.session'); -var website = require('website.website'); - -var qweb = core.qweb; - -var WebsitePlannerLauncher = planner.PlannerLauncher.extend({ - _fetch_planner_data: function () { - return rpc.query({ - model: 'web.planner', - method: 'search_read', - args: [[['planner_application', '=', 'planner_website']]], - }) - .then((function (planner) { - if (!planner.length) return; - - planner[0].data = $.parseJSON(planner[0].data) || {}; - this._setup_for_planner(planner[0]); - }).bind(this)); - }, -}); - -if (session.is_system) { - website.TopBar.include({ - start: function () { - var websitePlannerLauncher = new WebsitePlannerLauncher(); - var def = ajax.loadXML('/web_planner/static/src/xml/web_planner.xml', qweb).then((function() { - return websitePlannerLauncher.prependTo(this.$(".o_menu_systray")); - }).bind(this)); - return $.when(this._super.apply(this, arguments), def); - }, - }); -} - -}); diff --git a/addons/website/static/src/js/website.ace.js b/addons/website/static/src/js/website.ace.js deleted file mode 100644 index 2ce486fc1c8..00000000000 --- a/addons/website/static/src/js/website.ace.js +++ /dev/null @@ -1,96 +0,0 @@ -odoo.define('website.ace', function (require) { -'use strict'; - -var Widget = require('web.Widget'); -var website = require('website.website'); -var mixins = require('web.mixins'); - -var ViewEditor = require('web_editor.ace'); - -var hash = "#advanced-view-editor"; - -var Ace = Widget.extend(mixins.ServiceProvider, { - events: { - "click a[data-action=ace]": function (e) { - e.preventDefault(); - this.launchAce(); - }, - }, - init: function () { - mixins.ServiceProvider.init.apply(this, arguments); - this._super.apply(this, arguments); - }, - start: function () { - if (window.location.hash.substr(0, hash.length) === hash) { - this.launchAce(); - } - return this._super.apply(this, arguments); - }, - launchAce: function () { - if (this.globalEditor) { - this.globalEditor.do_show(); - } else { - var currentHash = window.location.hash; - var indexOfView = currentHash.indexOf("?res="); - var initialResID = undefined; - if (indexOfView >= 0) { - initialResID = currentHash.substr(indexOfView + ("?res=".length)); - var parsedResID = parseInt(initialResID, 10); - if (parsedResID) { - initialResID = parsedResID; - } - } - - this.globalEditor = new ViewEditor(this, $(document.documentElement).data('view-xmlid'), { - initialResID: initialResID, - defaultBundlesRestriction: [ - "web.assets_frontend", - "website.assets_frontend", - ], - }); - this.globalEditor.appendTo(document.body); - - $("a[data-action=edit]").on("click", this.globalEditor.do_hide.bind(this.globalEditor)); - } - }, -}); - -/** - * Extend the default view editor so that the URL hash is updated with view id. - */ -ViewEditor = ViewEditor.extend({ - displayResource: function () { - this._super.apply(this, arguments); - this._updateHash(); - }, - saveResources: function () { - return this._super.apply(this, arguments).then((function () { - this._updateHash(); - window.location.reload(); - }).bind(this)); - }, - resetResource: function () { - return this._super.apply(this, arguments).then((function () { - window.location.reload(); - }).bind(this)); - }, - do_hide: function () { - this._super.apply(this, arguments); - window.location.hash = ""; - }, - _updateHash: function () { - window.location.hash = hash + "?res=" + this.selectedResource(); - }, -}); - -website.TopBar.include({ - start: function () { - this.ace = new Ace(); - return $.when( - this._super.apply(this, arguments), - this.ace.attachTo(this.$('#html_editor')) - ); - }, -}); - -}); diff --git a/addons/website/static/src/js/website.backend.js b/addons/website/static/src/js/website.backend.js deleted file mode 100644 index 5033d9a690c..00000000000 --- a/addons/website/static/src/js/website.backend.js +++ /dev/null @@ -1,51 +0,0 @@ -odoo.define('website.backend', function (require) { -"use strict"; - -var AbstractField = require('web.AbstractField'); -var core = require('web.core'); -var field_registry = require('web.field_registry'); - -var _t = core._t; - -var WidgetWebsiteButton = AbstractField.extend({ - template: 'WidgetWebsiteButton', - - //-------------------------------------------------------------------------- - // Public - //-------------------------------------------------------------------------- - - /** - * @override - */ - isSet: function () { - return true; - }, - - //-------------------------------------------------------------------------- - // Private - //-------------------------------------------------------------------------- - - /** - * @override - * @private - */ - _render: function () { - this._super.apply(this, arguments); - - var $value = this.$('.o_value'); - - if (this.value === true) { - $value.html(_t('Published')) - .removeClass('text-danger') - .addClass('text-success'); - } else { - $value.html(_t('Unpublished')) - .removeClass('text-success') - .addClass('text-danger'); - } - }, -}); - -field_registry.add('website_button', WidgetWebsiteButton); - -}); diff --git a/addons/website/static/src/js/website.backendDashboard.js b/addons/website/static/src/js/website.backendDashboard.js deleted file mode 100644 index 16b1281f222..00000000000 --- a/addons/website/static/src/js/website.backendDashboard.js +++ /dev/null @@ -1,603 +0,0 @@ -odoo.define('website.backendDashboard', function (require) { -"use strict"; - - -var ajax = require('web.ajax'); -var ControlPanelMixin = require('web.ControlPanelMixin'); -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var field_utils = require('web.field_utils'); -var session = require('web.session'); -var web_client = require('web.web_client'); -var Widget = require('web.Widget'); - -var local_storage = require('web.local_storage'); - -var _t = core._t; -var QWeb = core.qweb; - -var Dashboard = Widget.extend(ControlPanelMixin, { - template: "website.WebsiteDashboardMain", - events: { - 'click .js_link_analytics_settings': 'on_link_analytics_settings', - 'click .o_dashboard_action': 'on_dashboard_action', - 'click .o_dashboard_action_form': 'on_dashboard_action_form', - 'click .o_dashboard_hide_panel': 'on_dashboard_hide_panel', - }, - - init: function(parent, context) { - this._super(parent, context); - - this.date_range = 'week'; // possible values : 'week', 'month', year' - this.date_from = moment().subtract(1, 'week'); - this.date_to = moment(); - this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); - - this.dashboards_templates = ['website.dashboard_visits']; - this.graphs = []; - }, - - willStart: function() { - var self = this; - return this._super().then(function() { - return $.when( - self.fetch_data() - ); - }); - }, - - start: function() { - var self = this; - return this._super().then(function() { - self.update_cp(); - self.render_dashboards(); - self.render_graphs(); - self.$el.parent().addClass('oe_background_grey'); - }); - }, - - fetch_data: function() { - var self = this; - return ajax.jsonRpc('/website/fetch_dashboard_data', 'call', { - 'date_from': this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(), - 'date_to': this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(), - }).done(function(result) { - self.data = result; - self.dashboards_data = result.dashboards; - self.currency_id = result.currency_id; - self.groups = result.groups; - }); - }, - - on_link_analytics_settings: function(ev) { - ev.preventDefault(); - - var self = this; - var dialog = new Dialog(this, { - size: 'medium', - title: _t('Google Analytics'), - $content: QWeb.render('website.ga_dialog_content', { - ga_key: this.dashboards_data.visits.ga_client_id, - ga_analytics_key: this.dashboards_data.visits.ga_analytics_key, - }), - buttons: [ - { - text: _t("Save"), - classes: 'btn-primary', - close: true, - click: function() { - var ga_client_id = dialog.$el.find('input[name="ga_client_id"]').val(); - var ga_analytics_key = dialog.$el.find('input[name="ga_analytics_key"]').val(); - self.on_save_ga_client_id(ga_client_id, ga_analytics_key); - }, - }, - { - text: _t("Cancel"), - close: true, - }, - ], - }).open(); - }, - - on_save_ga_client_id: function(ga_client_id, ga_analytics_key) { - var self = this; - return this._rpc({ - route: '/website/dashboard/set_ga_data', - params: { - 'ga_client_id': ga_client_id, - 'ga_analytics_key': ga_analytics_key, - }, - }).then(function (result) { - if (result.error) { - self.do_warn(result.error.title, result.error.message); - return; - } - self.on_date_range_button('week'); - }); - }, - - render_dashboards: function() { - var self = this; - _.each(this.dashboards_templates, function(template) { - self.$('.o_website_dashboard_content').append(QWeb.render(template, {widget: self})); - }); - }, - - render_graph: function(div_to_display, chart_values) { - this.$(div_to_display).empty(); - - var self = this; - nv.addGraph(function() { - var chart = nv.models.lineChart() - .x(function(d) { return self.getDate(d); }) - .y(function(d) { return self.getValue(d); }) - .forceY([0]); - chart - .useInteractiveGuideline(true) - .showLegend(false) - .showYAxis(true) - .showXAxis(true); - - var tick_values = self.getPrunedTickValues(chart_values[0].values, 5); - - chart.xAxis - .tickFormat(function(d) { return d3.time.format("%m/%d/%y")(new Date(d)); }) - .tickValues(_.map(tick_values, function(d) { return self.getDate(d); })) - .rotateLabels(-45); - - chart.yAxis - .tickFormat(d3.format('.02f')); - - var svg = d3.select(div_to_display) - .append("svg"); - - svg - .attr("height", '24em') - .datum(chart_values) - .call(chart); - - nv.utils.windowResize(chart.update); - return chart; - }); - }, - - render_graphs: function() { - var self = this; - _.each(this.graphs, function(e) { - if (self.groups[e.group]) { - self.render_graph('#o_graph_' + e.name, self.dashboards_data[e.name].graph); - } - }); - this.render_graph_analytics(this.dashboards_data.visits.ga_client_id); - }, - - render_graph_analytics: function(client_id) { - if (!this.dashboards_data.visits || !this.dashboards_data.visits.ga_client_id) { - return; - } - - this.load_analytics_api(); - - var $analytics_components = this.$('.js_analytics_components'); - this.addLoader($analytics_components); - - var self = this; - gapi.analytics.ready(function() { - - $analytics_components.empty(); - // 1. Authorize component - var $analytics_auth = $('
    ').addClass('col-md-12'); - gapi.analytics.auth.authorize({ - container: $analytics_auth[0], - clientid: client_id - }); - $analytics_auth.appendTo($analytics_components); - - self.handle_analytics_auth($analytics_components); - gapi.analytics.auth.on('signIn', function() { - self.handle_analytics_auth($analytics_components); - }); - - }); - }, - - on_date_range_button: function(date_range) { - if (date_range === 'week') { - this.date_range = 'week'; - this.date_from = moment().subtract(1, 'weeks'); - } else if (date_range === 'month') { - this.date_range = 'month'; - this.date_from = moment().subtract(1, 'months'); - } else if (date_range === 'year') { - this.date_range = 'year'; - this.date_from = moment().subtract(1, 'years'); - } else { - console.log('Unknown date range. Choose between [week, month, year]'); - return; - } - - var self = this; - $.when(this.fetch_data()).then(function() { - self.$('.o_website_dashboard_content').empty(); - self.render_dashboards(); - self.render_graphs(); - }); - - }, - - on_reverse_breadcrumb: function() { - web_client.do_push_state({}); - this.update_cp(); - }, - - on_dashboard_action: function (ev) { - ev.preventDefault(); - var $action = $(ev.currentTarget); - var additional_context = {}; - if (this.date_range === 'week') { - additional_context = {'search_default_week': true} - } else if (this.date_range === 'month') { - additional_context = {'search_default_month': true} - } else if (this.date_range === 'year') { - additional_context = {'search_default_year': true} - } - this.do_action($action.attr('name'), { - additional_context: additional_context, - on_reverse_breadcrumb: this.on_reverse_breadcrumb - }); - }, - - on_dashboard_action_form: function (ev) { - ev.preventDefault(); - var $action = $(ev.currentTarget); - this.do_action({ - name: $action.attr('name'), - res_model: $action.data('res_model'), - res_id: $action.data('res_id'), - views: [[false, 'form']], - type: 'ir.actions.act_window', - }, { - on_reverse_breadcrumb: this.on_reverse_breadcrumb - }); - }, - - on_dashboard_hide_panel: function (ev) { - ev.preventDefault(); - ev.stopPropagation(); - var $action = $(ev.currentTarget); - // update hidden module list - this.hidden_apps = JSON.parse(local_storage.getItem('website_dashboard_hidden_app_ids') || '[]'); - this.hidden_apps.push(JSON.parse($action.data('module_id'))); - local_storage.setItem('website_dashboard_hidden_app_ids', JSON.stringify(this.hidden_apps)); - // remove box - $action.closest(".o_box_item").remove(); - }, - - update_cp: function() { - var self = this; - if (!this.$searchview) { - this.$searchview = $(QWeb.render("website.DateRangeButtons", { - widget: this, - })); - this.$searchview.click('button.js_date_range', function(ev) { - self.on_date_range_button($(ev.target).data('date')); - $(this).find('button.js_date_range.active').removeClass('active'); - $(ev.target).addClass('active'); - }); - } - this.update_control_panel({ - cp_content: { - $searchview: this.$searchview, - }, - breadcrumbs: this.getParent().get_breadcrumbs(), - }); - }, - - // Loads Analytics API - load_analytics_api: function() { - var self = this; - if (!("gapi" in window)) { - (function(w,d,s,g,js,fjs){ - g=w.gapi||(w.gapi={});g.analytics={q:[],ready:function(cb){this.q.push(cb);}}; - js=d.createElement(s);fjs=d.getElementsByTagName(s)[0]; - js.src='https://apis.google.com/js/platform.js'; - fjs.parentNode.insertBefore(js,fjs);js.onload=function(){g.load('analytics');}; - }(window,document,'script')); - gapi.analytics.ready(function() { - self.analytics_create_components(); - }); - } - }, - - handle_analytics_auth: function($analytics_components) { - $analytics_components.find('.js_unauthorized_message').remove(); - - // Check if the user is authenticated and has the right to make API calls - if (!gapi.analytics.auth.getAuthResponse()) { - this.display_unauthorized_message($analytics_components, 'not_connected'); - } else if (gapi.analytics.auth.getAuthResponse() && gapi.analytics.auth.getAuthResponse().scope.indexOf('https://www.googleapis.com/auth/analytics') === -1) { - this.display_unauthorized_message($analytics_components, 'no_right'); - } else { - this.make_analytics_calls($analytics_components); - } - }, - - display_unauthorized_message: function($analytics_components, reason) { - $analytics_components.prepend($(QWeb.render('website.unauthorized_analytics', {reason: reason}))); - }, - - make_analytics_calls: function($analytics_components) { - // 2. ActiveUsers component - var $analytics_users = $('
    '); - var activeUsers = new gapi.analytics.ext.ActiveUsers({ - container: $analytics_users[0], - pollingInterval: 10, - }); - $analytics_users.appendTo($analytics_components); - - // 3. View Selector - var $analytics_view_selector = $('
    ').addClass('col-md-12 o_properties_selection'); - var viewSelector = new gapi.analytics.ViewSelector({ - container: $analytics_view_selector[0], - }); - viewSelector.execute(); - $analytics_view_selector.appendTo($analytics_components); - - // 4. Chart graph - var start_date = '7daysAgo'; - if (this.date_range === 'month') { - start_date = '30daysAgo'; - } else if (this.date_range === 'year') { - start_date = '365daysAgo'; - } - var $analytics_chart_2 = $('
    ').addClass('col-md-6 col-xs-12'); - var breakdownChart = new gapi.analytics.googleCharts.DataChart({ - query: { - 'dimensions': 'ga:date', - 'metrics': 'ga:sessions', - 'start-date': start_date, - 'end-date': 'yesterday' - }, - chart: { - type: 'LINE', - container: $analytics_chart_2[0], - options: { - title: 'All', - width: '100%' - } - } - }); - $analytics_chart_2.appendTo($analytics_components); - - // 5. Chart table - var $analytics_chart_1 = $('
    ').addClass('col-md-6 col-xs-12'); - var mainChart = new gapi.analytics.googleCharts.DataChart({ - query: { - 'dimensions': 'ga:medium', - 'metrics': 'ga:sessions', - 'sort': '-ga:sessions', - 'max-results': '6' - }, - chart: { - type: 'TABLE', - container: $analytics_chart_1[0], - options: { - width: '100%' - } - } - }); - $analytics_chart_1.appendTo($analytics_components); - - // Events handling & animations - - var table_row_listener; - - viewSelector.on('change', function(ids) { - var options = {query: {ids: ids}}; - activeUsers.set({ids: ids}).execute(); - mainChart.set(options).execute(); - breakdownChart.set(options).execute(); - - if (table_row_listener) { google.visualization.events.removeListener(table_row_listener); } - }); - - mainChart.on('success', function(response) { - var chart = response.chart; - var dataTable = response.dataTable; - - table_row_listener = google.visualization.events.addListener(chart, 'select', function() { - var options; - if (chart.getSelection().length) { - var row = chart.getSelection()[0].row; - var medium = dataTable.getValue(row, 0); - options = { - query: { - filters: 'ga:medium==' + medium, - }, - chart: { - options: { - title: medium, - } - } - }; - } else { - options = { - chart: { - options: { - title: 'All', - } - } - }; - delete breakdownChart.get().query.filters; - } - breakdownChart.set(options).execute(); - }); - }); - - // Add CSS animation to visually show the when users come and go. - activeUsers.once('success', function() { - var element = this.container.firstChild; - var timeout; - - this.on('change', function(data) { - element = this.container.firstChild; - var animationClass = data.delta > 0 ? 'is-increasing' : 'is-decreasing'; - element.className += (' ' + animationClass); - - clearTimeout(timeout); - timeout = setTimeout(function() { - element.className = element.className.replace(/ is-(increasing|decreasing)/g, ''); - }, 3000); - }); - }); - }, - - /* - * Credits to https://github.com/googleanalytics/ga-dev-tools - * This is the Active Users component that polls - * the number of active users on Analytics each 5 secs - */ - analytics_create_components: function() { - - gapi.analytics.createComponent('ActiveUsers', { - - initialize: function() { - this.activeUsers = 0; - gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); - }, - - execute: function() { - // Stop any polling currently going on. - if (this.polling_) { - this.stop(); - } - - this.render_(); - - // Wait until the user is authorized. - if (gapi.analytics.auth.isAuthorized()) { - this.pollActiveUsers_(); - } else { - gapi.analytics.auth.once('signIn', this.pollActiveUsers_.bind(this)); - } - }, - - stop: function() { - clearTimeout(this.timeout_); - this.polling_ = false; - this.emit('stop', {activeUsers: this.activeUsers}); - }, - - render_: function() { - var opts = this.get(); - - // Render the component inside the container. - this.container = typeof opts.container === 'string' ? - document.getElementById(opts.container) : opts.container; - - this.container.innerHTML = opts.template || this.template; - this.container.querySelector('b').innerHTML = this.activeUsers; - }, - - pollActiveUsers_: function() { - var options = this.get(); - var pollingInterval = (options.pollingInterval || 5) * 1000; - - if (isNaN(pollingInterval) || pollingInterval < 5000) { - throw new Error('Frequency must be 5 seconds or more.'); - } - - this.polling_ = true; - gapi.client.analytics.data.realtime - .get({ids:options.ids, metrics:'rt:activeUsers'}) - .then(function(response) { - var result = response.result; - var newValue = result.totalResults ? +result.rows[0][0] : 0; - var oldValue = this.activeUsers; - - this.emit('success', {activeUsers: this.activeUsers}); - - if (newValue !== oldValue) { - this.activeUsers = newValue; - this.onChange_(newValue - oldValue); - } - - if (this.polling_) { - this.timeout_ = setTimeout(this.pollActiveUsers_.bind(this), pollingInterval); - } - }.bind(this)); - }, - - onChange_: function(delta) { - var valueContainer = this.container.querySelector('b'); - if (valueContainer) { valueContainer.innerHTML = this.activeUsers; } - - this.emit('change', {activeUsers: this.activeUsers, delta: delta}); - if (delta > 0) { - this.emit('increase', {activeUsers: this.activeUsers, delta: delta}); - } else { - this.emit('decrease', {activeUsers: this.activeUsers, delta: delta}); - } - }, - - handleSignOut_: function() { - this.stop(); - gapi.analytics.auth.once('signIn', this.handleSignIn_.bind(this)); - }, - - handleSignIn_: function() { - this.pollActiveUsers_(); - gapi.analytics.auth.once('signOut', this.handleSignOut_.bind(this)); - }, - - template: - '
    ' + - 'Active Users: ' + - '
    ' - - }); - }, - - // Utility functions - addLoader: function(selector) { - var loader = ''; - selector.html("
    " + loader + "
    "); - }, - getDate: function(d) { return new Date(d[0]); }, - getValue: function(d) { return d[1]; }, - getPrunedTickValues: function(ticks, nb_desired_ticks) { - var nb_values = ticks.length; - var keep_one_of = Math.max(1, Math.floor(nb_values / nb_desired_ticks)); - - return _.filter(ticks, function(d, i) { - return i % keep_one_of === 0; - }); - }, - format_number: function(value, type, digits, symbol) { - if (type === 'currency') { - return this.render_monetary_field(value, this.currency_id); - } else { - return field_utils.format[type](value || 0, {digits: digits}) + ' ' + symbol; - } - }, - render_monetary_field: function(value, currency_id) { - var currency = session.get_currency(currency_id); - var formatted_value = field_utils.format.float(value || 0, {digits: currency && currency.digits}); - if (currency) { - if (currency.position === "after") { - formatted_value += currency.symbol; - } else { - formatted_value = currency.symbol + formatted_value; - } - } - return formatted_value; - }, - -}); - -core.action_registry.add('backend_dashboard', Dashboard); - - -return Dashboard; - -}); diff --git a/addons/website/static/src/js/website.contentMenu.js b/addons/website/static/src/js/website.contentMenu.js deleted file mode 100644 index 70f35871e74..00000000000 --- a/addons/website/static/src/js/website.contentMenu.js +++ /dev/null @@ -1,361 +0,0 @@ -odoo.define('website.contentMenu', function (require) { -"use strict"; - -var core = require('web.core'); -var ajax = require('web.ajax'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var editor = require('web_editor.editor'); -var widget = require('web_editor.widget'); -var website = require('website.website'); - -var _t = core._t; -var qweb = core.qweb; - -ajax.loadXML('/website/static/src/xml/website.contentMenu.xml', qweb); - -var TopBarContent = Widget.extend({ - start: function () { - var self = this; - - // Add page modal + menu content event - this.$el.add($('#o_website_add_page_modal')).on('click', 'a[data-action]', function (e) { - e.preventDefault(); - self[$(this).data('action')](); - }); - - return this._super(); - }, - edit_menu: function (action_before_reload) { - var context = base.get_context(); - var def = $.Deferred(); - if ($("[data-content_menu_id]").length) { - var select = new SelectEditMenuDialog(); - select.on('save', this, function (root) { - def.resolve(root); - }); - select.open(); - } else { - def.resolve(null); - } - - return def.then(function (root_id) { - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website.menu', - method: 'get_tree', - args: [context.website_id, root_id], - kwargs: { - context: context - }, - }).then(function (menu) { - var dialog = new EditMenuDialog(this, {}, menu).open(); - dialog.on("save", null, function () { - $.when(action_before_reload && action_before_reload()).then(function () { - editor.reload(); - }); - }); - return dialog; - }); - }); - }, - new_page: function () { - website.prompt({ - id: "editor_new_page", - window_title: _t("New Page"), - input: _t("Page Title"), - init: function () { - var $group = this.$dialog.find("div.form-group"); - $group.removeClass("mb0"); - - var $add = $('
    ', {'class': 'form-group mb0'}) - .append($('', {'class': 'col-sm-offset-3 col-sm-9 text-left'}) - .append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add page in menu")}))); - $add.find('input').prop('checked', true); - $group.after($add); - } - }).then(function (val, field, $dialog) { - if (val) { - var url = '/website/add/' + encodeURIComponent(val); - if ($dialog.find('input[type="checkbox"]').is(':checked')) url +="?add_menu=1"; - document.location = url; - } - }); - }, - rename_page: function () { - var self = this; - var context = base.get_context(); - self.mo_id = self.getMainObject().id; - - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'page_search_dependencies', - args: [self.mo_id], - kwargs: { - context: context - }, - }).then(function (deps) { - website.prompt({ - id: "editor_rename_page", - window_title: _t("Rename This Page"), - dependencies: deps, - }, 'website.rename_page').then(function (val, field, $dialog) { - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'rename_page', - args: [ - self.mo_id, - val, - ], - kwargs: { - context: context - }, - }).then(function (new_name) { - window.location = "/page/" + encodeURIComponent(new_name); - }); - }); - }); - }, - delete_page: function () { - var self = this; - var context = base.get_context(); - self.mo_id = self.getMainObject().id; - - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'page_search_dependencies', - args: [self.mo_id], - kwargs: { - context: context, - }, - }).then(function (deps) { - website.prompt({ - id: "editor_delete_page", - window_title: _t("Delete Page"), - dependencies: deps, - init: function () { $('.btn-continue').prop("disabled", true); }, - }, 'website.delete_page').then(function (val, field, $dialog) { - - if ($dialog.find('input[type="checkbox"]').is(':checked')) { - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'delete_page', - args: [self.mo_id], - kwargs: { - context: context - }, - }).then(function () { - window.location = "/"; - }); - } - }); - }); - }, - getMainObject: function () { - var repr = $('html').data('main-object'); - var m = repr.match(/(.+)\((\d+),(.*)\)/); - if (!m) { - return null; - } else { - return { - model: m[1], - id: m[2]|0 - }; - } - } -}); - -website.TopBar.include({ - start: function () { - this.content_menu = new TopBarContent(); - var def = this.content_menu.attachTo($('.oe_content_menu')); - return $.when(this._super(), def); - } -}); - -var SelectEditMenuDialog = widget.Dialog.extend({ - template: 'website.contentMenu.dialog.select', - init: function (parent, options) { - var self = this; - self.roots = [{id: null, name: _t("Top Menu")}]; - $("[data-content_menu_id]").each(function () { - self.roots.push({id: $(this).data("content_menu_id"), name: $(this).attr("name")}); - }); - this._super(parent, _.extend({}, { - title: _t("Select a Menu"), - save_text: _t("Continue") - }, options || {})); - }, - save: function () { - this.final_data = parseInt(this.$el.find("select").val() || null); - this._super.apply(this, arguments); - } -}); - -var EditMenuDialog = widget.Dialog.extend({ - template: 'website.contentMenu.dialog.edit', - events: _.extend({}, widget.Dialog.prototype.events, { - 'click a.js_add_menu': 'add_menu', - 'click button.js_edit_menu': 'edit_menu', - 'click button.js_delete_menu': 'delete_menu', - }), - init: function (parent, options, menu) { - this.menu = menu; - this.root_menu_id = menu.id; - this.flat = this.flatenize(menu); - this.to_delete = []; - this._super(parent, _.extend({}, { - title: _t("Edit Menu"), - size: 'medium', - }, options || {})); - }, - start: function () { - var r = this._super.apply(this, arguments); - this.$('.oe_menu_editor').nestedSortable({ - listType: 'ul', - handle: 'div', - items: 'li', - maxLevels: 2, - toleranceElement: '> div', - forcePlaceholderSize: true, - opacity: 0.6, - placeholder: 'oe_menu_placeholder', - tolerance: 'pointer', - attribute: 'data-menu-id', - expression: '()(.+)', // nestedSortable takes the second match of an expression (*sigh*) - }); - return r; - }, - flatenize: function (node, dict) { - dict = dict || {}; - var self = this; - dict[node.id] = node; - node.children.forEach(function (child) { - self.flatenize(child, dict); - }); - return dict; - }, - add_menu: function () { - var self = this; - var dialog = new MenuEntryDialog(this, {menu_link_options: true}, undefined, {}); - dialog.on('save', this, function (link) { - var new_menu = { - id: _.uniqueId('new-'), - name: link.text, - url: link.url, - new_window: link.isNewWindow, - parent_id: false, - sequence: 0, - children: [], - }; - self.flat[new_menu.id] = new_menu; - self.$('.oe_menu_editor').append( - qweb.render('website.contentMenu.dialog.submenu', { submenu: new_menu })); - }); - dialog.open(); - }, - edit_menu: function (ev) { - var self = this; - var menu_id = $(ev.currentTarget).closest('[data-menu-id]').data('menu-id'); - var menu = self.flat[menu_id]; - if (menu) { - var dialog = new MenuEntryDialog(this, {}, undefined, menu); - dialog.on('save', this, function (link) { - var id = link.id; - var menu_obj = self.flat[id]; - _.extend(menu_obj, { - 'name': link.text, - 'url': link.url, - 'new_window': link.isNewWindow, - }); - var $menu = self.$('[data-menu-id="' + id + '"]'); - $menu.find('.js_menu_label').first().text(menu_obj.name); - }); - dialog.open(); - } else { - alert("Could not find menu entry"); - } - }, - delete_menu: function (ev) { - var $menu = $(ev.currentTarget).closest('[data-menu-id]'); - var mid = $menu.data('menu-id')|0; - if (mid) { - this.to_delete.push(mid); - } - $menu.remove(); - }, - save: function () { - var _super = this._super.bind(this); - var self = this; - var new_menu = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0}); - var levels = []; - var data = []; - var context = base.get_context(); - // Resequence, re-tree and remove useless data - new_menu.forEach(function (menu) { - if (menu.id) { - levels[menu.depth] = (levels[menu.depth] || 0) + 1; - var mobj = self.flat[menu.id]; - mobj.sequence = levels[menu.depth]; - mobj.parent_id = (menu.parent_id|0) || menu.parent_id || self.root_menu_id; - delete(mobj.children); - data.push(mobj); - } - }); - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website.menu', - method: 'save', - args: [[context.website_id], { data: data, to_delete: self.to_delete }], - kwargs: { - context: context - }, - }).then(function () { - return _super(); - }); - }, -}); - -var MenuEntryDialog = widget.LinkDialog.extend({ - init: function (parent, options, editor, data) { - data.text = data.name || ''; - data.isNewWindow = data.new_window; - this.data = data; - this.menu_link_options = options.menu_link_options; - return this._super.apply(this, arguments); - }, - start: function () { - var self = this; - this.$(".o_link_dialog_preview").remove(); - this.$(".window-new, .link-style").closest(".form-group").remove(); - this.$("label[for='o_link_dialog_label_input']").text(_t("Menu Label")); - if (this.menu_link_options) { // add menu link option only when adding new menu - this.$('#o_link_dialog_label_input').closest('.form-group').after(qweb.render('website.contentMenu.dialog.edit.link_menu_options')); - this.$('input[name=link_menu_options]').on('change', function() { - self.$('#o_link_dialog_url_input').closest('.form-group').toggle(); - }); - } - this.$modal.find('.modal-lg').removeClass('modal-lg') - .find('.col-md-8').removeClass('col-md-8').addClass('col-xs-12'); - - return this._super.apply(this, arguments); - }, - save: function () { - var $e = this.$('#o_link_dialog_label_input'); - if (!$e.val() || !$e[0].checkValidity()) { - $e.closest('.form-group').addClass('has-error'); - $e.focus(); - return; - } - if (this.$('input[name=link_menu_options]:checked').val() === 'new_page') { - window.location = '/website/add/' + encodeURIComponent($e.val()) + '?add_menu=1'; - return; - } - return this._super.apply(this, arguments); - } -}); - -return { - 'TopBar': TopBarContent, - 'EditMenuDialog': EditMenuDialog, -}; - -}); diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js deleted file mode 100644 index fb2e03f3aa8..00000000000 --- a/addons/website/static/src/js/website.editor.js +++ /dev/null @@ -1,151 +0,0 @@ -odoo.define('website.editor', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var editor = require('web_editor.editor'); -var widget = require('web_editor.widget'); -var website = require('website.website'); - -var qweb = core.qweb; -var _t = core._t; - -ajax.loadXML('/website/static/src/xml/website.editor.xml', qweb); - -/** - * Object who contains all method and bind for the top bar, the template is create server side. - */ -website.TopBar.include({ - events: { - 'click [data-action="edit"]': 'edit', - }, - start: function () { - $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]:empty") - .filter(".oe_not_editable") - .filter(".oe_no_empty") - .addClass("oe_empty"); - - if (location.search.indexOf("enable_editor") >= 0 && $('html').attr('lang').match(/en[-_]US/)) { - this.$el.addClass('editing_mode'); - this.delayed_hide(); - } - - return this._super.apply(this, arguments); - }, - edit: function () { - this.$('button[data-action=edit]').prop('disabled', true); - this.$el.addClass('editing_mode'); - editor.editor_bar = new editor.Class(this); - editor.editor_bar.prependTo(document.body); - $('.o_homepage_editor_welcome_message').remove(); - - this.delayed_hide(); - }, - delayed_hide: function () { - _.delay((function () { - this.do_hide(); - }).bind(this), 800); - }, -}); - -/* ----- Customize Template ---- */ - -website.TopBarCustomize = Widget.extend({ - events: { - 'click > ul a[data-view-id]': 'do_customize', - }, - start: function () { - this.view_name = $(document.documentElement).data('view-xmlid'); - if (!this.view_name) { - this.$el.hide(); - } - - if (this.$el.is(".open")) { - this.load_menu(); - } else { - this.$el.one("mousedown", "> a.dropdown-toggle", this.load_menu.bind(this)); - } - }, - load_menu: function () { - var $menu = this.$el.children("ul"); - ajax.jsonRpc('/website/get_switchable_related_views', 'call', { - key: this.view_name, - }).then(function (result) { - var current_group = ""; - _.each(result, function (item) { - if (current_group !== item.inherit_id[1]) { - current_group = item.inherit_id[1]; - $menu.append("
  • " + current_group + "
  • "); - } - var $li = $('
  • ', {role: 'presentation'}) - .append($('', {href: '#', 'data-view-id': item.id, role: 'menuitem'}) - .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name}))); - $li.find('input').prop('checked', !!item.active); - $menu.append($li); - }); - }); - }, - do_customize: function (e) { - e.preventDefault(); - var view_id = $(e.currentTarget).data('view-id'); - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'ir.ui.view', - method: 'toggle', - args: [[parseInt(view_id, 10)]], - kwargs: {context: base.get_context()} - }).then(function () { - window.location.reload(); - }); - }, -}); -website.TopBar.include({ - start: function () { - this.customize_menu = new website.TopBarCustomize(); - var def = this.customize_menu.attachTo($('#customize-menu')); - return $.when(this._super(), def); - } -}); - -/* ----- EDITOR: LINK & MEDIA ---- */ - -widget.LinkDialog.include({ - bind_data: function () { - this.$( "#o_link_dialog_url_input" ).autocomplete({ - source: function (request, response) { - return ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'search_pages', - args: [null, request.term], - kwargs: { - limit: 15, - context: base.get_context(), - }, - }).then(function (exists) { - var rs=_.map(exists, function (r) { - return r.loc; - }); - response(rs); - }); - } - }); - return this._super(); - } -}); - -/** - * Display a welcome message on the homepage when it is empty and that the user is connected. - */ -base.ready().then(function () { - if (location.search.indexOf("enable_editor") < 0 && $(".editor_enable").length === 0) { - var $wrap = $("#wrapwrap.homepage #wrap"); - if ($wrap.length && $wrap.html().trim() === "") { - var $welcome_message = $(qweb.render("website.homepage_editor_welcome_message")); - $welcome_message.css("min-height", $wrap.parent("main").height() - ($wrap.outerHeight(true) - $wrap.height())); - $wrap.empty().append($welcome_message); - } - } -}); - -}); diff --git a/addons/website/static/src/js/website.mobile.js b/addons/website/static/src/js/website.mobile.js deleted file mode 100644 index 791775f57b8..00000000000 --- a/addons/website/static/src/js/website.mobile.js +++ /dev/null @@ -1,49 +0,0 @@ -odoo.define('website.mobile', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var Dialog = require('web.Dialog'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var website = require('website.website'); - -var _t = core._t; - -var MobilePreviewDialog = Dialog.extend({ - template: 'website.mobile_preview', - - init: function () { - this._super.apply(this, arguments); - this.mobile_src = $.param.querystring(window.location.href, 'mobilepreview'); - }, - - start: function () { - var self = this; - this.$modal.addClass('oe_mobile_preview'); - this.$modal.on('click', '.modal-header', function () { - self.$el.toggleClass('o_invert_orientation'); - }); - this.$iframe = this.$('iframe'); - this.$iframe.on('load', function (e) { - self.$iframe.contents().find('body').removeClass('o_connected_user'); - self.$iframe.contents().find('#oe_main_menu_navbar, #o_website_add_page_modal').remove(); - }); - - return this._super.apply(this, arguments); - }, -}); - -website.TopBar.include({ - start: function () { - var self = this; - this.$el.on('click', 'a[data-action=show-mobile-preview]', function () { - new MobilePreviewDialog(self, { - title: _t('Mobile preview') + " ", - }).open(); - }); - return this._super(); - }, -}); - -}); diff --git a/addons/website/static/src/js/website.seo.js b/addons/website/static/src/js/website.seo.js deleted file mode 100644 index 93b434a5f44..00000000000 --- a/addons/website/static/src/js/website.seo.js +++ /dev/null @@ -1,582 +0,0 @@ -odoo.define('website.seo', function (require) { -'use strict'; - -var core = require('web.core'); -var ajax = require('web.ajax'); -var Class = require('web.Class'); -var Dialog = require('web.Dialog'); -var mixins = require('web.mixins'); -var rpc = require("web.rpc"); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var website = require('website.website'); - -var _t = core._t; - -var qweb = core.qweb; - -ajax.loadXML('/website/static/src/xml/website.seo.xml', qweb); - - // This replaces \b, because accents(e.g. à, é) are not seen as word boundaries. - // Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b - var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)'; - -function analyzeKeyword(htmlPage, keyword) { - return htmlPage.isInTitle(keyword) ? { - title: 'label label-success', - description: "This keyword is used in the page title", - } : htmlPage.isInDescription(keyword) ? { - title: 'label label-primary', - description: "This keyword is used in the page description", - } : htmlPage.isInBody(keyword) ? { - title: 'label label-info', - description: "This keyword is used in the page content." - } : { - title: 'label label-default', - description: "This keyword is not used anywhere on the page." - }; -} - -var Suggestion = Widget.extend({ - template: 'website.seo_suggestion', - events: { - 'click .js_seo_suggestion': 'select', - }, - init: function (parent, options) { - this.root = options.root; - this.keyword = options.keyword; - this.language = options.language; - this.htmlPage = options.page; - this._super(parent); - }, - start: function () { - this.htmlPage.on('title-changed', this, this.renderElement); - this.htmlPage.on('description-changed', this, this.renderElement); - }, - analyze: function () { - return analyzeKeyword(this.htmlPage, this.keyword); - }, - highlight: function () { - return this.analyze().title; - }, - tooltip: function () { - return this.analyze().description; - }, - select: function () { - this.trigger('selected', this.keyword); - }, -}); - -var SuggestionList = Widget.extend({ - template: 'website.seo_suggestion_list', - init: function (parent, options) { - this.root = options.root; - this.language = options.language; - this.htmlPage = options.page; - this._super(parent); - }, - start: function () { - this.refresh(); - }, - refresh: function () { - var self = this; - self.$el.append(_t("Loading...")); - var language = self.language || base.get_context().lang.toLowerCase(); - ajax.jsonRpc('/website/seo_suggest', 'call', { - 'keywords': self.root, - 'lang': language, - }).then(function (keyword_list) { - self.addSuggestions(JSON.parse(keyword_list)); - }); - }, - addSuggestions: function (keywords) { - var self = this; - self.$el.empty(); - // TODO Improve algorithm + Ajust based on custom user keywords - var regex = new RegExp(self.root, "gi"); - var keywords = _.map(_.uniq(keywords), function (word) { - return word.replace(regex, "").trim(); - }); - // TODO Order properly ? - _.each(keywords, function (keyword) { - if (keyword) { - var suggestion = new Suggestion(self, { - root: self.root, - language: self.language, - keyword: keyword, - page: self.htmlPage, - }); - suggestion.on('selected', self, function (word, language) { - self.trigger('selected', word, language); - }); - suggestion.appendTo(self.$el); - } - }); - }, -}); - -var Keyword = Widget.extend({ - template: 'website.seo_keyword', - events: { - 'click a[data-action=remove-keyword]': 'destroy', - }, - maxWordsPerKeyword: 4, // TODO Check - init: function (parent, options) { - this.keyword = options.word; - this.language = options.language; - this.htmlPage = options.page; - this._super(parent); - }, - start: function () { - this.htmlPage.on('title-changed', this, this.updateLabel); - this.htmlPage.on('description-changed', this, this.updateLabel); - this.suggestionList = new SuggestionList(this, { - root: this.keyword, - language: this.language, - page: this.htmlPage, - }); - this.suggestionList.on('selected', this, function (word, language) { - this.trigger('selected', word, language); - }); - this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion')); - }, - analyze: function () { - return analyzeKeyword(this.htmlPage, this.keyword); - }, - highlight: function () { - return this.analyze().title; - }, - tooltip: function () { - return this.analyze().description; - }, - updateLabel: function () { - var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight(); - this.$(".js_seo_keyword").attr('class', cssClass); - this.$(".js_seo_keyword").attr('title', this.tooltip()); - }, - destroy: function () { - this.trigger('removed'); - this._super(); - }, -}); - -var KeywordList = Widget.extend({ - template: 'website.seo_list', - maxKeywords: 10, - init: function (parent, options) { - this.htmlPage = options.page; - this._super(parent); - }, - start: function () { - var self = this; - var existingKeywords = self.htmlPage.keywords(); - if (existingKeywords.length > 0) { - _.each(existingKeywords, function (word) { - self.add.call(self, word); - }); - } - }, - keywords: function () { - var result = []; - this.$('.js_seo_keyword').each(function () { - result.push($(this).data('keyword')); - }); - return result; - }, - isFull: function () { - return this.keywords().length >= this.maxKeywords; - }, - exists: function (word) { - return _.contains(this.keywords(), word); - }, - add: function (candidate, language) { - var self = this; - // TODO Refine - var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : ""; - if (word && !self.isFull() && !self.exists(word)) { - var keyword = new Keyword(self, { - word: word, - language: language, - page: this.htmlPage, - }); - keyword.on('removed', self, function () { - self.trigger('list-not-full'); - self.trigger('removed', word); - self.trigger('content-updated', true); - }); - keyword.on('selected', self, function (word, language) { - self.trigger('selected', word, language); - }); - keyword.appendTo(self.$el); - } - if (self.isFull()) { - self.trigger('list-full'); - } - self.trigger('content-updated'); - }, -}); - -var Image = Widget.extend({ - template: 'website.seo_image', - init: function (parent, options) { - this.src = options.src; - this.alt = options.alt; - this._super(parent); - }, -}); - -var ImageList = Widget.extend({ - init: function (parent, options) { - this.htmlPage = options.page; - this._super(parent); - }, - start: function () { - var self = this; - this.htmlPage.images().each(function (index, image) { - new Image(self, image).appendTo(self.$el); - }); - }, - images: function () { - var result = []; - this.$('input').each(function () { - var $input = $(this); - result.push({ - src: $input.attr('src'), - alt: $input.val(), - }); - }); - return result; - }, - add: function (image) { - new Image(this, image).appendTo(this.$el); - }, -}); - -var Preview = Widget.extend({ - template: 'website.seo_preview', - init: function (parent, options) { - this.title = options.title; - this.url = options.url; - this.description = options.description || "[ The description will be generated by google unless you specify one ]"; - this._super(parent); - }, -}); - -var HtmlPage = Class.extend(mixins.PropertiesMixin, { - url: function () { - var url = window.location.href; - var hashIndex = url.indexOf('#'); - return hashIndex >= 0 ? url.substring(0, hashIndex) : url; - }, - title: function () { - var $title = $('title'); - return ($title.length > 0) && $title.text() && $title.text().trim(); - }, - changeTitle: function (title) { - // TODO create tag if missing - $('title').text(title); - this.trigger('title-changed', title); - }, - description: function () { - var $description = $('meta[name=description]'); - return ($description.length > 0) && ($description.attr('content') && $description.attr('content').trim()); - }, - changeDescription: function (description) { - // TODO create tag if missing - $('meta[name=description]').attr('content', description); - this.trigger('description-changed', description); - }, - keywords: function () { - var $keywords = $('meta[name=keywords]'); - var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(","); - return (parsed && parsed[0]) ? parsed: []; - }, - changeKeywords: function (keywords) { - // TODO create tag if missing - $('meta[name=keywords]').attr('content', keywords.join(",")); - this.trigger('keywords-changed', keywords); - }, - headers: function (tag) { - return $('#wrap '+tag).map(function () { - return $(this).text(); - }); - }, - images: function () { - return $('#wrap img').map(function () { - var $img = $(this); - return { - src: $img.attr('src'), - alt: $img.attr('alt'), - }; - }); - }, - company: function () { - return $('html').attr('data-oe-company-name'); - }, - bodyText: function () { - return $('body').children().not('.js_seo_configuration').text(); - }, - isInBody: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.bodyText()); - }, - isInTitle: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.title()); - }, - isInDescription: function (text) { - return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.description()); - }, -}); - -var Tip = Widget.extend({ - template: 'website.seo_tip', - events: { - 'closed.bs.alert': 'destroy', - }, - init: function (parent, options) { - this.message = options.message; - // cf. http://getbootstrap.com/components/#alerts - // success, info, warning or danger - this.type = options.type || 'info'; - this._super(parent); - }, -}); - -var Configurator = Dialog.extend({ - template: 'website.seo_configuration', - events: { - 'keyup input[name=seo_page_keywords]': 'confirmKeyword', - 'blur input[name=seo_page_title]': 'titleChanged', - 'blur textarea[name=seo_page_description]': 'descriptionChanged', - 'click button[data-action=add]': 'addKeyword', - }, - canEditTitle: false, - canEditDescription: false, - canEditKeywords: false, - canEditLanguage: false, - maxTitleSize: 65, - maxDescriptionSize: 160, // TODO master: remove me and add warning - - init: function (parent, options) { - options = options || {}; - _.defaults(options, { - title: _t('Promote This Page'), - subtitle: _t('Get this page efficiently referenced in Google to attract more visitors.'), - buttons: [ - {text: _t('Save'), classes: 'btn-primary', click: this.update}, - {text: _t('Discard'), close: true}, - ], - }); - - this._super(parent, options); - }, - start: function () { - var self = this; - - this.$modal.addClass('oe_seo_configuration js_seo_configuration'); - - this.htmlPage = new HtmlPage(); - this.$('.js_seo_page_url').text(this.htmlPage.url()); - this.$('input[name=seo_page_title]').val(this.htmlPage.title()); - this.$('textarea[name=seo_page_description]').val(this.htmlPage.description()); - - this.keywordList = new KeywordList(self, { page: this.htmlPage }); - this.keywordList.on('list-full', self, function () { - self.$('input[name=seo_page_keywords]').attr({ - readonly: "readonly", - placeholder: "Remove a keyword first" - }); - self.$('button[data-action=add]').prop('disabled', true).addClass('disabled'); - }); - this.keywordList.on('list-not-full', self, function () { - self.$('input[name=seo_page_keywords]').removeAttr('readonly').attr('placeholder', ""); - self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled'); - }); - this.keywordList.on('selected', self, function (word, language) { - self.keywordList.add(word, language); - }); - this.keywordList.on('content-updated', self, function (removed) { - self.updateTable(removed); - }); - this.keywordList.insertAfter(this.$('.table thead')); - this.disableUnsavableFields(); - this.renderPreview(); - this.getLanguages(); - this.updateTable(); - }, - getLanguages: function () { - var self = this; - ajax.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'get_languages', - args: [[base.get_context().website_id]], - kwargs: {context: base.get_context()} - }).then( function (data) { - self.$('#language-box').html(core.qweb.render('Configurator.language_promote', { - 'language': data, - 'def_lang': base.get_context().lang - })); - }); - }, - disableUnsavableFields: function () { - var self = this; - this.loadMetaData().then(function (data) { - self.canEditTitle = data && ('website_meta_title' in data); - self.canEditDescription = data && ('website_meta_description' in data); - self.canEditKeywords = data && ('website_meta_keywords' in data); - if (!self.canEditTitle) { - self.$('input[name=seo_page_title]').attr('disabled', true); - } - if (!self.canEditDescription) { - self.$('textarea[name=seo_page_description]').attr('disabled', true); - } - if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) { - self.$footer.find('button[data-action=update]').attr('disabled', true); - } - }); - }, - suggestImprovements: function () { - var self = this; - var tips = []; - _.each(tips, function (tip) { - displayTip(tip.message, tip.type); - }); - - function displayTip(message, type) { - new Tip(self, { - message: message, - type: type, - }).appendTo(self.$('.js_seo_tips')); - } - }, - confirmKeyword: function (e) { - if (e.keyCode == 13) { - this.addKeyword(); - } - }, - addKeyword: function (word) { - var $input = this.$('input[name=seo_page_keywords]'); - var $language = this.$('select[name=seo_page_language]'); - var keyword = _.isString(word) ? word : $input.val(); - var language = $language.val().toLowerCase(); - this.keywordList.add(keyword, language); - $input.val("").focus(); - }, - update: function () { - var self = this; - var data = {}; - if (this.canEditTitle) { - data.website_meta_title = this.htmlPage.title(); - } - if (this.canEditDescription) { - data.website_meta_description = this.htmlPage.description(); - } - if (this.canEditKeywords) { - data.website_meta_keywords = this.keywordList.keywords().join(", "); - } - this.saveMetaData(data).then(function () { - self.close(); - }); - }, - getMainObject: function () { - var repr = $('html').data('main-object'); - var m = repr.match(/(.+)\((\d+),(.*)\)/); - if (!m) { - return null; - } else { - return { - model: m[1], - id: m[2]|0 - }; - } - }, - loadMetaData: function () { - var obj = this.getMainObject(); - var def = $.Deferred(); - if (!obj) { - // return $.Deferred().reject(new Error("No main_object was found.")); - def.resolve(null); - } else { - var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords']; - rpc.query({ - model: obj.model, - method: 'read', - args: [[obj.id], fields, base.get_context()], - }).then(function (data) { - if (data.length) { - var meta = data[0]; - meta.model = obj.model; - def.resolve(meta); - } else { - def.resolve(null); - } - }).fail(function () { - def.reject(); - }); - } - return def; - }, - saveMetaData: function (data) { - var obj = this.getMainObject(); - if (!obj) { - return $.Deferred().reject(); - } else { - return rpc.query({ - model: obj.model, - method: 'write', - args: [[obj.id], data, base.get_context()], - }); - } - }, - titleChanged: function () { - var self = this; - _.defer(function () { - var title = self.$('input[name=seo_page_title]').val(); - self.htmlPage.changeTitle(title); - self.renderPreview(); - }); - }, - descriptionChanged: function () { - var self = this; - _.defer(function () { - var description = self.$('textarea[name=seo_page_description]').val(); - self.htmlPage.changeDescription(description); - self.renderPreview(); - }); - }, - renderPreview: function () { - var preview = new Preview(this, { - title: this.htmlPage.title(), - description: this.htmlPage.description(), - url: this.htmlPage.url(), - }); - var $preview = this.$('.js_seo_preview'); - $preview.empty(); - preview.appendTo($preview); - }, - destroy: function () { - this.htmlPage.changeKeywords(this.keywordList.keywords()); - this._super.apply(this, arguments); - }, - updateTable : function (removed) { - var self = this, - val = removed ? (this.$el.find('tbody > tr').length - 1) : (this.$el.find('tbody > tr').length); - this.$('table').toggleClass('js_seo_has_content', val > 0 ); - this.$el.scrollTop(self.$el[0].scrollHeight); - }, -}); - -website.TopBar.include({ - start: function () { - var self = this; - this.$el.on('click', 'a[data-action=promote-current-page]', function () { - new Configurator(self).open(); - }); - return this._super(); - } -}); - -return { - Configurator: Configurator, -}; - -}); diff --git a/addons/website/static/src/js/website.share.js b/addons/website/static/src/js/website.share.js deleted file mode 100644 index 4de4dd65117..00000000000 --- a/addons/website/static/src/js/website.share.js +++ /dev/null @@ -1,87 +0,0 @@ -odoo.define('website.share', function (require) { -"use strict"; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); - -var _t = core._t; -var qweb = core.qweb; - -ajax.loadXML('/website/static/src/xml/website.share.xml', qweb); - -var SocialShare = Widget.extend({ - template: 'website.social_hover', - /** - :param element: element to bind popover - :param social_list: list of social media strings available from - ['facebook','twitter', 'linkedin', 'google-plus'] - :param hashtags: string of hashtags for twitter - */ - init: function (parent) { - this._super.apply(this, arguments); - this.element = parent; - if (parent.data('social')) { - this.social_list = (parent.data('social')).split(); - } else { - this.social_list = ['facebook','twitter', 'linkedin', 'google-plus']; - } - this.hashtags = parent.data('hashtags') || ''; - this.renderElement(); - this.bind_events(); - }, - bind_events: function () { - $('.oe_social_facebook').click($.proxy(this.renderSocial, this, 'facebook')); - $('.oe_social_twitter').click($.proxy(this.renderSocial, this, 'twitter')); - $('.oe_social_linkedin').click($.proxy(this.renderSocial, this, 'linkedin')); - $('.oe_social_google-plus').click($.proxy(this.renderSocial, this, 'google-plus')); - }, - renderElement: function () { - this.$el.append( - qweb.render('website.social_hover', {medias: this.social_list})); - //we need to re-render the element on each hover; popover has the nasty habit of not hiding but completely removing its - // code from the page - //so the binding is lost if we simply trigger on hover. - this.element.popover({ - 'content': this.$el.html(), - 'placement': 'bottom', - 'container': this.element, - 'html': true, - 'trigger': 'manual', - 'animation': false, - }).popover("show").on("mouseleave", function () { - var self = this; - setTimeout(function () { - if (! $(".popover:hover").length) { - $(self).popover("destroy"); - } - }, 200); - }); - }, - renderSocial: function (social) { - var url = document.URL.split(/[?#]/)[0]; // get current url without query string - var title = document.title.split(" | ")[0]; // get the page title without the company name - var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag) - var social_network = { - 'facebook':'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url), - 'twitter': 'https://twitter.com/intent/tweet?original_referer=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url), - 'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title), - 'google-plus': 'https://plus.google.com/share?url=' + encodeURIComponent(url) - }; - if (! _.contains(_.keys(social_network), social)) return; - var window_height = 500, window_width = 500; - window.open(social_network[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + window_height + ',width=' + window_width); - }, -}); - -// Initialize all social_share links when ready -base.ready().done(function () { - $('.oe_social_share').mouseenter(function () { - new SocialShare($(this)); - }); -}); - -return SocialShare; - -}); diff --git a/addons/website/static/src/js/website.snippets.animation.js b/addons/website/static/src/js/website.snippets.animation.js deleted file mode 100644 index 234a43463dd..00000000000 --- a/addons/website/static/src/js/website.snippets.animation.js +++ /dev/null @@ -1,593 +0,0 @@ -odoo.define('website.snippets.animation', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var Class = require('web.Class'); -var core = require('web.core'); -var base = require('web_editor.base'); -var animation = require('web_editor.snippets.animation'); - -var qweb = core.qweb; - -// Initialize fallbacks for the use of requestAnimationFrame, cancelAnimationFrame and performance.now() -window.requestAnimationFrame = window.requestAnimationFrame - || window.webkitRequestAnimationFrame - || window.mozRequestAnimationFrame - || window.msRequestAnimationFrame - || window.oRequestAnimationFrame - || function (callback) { window.setTimeout(callback, 10); }; - -window.cancelAnimationFrame = window.cancelAnimationFrame - || window.webkitCancelAnimationFrame - || window.mozCancelAnimationFrame - || window.msCancelAnimationFrame - || window.oCancelAnimationFrame - || function (id) { window.clearTimeout(id); }; - -if (!window.performance || !window.performance.now) window.performance = {now: function () { return Date.now(); }}; - -/** - * The AnimationComponent class in in charge of handling one animation loop using the requestAnimationFrame - * feature. This is used by the Animation class below and should not be called directly by an end developer. - * The component contains a simple API, it can be started, stopped, played and paused. - */ -var AnimationComponent = Class.extend({ - init: function (parent, updateCallback, startEvents, $startTarget, options) { - this.parent = parent; // The Animation class instance which is using this component - - options = options || {}; - this._minFrameTime = 1000 / (options.maxFPS || 100); - - // Initialize the animation startEvents, startTarget, endEvents, endTarget and callbacks - this._updateCallback = updateCallback || function () {}; - this.startEvents = startEvents || "scroll"; - this.$startTarget = $($startTarget || window); - this._getStateCallback = options.getStateCallback - || ((this.startEvents === "scroll" && this.$startTarget[0] === window) ? function () { return window.pageYOffset; } : false) - || ((this.startEvents === "resize" && this.$startTarget[0] === window) ? function () { return {width: window.innerWidth, height: window.innerHeight}; } : false) - || function () { return undefined; }; - this.endEvents = options.endEvents || false; - this.$endTarget = options.$endTarget ? $(options.$endTarget) : this.$startTarget; - - // Add a namespace to events using the generated uid - this._uid = "_animationComponent" + _.uniqueId(); - this.startEvents = _processEvents(this.startEvents, this._uid); - if (this.endEvents) { - this.endEvents = _processEvents(this.endEvents, this._uid); - } - - function _processEvents(events, namespace) { - events = events.split(" "); - return _.each(events, function (e, index) { - events[index] += ("." + namespace); - }).join(" "); - } - }, - /** - * The start method initializes when the animation must be played and paused and initializes the - * animation first frame. - */ - start: function () { - // Initialize the animation first frame - this._paused = false; - this._rafID = window.requestAnimationFrame((function (t) { - this._update(t); - this._paused = true; - }).bind(this)); - - // Initialize the animation play/pause events - if (this.endEvents) { - /** - * If there are endEvents, the animation should begin playing when the startEvents are triggered - * on the startTarget and pause when the endEvents are triggered on the endTarget. - */ - this.$startTarget.on(this.startEvents, (function (e) { - if (this._paused) { - _.defer(this.play.bind(this, e)); - } - }).bind(this)); - this.$endTarget.on(this.endEvents, (function () { - if (!this._paused) { - _.defer(this.pause.bind(this)); - } - }).bind(this)); - } else { - /** - * Else, if there is no endEvent, the animation should begin playing when the startEvents are - * *continuously* triggered on the startTarget or fully played once. To achieve this, the animation - * begins playing and is scheduled to pause after 2 seconds. If the startEvents are triggered during - * that time, the schedule is delayed for another 2 seconds. This allows to describe an "effect" - * animation (which lasts less than 2 seconds) or an animation which must be playing *during* an event - * (scroll, mousemove, resize, repeated clicks, ...). - */ - var pauseTimer = null; - this.$startTarget.on(this.startEvents, _.throttle((function (e) { - this.play(e); - - clearTimeout(pauseTimer); - pauseTimer = _.delay((function () { - this.pause(); - pauseTimer = null; - }).bind(this), 2000); - }).bind(this), 250, {trailing: false})); - } - }, - /** - * The stop method pauses the animation and destroys the attached events which trigger the - * animation to be played or paused. - */ - stop: function () { - this.$startTarget.off(this.startEvents); - if (this.endEvents) { - this.$endTarget.off(this.endEvents); - } - this.pause(); - }, - /** - * The play method forces the requestAnimationFrame loop to start. - * @param e -> the event which triggered the animation to play - */ - play: function (e) { - this._newEvent = e; - if (!this._paused) return; - this._paused = false; - this._rafID = window.requestAnimationFrame(this._update.bind(this)); - this._lastUpdateTimestamp = undefined; - }, - /** - * The pause method forces the requestAnimationFrame loop to stop. - */ - pause: function () { - if (this._paused) return; - this._paused = true; - window.cancelAnimationFrame(this._rafID); - this._lastUpdateTimestamp = undefined; - }, - /** - * The private _update method is the callback which is repeatedly called by the requestAnimationFrame - * loop. It controls the max fps at which the animation is running and initializes the values that - * the update callback needs to describe the animation (state, elapsedTime, triggered event). - * @param timestamp -> the DOMHighResTimeStamp timestamp at which the function is called - */ - _update: function (timestamp) { - if (this._paused) return; - this._rafID = window.requestAnimationFrame(this._update.bind(this)); - - // Check the elapsed time since the last update callback call. - // Consider it 0 if there is no info of last timestamp and leave this _update call if it was - // called too soon (would overflow the set max FPS). - var elapsedTime = 0; - if (this._lastUpdateTimestamp) { - elapsedTime = timestamp - this._lastUpdateTimestamp; - if (elapsedTime < this._minFrameTime) return; - } - - // Check the new animation state thanks to the get state callback and store its new value. - // If the state is the same as the previous one, leave this _update call, except if there - // is an event which triggered the "play" method again. - var animationState = this._getStateCallback.call(this.parent, elapsedTime, this._newEvent); - if (!this._newEvent && animationState !== undefined && _.isEqual(animationState, this._animationLastState)) return; - this._animationLastState = animationState; - - // Call the update callback with frame parameters - this._updateCallback.call(this.parent, this._animationLastState, elapsedTime, this._newEvent); - this._lastUpdateTimestamp = timestamp; // Save the timestamp at which the update callback was really called - this._newEvent = undefined; // Forget the event which triggered the last "play" call - }, -}); - -/** - * Extend the Animation class to be able to register AnimationComponent's automatically according to - * the methods it defines. Also allows to register them in more conventional ways by extending the - * _prepareComponents method and calling the _addComponent method. - */ -animation.Class.include({ - maxFPS: 100, // The max FPS at which all the automatic animation components will be running by default - - /** - * Extend the start method to prepare animation components and start them. - */ - start: function () { - this._super.apply(this, arguments); - - this._prepareComponents(); - _.each(this._animationComponents, function (component) { - component.start(); - }); - }, - /** - * Extend the start method to stop animation components and destroy them. - */ - stop: function () { - this._super.apply(this, arguments); - - _.each(this._animationComponents, function (component) { - component.stop(); - }); - this._animationComponents = []; - }, - /** - * The private _prepareComponents method is the place to register AnimationComponent instances. - * This can be done by extending this method and calling the _addComponent method in it or by - * defining particulary-named methods in the specialized Animation class. - * - * The automatic creation of components according to method names is working like this: - * - Search for methods which match the pattern: on_startEvent[_startTarget[_off_endEvent[_endTarget]]] where: - * - startEvent: the event name which triggers the animation to begin playing - * - startTarget (optional): the selector (see description below) to find the target where to listen - * for startEvent (if no selector, the window target will be used) - * - endEvent (optional): the event name which triggers the end of the animation (if none is defined, the - * animation will stop after a while, @see AnimationComponent.start) - * - endTarget (optional): the selector (see description below) to find the target where to listen - * for endEvent (if no selector, the startTarget will be used) - * - For all of these methods, register the appropriate animation component with the method implementation used - * as the update callback. The method receives 3 arguments: the animation state, the elapsedTime since last - * update and the event which triggered the animation (undefined if just a new update call without trigger). - * The animation state is undefined by default, the scroll offset for the particular "on_scroll" method and - * and object with width and height for the particular "on_resize" method. There is the possibility to define - * the getState callback of the animation component with this method. For the component created by the definition - * of the "on_abc_def", define the "get_state_for_on_abc_def" (so "get_state_for_" followed by the method name). - * This allows to improve performance even further in some cases. - * - * Selectors mentioned above can be: - * - the "selector" string: this tells the system to use the Animation instance $target element as target for the - * animation component - * - an underscore-separated list of classnames: for example "leftPanel_playBtn", the system will search inside - * the Animation instance $target element for a DOM which matches the selector ".leftPanel .playBtn" - */ - _prepareComponents: function () { - this._animationComponents = []; - - var self = this; - _.each(this.__proto__, function (callback, key) { - if (!_.isFunction(callback)) return; - var m = key.match(/^on_([^_]+)(?:_(.+?))?(?:_off_([^_]+)(?:_(.+))?)?$/); // match the pattern described above - if (!m) return; - - self._addComponent(callback, m[1], _target_from_ugly_selector(m[2]), { - getStateCallback: self["get_state_for_" + key] || undefined, - endEvents: m[3] || undefined, - $endTarget: _target_from_ugly_selector(m[4]), - maxFPS: self.maxFPS, - }); - - // Return the DOM element matching the selector in the form described above. - function _target_from_ugly_selector(selector) { - if (selector) { - if (selector === "selector") { - return self.$target; - } else { - return self.$(_.map(selector.split("_"), function (v) { - return "." + v; - }).join(" ")); - } - } - return undefined; - } - }); - }, - /** - * The private _addComponent method registers a new AnimationComponent according to given parameters - * @see AnimationComponent.init - */ - _addComponent: function (updateCallback, startEvents, $startTarget, options) { - this._animationComponents.push(new AnimationComponent(this, updateCallback, startEvents, $startTarget, options)); - }, -}); - -function load_called_template () { - var ids_or_xml_ids = _.uniq($("[data-oe-call]").map(function () {return $(this).data('oe-call');}).get()); - if (ids_or_xml_ids.length) { - ajax.jsonRpc('/website/multi_render', 'call', { - 'ids_or_xml_ids': ids_or_xml_ids - }).then(function (data) { - for (var k in data) { - var $data = $(data[k]).addClass('o_block_'+k); - $("[data-oe-call='"+k+"']").each(function () { - $(this).replaceWith($data.clone()); - }); - } - }); - } -} - -base.ready().then(function () { - load_called_template(); - if ($(".o_gallery:not(.oe_slideshow)").size()) { - // load gallery modal template - ajax.loadXML('/website/static/src/xml/website.gallery.xml', qweb); - } -}); - -animation.registry.slider = animation.Class.extend({ - selector: ".carousel", - start: function () { - this.$target.carousel(); - return this._super.apply(this, arguments); - }, - stop: function () { - this.$target.carousel('pause'); - this.$target.removeData("bs.carousel"); - return this._super.apply(this, arguments); - }, -}); - -animation.registry.parallax = animation.Class.extend({ - selector: ".parallax", - start: function () { - this._super.apply(this, arguments); - this._rebuild(); - $(window).on("resize.animation_parallax", _.debounce(this._rebuild.bind(this), 500)); - }, - stop: function () { - this._super.apply(this, arguments); - $(window).off(".animation_parallax"); - }, - _rebuild: function () { - // Add/find bg DOM element to hold the parallax bg (support old v10.0 parallax) - if (!this.$bg || !this.$bg.length) { - this.$bg = this.$("> .s_parallax_bg"); - if (!this.$bg.length) { - this.$bg = $('', { - class: 's_parallax_bg' + (this.$target.hasClass('oe_custom_bg') ? ' oe_custom_bg' : ''), - }).prependTo(this.$target); - } - } - var urlTarget = this.$target.css("background-image"); - if (urlTarget !== "none") { - this.$bg.css("background-image", urlTarget); - } - this.$target.css("background-image", "none"); - - // Get parallax speed - this.speed = parseFloat(this.$target.attr("data-scroll-background-ratio") || 0); - - // Reset offset if parallax effect will not be performed and leave - this.$target.toggleClass("s_parallax_is_fixed", this.speed === 1); - if (this.speed === 0 || this.speed === 1) { - this.$bg.css({ - transform: "", - top: "", - bottom: "" - }); - return; - } - - // Initialize parallax data according to snippet and viewport dimensions - this.viewport = document.body.clientHeight - $('#wrapwrap').position().top; - this.visible_area = [this.$target.offset().top]; - this.visible_area.push(this.visible_area[0] + this.$target.innerHeight() + this.viewport); - this.ratio = this.speed * (this.viewport / 10); - - // Provide a "safe-area" to limit parallax - this.$bg.css({ - top: -this.ratio, - bottom: -this.ratio - }); - }, - on_scroll: function (scrollOffset) { - if (this.speed === 0 || this.speed === 1) return; - - // Perform translate if the element is visible only - var vpEndOffset = scrollOffset + this.viewport; - if (vpEndOffset >= this.visible_area[0] && vpEndOffset <= this.visible_area[1]) { - this.$bg.css("transform", "translateY(" + _getNormalizedPosition.call(this, vpEndOffset) + "px)"); - } - - function _getNormalizedPosition(pos) { - // Normalize scroll in a 1 to 0 range - var r = (pos - this.visible_area[1]) / (this.visible_area[0] - this.visible_area[1]); - // Normalize accordingly to current options - return Math.round(this.ratio * (2 * r - 1)); - } - } -}); - -animation.registry.share = animation.Class.extend({ - selector: ".oe_share", - start: function () { - var url_regex = /(\?(?:|.*&)(?:u|url|body)=)(.*?)(&|#|$)/; - var title_regex = /(\?(?:|.*&)(?:title|text|subject)=)(.*?)(&|#|$)/; - var url = encodeURIComponent(window.location.href); - var title = encodeURIComponent($("title").text()); - this.$("a").each(function () { - var $a = $(this); - $a.attr("href", function(i, href) { - return href.replace(url_regex, function (match, a, b, c) { - return a + url + c; - }).replace(title_regex, function (match, a, b, c) { - return a + title + c; - }); - }); - if ($a.attr("target") && $a.attr("target").match(/_blank/i) && !$a.closest('.o_editable').length) { - $a.on('click', function () { - window.open(this.href,'','menubar=no,toolbar=no,resizable=yes,scrollbars=yes,height=550,width=600'); - return false; - }); - } - }); - return this._super.apply(this, arguments); - } -}); - -animation.registry.media_video = animation.Class.extend({ - selector: ".media_iframe_video", - start: function () { - if (!this.$target.has('> iframe').length) { - var editor = '
     
    '; - var size = '
     
    '; - this.$target.html(editor+size); - } - this.$target.html(this.$target.html()+''); - return this._super.apply(this, arguments); - }, -}); - -animation.registry.ul = animation.Class.extend({ - selector: "ul.o_ul_folded, ol.o_ul_folded", - start: function (editable_mode) { - this.$('.o_ul_toggle_self').off('click').on('click', function (event) { - $(this).toggleClass('o_open'); - $(this).closest('li').find('ul,ol').toggleClass('o_close'); - event.preventDefault(); - }); - this.$('.o_ul_toggle_next').off('click').on('click', function (event) { - $(this).toggleClass('o_open'); - $(this).closest('li').next().toggleClass('o_close'); - event.preventDefault(); - }); - return this._super.apply(this, arguments); - }, -}); - -/** - * This is a fix for apple device (<= IPhone 4, IPad 2) - * Standard bootstrap requires data-toggle='collapse' element to be
    tags. Unfortunatly one snippet uses a - *
    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. - */ -animation.registry._fix_apple_collapse = animation.Class.extend({ - selector: ".s_faq_collapse [data-toggle='collapse']", - start: function () { - this.$target.off("click._fix_apple_collapse").on("click._fix_apple_collapse", function () {}); - return this._super.apply(this, arguments); - }, -}); - -/* ------------------------------------------------------------------------- -Gallery Animation - -This ads a Modal window containing a slider when an image is clicked -inside a gallery --------------------------------------------------------------------------*/ -animation.registry.gallery = animation.Class.extend({ - selector: ".o_gallery:not(.o_slideshow)", - start: function () { - this.$el.on("click", "img", this.click_handler); - return this._super.apply(this, arguments); - }, - click_handler : function (event) { - var $cur = $(event.currentTarget); - var edition_mode = ($cur.closest("[contenteditable='true']").size() !== 0); - - // show it only if not in edition mode - if (!edition_mode) { - var urls = [], - idx = undefined, - milliseconds = undefined, - params = undefined, - $images = $cur.closest(".o_gallery").find("img"), - size = 0.8, - dimensions = { - min_width : Math.round( window.innerWidth * size*0.9), - min_height : Math.round( window.innerHeight * size), - max_width : Math.round( window.innerWidth * size*0.9), - max_height : Math.round( window.innerHeight * size), - width : Math.round( window.innerWidth * size*0.9), - height : Math.round( window.innerHeight * size) - }; - - $images.each(function () { - urls.push($(this).attr("src")); - }); - var $img = ($cur.is("img") === true) ? $cur : $cur.closest("img"); - idx = urls.indexOf($img.attr("src")); - - milliseconds = $cur.closest(".o_gallery").data("interval") || false; - params = { - srcs : urls, - index: idx, - dim : dimensions, - interval : milliseconds, - id: _.uniqueId("slideshow_") - }; - var $modal = $(qweb.render('website.gallery.slideshow.lightbox', params)); - $modal.modal({ - keyboard : true, - backdrop : true - }); - $modal.on('hidden.bs.modal', function () { - $(this).hide(); - $(this).siblings().filter(".modal-backdrop").remove(); // bootstrap leaves a modal-backdrop - $(this).remove(); - - }); - $modal.find(".modal-content, .modal-body.o_slideshow").css("height", "100%"); - $modal.appendTo(document.body); - - this.carousel = new animation.registry.gallery_slider($modal.find(".carousel").carousel()); - } - } // click_handler -}); - -animation.registry.gallery_slider = animation.Class.extend({ - selector: ".o_slideshow", - start: function (editable_mode) { - var self = this; - this.$carousel = this.$target.is(".carousel") ? this.$target : this.$target.find(".carousel"); - this.$indicator = this.$carousel.find('.carousel-indicators'); - this.$prev = this.$indicator.find('li.fa:first').css('visibility', ''); // force visibility as some databases have it hidden - this.$next = this.$indicator.find('li.fa:last').css('visibility', ''); - var $lis = this.$indicator.find('li:not(.fa)'); - var nbPerPage = Math.floor(this.$indicator.width() / $lis.first().outerWidth(true)) - 3; // - navigator - 1 to leave some space - var realNbPerPage = nbPerPage || 1; - var nbPages = Math.ceil($lis.length / realNbPerPage); - - var index; - var page; - update(); - - function hide() { - $lis.each(function (i) { - $(this).toggleClass('hidden', !(i >= page*nbPerPage && i < (page+1)*nbPerPage)); - }); - if (editable_mode) { // do not remove DOM in edit mode - return; - } - if (page <= 0) { - self.$prev.detach(); - } else { - self.$prev.prependTo(self.$indicator); - } - if (page >= nbPages - 1) { - self.$next.detach(); - } else { - self.$next.appendTo(self.$indicator); - } - } - - function update() { - index = $lis.index($lis.filter('.active')) || 0; - page = Math.floor(index / realNbPerPage); - hide(); - } - - this.$carousel.on('slide.bs.carousel.gallery_slider', function () { - setTimeout(function () { - var $item = self.$carousel.find('.carousel-inner .prev, .carousel-inner .next'); - var index = $item.index(); - $lis.removeClass("active") - .filter('[data-slide-to="'+index+'"]') - .addClass("active"); - }, 0); - }); - this.$indicator.on('click.gallery_slider', '> li.fa', function () { - page += ($(this).hasClass('o_indicators_left') ? -1 : 1); - page = Math.max(0, Math.min(nbPages - 1, page)); // should not be necessary - self.$carousel.carousel(page * realNbPerPage); - hide(); - }); - this.$carousel.on('slid.bs.carousel.gallery_slider', update); - }, - stop: function () { - this._super.apply(this, arguments); - - this.$prev.prependTo(this.$indicator); - this.$next.appendTo(this.$indicator); - this.$carousel.off('.gallery_slider'); - this.$indicator.off('.gallery_slider'); - }, -}); - -}); diff --git a/addons/website/static/src/js/website.snippets.editor.js b/addons/website/static/src/js/website.snippets.editor.js deleted file mode 100644 index 098e7fc94a2..00000000000 --- a/addons/website/static/src/js/website.snippets.editor.js +++ /dev/null @@ -1,538 +0,0 @@ -odoo.define('website.snippets.editor', function (require) { -'use strict'; - -var ajax = require("web.ajax"); -var core = require("web.core"); -var Dialog = require("web.Dialog"); -var rpc = require("web.rpc"); -var editor = require("web_editor.editor"); -var animation = require('web_editor.snippets.animation'); -var options = require('web_editor.snippets.options'); -var snippet_editor = require('web_editor.snippet.editor'); -var website = require('website.website'); - -var _t = core._t; - -snippet_editor.Class.include({ - _get_snippet_url: function () { - return '/website/snippets'; - } -}); - -options.registry.menu_data = options.Class.extend({ - start: function () { - this._super.apply(this, arguments); - this.link = this.$target.attr("href"); - }, - - on_focus: function () { - this._super.apply(this, arguments); - - (new Dialog(null, { - title: _t("Confirmation"), - $content: $(core.qweb.render("website.leaving_current_page_edition")), - buttons: [ - {text: _t("Go to Link"), classes: "btn-primary", click: save_editor_then_go_to.bind(null, this.link)}, - {text: _t("Edit the menu"), classes: "btn-primary", click: function () { - var self = this; - website.topBar.content_menu.edit_menu(function () { - return editor.editor_bar.save_without_reload(); - }).then(function (dialog) { - self.close(); - }); - }}, - {text: _t("Stay on this page"), close: true} - ] - })).open(); - - function save_editor_then_go_to(url) { - editor.editor_bar.save_without_reload().then(function () { - window.location.href = url; - }); - } - }, -}); - -options.registry.company_data = options.Class.extend({ - start: function () { - this._super.apply(this, arguments); - - var proto = options.registry.company_data.prototype; - - if (proto.__link_deferred === undefined) { - proto.__link_deferred = $.Deferred(); - return ajax.jsonRpc("/web/session/get_session_info", "call").then(function (session) { - return rpc.query({ - model: 'res.users', - method: 'read', - args: [session.uid, ['company_id']], - }) - .then(function (res) { - proto.__link_deferred.resolve( - "/web#action=base.action_res_company_form&view_type=form&id=" + (res && res[0] && res[0].company_id[0] || 1) - ); - }); - }); - } - }, - - on_focus: function () { - this._super.apply(this, arguments); - - var proto = options.registry.company_data.prototype; - - Dialog.confirm(null, _t("Do you want to edit the company data ?"), { - confirm_callback: function () { - editor.editor_bar.save_without_reload().then(function () { - proto.__link_deferred.then(function (link) { - window.location.href = link; - }); - }); - }, - }); - }, -}); - -options.registry.slider = options.Class.extend({ - drop_and_build_snippet: function () { - this.id = "myCarousel" + new Date().getTime(); - this.$target.attr("id", this.id); - this.$target.find("[data-slide]").attr("data-cke-saved-href", "#" + this.id); - this.$target.find("[data-target]").attr("data-target", "#" + this.id); - this.rebind_event(); - }, - on_clone: function ($clone) { - var id = "myCarousel" + new Date().getTime(); - $clone.attr("id", id); - $clone.find("[data-slide]").attr("href", "#" + id); - $clone.find("[data-slide-to]").attr("data-target", "#" + id); - }, - // rebind event to active carousel on edit mode - rebind_event: function () { - var self = this; - this.$target.find('.carousel-indicators [data-slide-to]').off('click').on('click', function () { - self.$target.carousel(+$(this).data('slide-to')); }); - }, - clean_for_save: function () { - this._super(); - this.$target.find(".item").removeClass("next prev left right active") - .first().addClass("active"); - this.$target.find('.carousel-indicators').find('li').removeClass('active') - .first().addClass("active"); - }, - start : function () { - this._super.apply(this, arguments); - this.$target.carousel({interval: false}); - this.id = this.$target.attr("id"); - this.$inner = this.$target.find('.carousel-inner'); - this.$indicators = this.$target.find('.carousel-indicators'); - this.$target.carousel('pause'); - this.rebind_event(); - }, - add_slide: function (type) { - if(type !== "click") return; - - var self = this; - var cycle = this.$inner.find('.item').length; - var $active = this.$inner.find('.item.active, .item.prev, .item.next').first(); - var index = $active.index(); - this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden"); - this.$indicators.append('
  • '); - - // clone the best candidate from template to use new features - var $snippets = this.buildingBlock.$snippets; - //since saas-6, all snippets must start by s_ - var selection = this.$target.closest('[class*="s_"'); - if (_.isUndefined(selection)) { - var point = 0; - var className = _.compact(this.$target.attr("class").split(" ")); - $snippets.find('.oe_snippet_body').each(function () { - var len = _.intersection(_.compact(this.className.split(" ")), className).length; - if (len > point) { - point = len; - selection = this; - } - }); - } - else { - var s_class = selection.attr('class').split(' ').filter(function (o) { return _.str.startsWith(o, "s_"); })[0]; - selection = $snippets.find("." + s_class); - } - var $clone = $(selection).find('.item:first').clone(); - - // insert - $clone.removeClass('active').insertAfter($active); - setTimeout(function () { - self.$target.carousel().carousel(++index); - self.rebind_event(); - },0); - return $clone; - }, - remove_slide: function (type) { - if (type !== "click" || this.remove_process) return; - var self = this; - - var $items = this.$inner.find('.item'); - var cycle = $items.length - 1; - var $active = $items.filter('.active'); - var index = $active.index(); - - if (cycle > 0) { - this.remove_process = true; - this.$target.on('slid.bs.carousel.slide_removal', function (event) { - $active.remove(); - self.$indicators.find("li:last").remove(); - self.$target.off('slid.bs.carousel.slide_removal'); - self.rebind_event(); - self.remove_process = false; - if (cycle === 1) { - self.$target.find('.carousel-control, .carousel-indicators').addClass("hidden"); - } - }); - _.defer(function () { - self.$target.carousel(index > 0 ? --index : cycle); - }); - } - }, - interval : function (type, value) { - this.$target.attr("data-interval", value); - }, - set_active: function () { - this.$el.find('li[data-interval]').removeClass("active") - .filter('li[data-interval='+this.$target.attr("data-interval")+']').addClass("active"); - }, -}); - -options.registry.carousel = options.registry.slider.extend({ - getSize: function () { - this.grid = this._super(); - this.grid.size = 8; - return this.grid; - }, - clean_for_save: function () { - this._super(); - this.$target.removeClass('oe_img_bg ' + this._class).css("background-image", ""); - }, - load_style_options : function () { - this._super(); - $(".snippet-option-size li[data-value='']").remove(); - }, - start : function () { - var self = this; - this._super.apply(this, arguments); - - // set background and prepare to clean for save - this.$target.on('slid.bs.carousel', function () { - self.$target.carousel("pause"); - if (!self.editor) return; - - _.each(["background", "background_position", "colorpicker"], function (opt_name) { - var s_option = self.editor.styles[opt_name]; - if (!s_option) return; - - s_option.$target = self.$target.find(".item.active"); - s_option.set_active(); - s_option.$target.trigger("snippet-option-change", [s_option]); - if (opt_name === 'background') { - s_option.bind_bg_events(); - } - }); - }); - this.$target.trigger('slid.bs.carousel'); - }, - // rebind event to active carousel on edit mode - rebind_event: function () { - var self = this; - this.$target.find('.carousel-control').off('click').on('click', function () { - self.$target.carousel($(this).data('slide')); - }); - this._super.apply(this, arguments); - - /* Fix: backward compatibility saas-3 */ - this.$target.find('.item.text_image, .item.image_text, .item.text_only').find('.container > .carousel-caption > div, .container > img.carousel-image').attr('contentEditable', 'true'); - }, -}); - -options.registry["margin-x"] = options.registry.marginAndResize.extend({ - preventChildPropagation: true, - - getSize: function () { - this.grid = this._super(); - var width = this.$target.parents(".row:first").first().outerWidth(); - - var grid = [1,2,3,4,5,6,7,8,9,10,11,12]; - this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})]; - - grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11]; - this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12]; - - return this.grid; - }, - on_clone: function ($clone, options) { - // Below condition is added to remove offset of target element only - // and not its children to avoid design alteration of a container / block. - if (options.isCurrent) { - var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''); - $clone.attr("class", _class); - } - return false; - }, - on_resize: function (compass, beginClass, current) { - if (compass === 'w') { - // don't change the right border position when we change the offset (replace col size) - var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0); - var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0); - var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0); - if (offset < 0) { - offset = 0; - } - var colSize = beginCol - (offset - beginOffset); - if (colSize <= 0) { - colSize = 1; - offset = beginOffset + beginCol - 1; - } - this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, '')); - - this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize)); - if (offset > 0) { - this.$target.addClass('col-md-offset-' + offset); - } - } - this._super(compass, beginClass, current); - }, -}); - -options.registry.parallax = options.Class.extend({ - getSize: function () { - this.grid = this._super.apply(this, arguments); - this.grid.size = 8; - return this.grid; - }, - start: function () { - this._super.apply(this, arguments); - if (!this.$target.data("snippet-view")) { - this.$target.data("snippet-view", new animation.registry.parallax(this.$target)); - } - this._refresh_callback = this._refresh.bind(this); - this._toggle_refresh_callback(true); - }, - on_focus: function () { - this._super.apply(this, arguments); - this._update_target_to_bg(); - }, - on_resize: function () { - this._super.apply(this, arguments); - this._refresh(); - }, - scroll: function (type, value) { - this.$target.attr("data-scroll-background-ratio", value); - this._refresh(); - }, - set_active: function () { - this._super.apply(this, arguments); - this.$el.find('[data-scroll]').removeClass("active") - .filter('[data-scroll="' + (this.$target.attr('data-scroll-background-ratio') || 0) + '"]').addClass("active"); - }, - clean_for_save: function () { - this._super.apply(this, arguments); - this._toggle_refresh_callback(false); - }, - on_move: function () { - this._super.apply(this, arguments); - this._refresh(); - }, - on_remove: function () { - this._super.apply(this, arguments); - this._toggle_refresh_callback(false); - }, - _update_target_to_bg: function () { - this.editor.styles.background.$target = this.$target.data("snippet-view").$bg; - this.editor.styles.background.set_active(); - this.editor.styles.background_position.$target = this.$target.data("snippet-view").$bg; - this.editor.styles.background_position.set_active(); - }, - _refresh: function () { - _.defer((function () { - this.$target.data("snippet-view")._rebuild(); - }).bind(this)); - }, - _toggle_refresh_callback: function (on) { - this.$target[on ? "on" : "off"]("snippet-option-change snippet-option-preview", this._refresh_callback); - this.buildingBlock.$el[on ? "on" : "off"]("snippet-dropped snippet-activated", this._refresh_callback); - }, -}); - -options.registry.ul = options.Class.extend({ - start: function () { - var self = this; - this._super(); - this.$target.data("snippet-view", new animation.registry.ul(this.$target, true)); - this.$target.on('mouseup', '.o_ul_toggle_self, .o_ul_toggle_next', function () { - setTimeout(function () { - self.buildingBlock.cover_target(self.$overlay, self.$target); - },0); - }); - }, - reset_ul: function () { - this.$target.find('.o_ul_toggle_self, .o_ul_toggle_next').remove(); - - this.$target.find('li:has(>ul,>ol)').map(function () { - // get if the li contain a text label - var texts = _.filter(_.toArray(this.childNodes), function (a) { return a.nodeType == 3;}); - if (!texts.length || !texts.reduce(function (a,b) { return a.textContent + b.textContent;}).match(/\S/)) { - return; - } - $(this).children('ul,ol').addClass('o_close'); - return $(this).children(':not(ul,ol)')[0] || this; - }) - .prepend(''); - - var $li = this.$target.find('li:has(+li:not(>.o_ul_toggle_self)>ul, +li:not(>.o_ul_toggle_self)>ol)'); - $li.map(function () { return $(this).children()[0] || this; }) - .prepend(''); - $li.removeClass('o_open').next().addClass('o_close'); - - this.$target.find("li").removeClass('o_open').css('list-style', ''); - this.$target.find("li:has(.o_ul_toggle_self, .o_ul_toggle_next), li:has(>ul,>ol):not(:has(>li))").css('list-style', 'none'); - }, - clean_for_save: function () { - this._super(); - if (!this.$target.hasClass('o_ul_folded')) { - this.$target.find(".o_close").removeClass("o_close"); - } - this.$target.find("li:not(:has(>ul))").css('list-style', ''); - }, - toggle_class: function (type, value, $li) { - this._super(type, value, $li); - this.$target.data("snippet-view").stop(); - this.reset_ul(); - this.$target.find("li:not(:has(>ul))").css('list-style', ''); - this.$target.data("snippet-view", new animation.registry.ul(this.$target, true)); - } -}); - -options.registry.collapse = options.Class.extend({ - start: function () { - var self = this; - this._super(); - this.$target.on('shown.bs.collapse hidden.bs.collapse', '[role="tabpanel"]', function () { - self.buildingBlock.cover_target(self.$overlay, self.$target); - }); - }, - create_ids: function ($target) { - var time = new Date().getTime(); - var $tab = $target.find('[data-toggle="collapse"]'); - - // link to the parent group - - var $tablist = $target.closest('.panel-group'); - var tablist_id = $tablist.attr("id"); - if (!tablist_id) { - tablist_id = "myCollapse" + time; - $tablist.attr("id", tablist_id); - } - $tab.attr('data-parent', "#"+tablist_id); - $tab.data('parent', "#"+tablist_id); - - // link to the collapse - - var $panel = $target.find('.panel-collapse'); - var panel_id = $panel.attr("id"); - if (!panel_id) { - while($('#'+(panel_id = "myCollapseTab" + time)).length) { - time++; - } - $panel.attr("id", panel_id); - } - $tab.attr('data-target', "#"+panel_id); - $tab.data('target', "#"+panel_id); - }, - drop_and_build_snippet: function () { - this._super(); - this.create_ids(this.$target); - }, - on_clone: function ($clone) { - this._super.apply(this, arguments); - $clone.find('[data-toggle="collapse"]').removeAttr('data-target').removeData('target'); - $clone.find('.panel-collapse').removeAttr('id'); - this.create_ids($clone); - }, - on_move: function () { - this._super(); - this.create_ids(this.$target); - var $panel = this.$target.find('.panel-collapse').removeData('bs.collapse'); - if ($panel.attr('aria-expanded') === 'true') { - $panel.closest('.panel-group').find('.panel-collapse[aria-expanded="true"]') - .filter(function () {return this !== $panel[0];}) - .collapse('hide') - .one('hidden.bs.collapse', function () { - $panel.trigger('shown.bs.collapse'); - }); - } - } -}); - -return options; -}); - - -odoo.define('website.rte.summernote', function (require) { -'use strict'; - -var core = require('web.core'); -require('web_editor.rte.summernote'); - -var eventHandler = $.summernote.eventHandler; -var renderer = $.summernote.renderer; -var tplIconButton = renderer.getTemplate().iconButton; -var _t = core._t; - -var fn_tplPopovers = renderer.tplPopovers; -renderer.tplPopovers = function (lang, options) { - var $popover = $(fn_tplPopovers.call(this, lang, options)); - $popover.find('.note-image-popover .btn-group:has([data-value="img-thumbnail"])').append( - tplIconButton('fa fa-object-ungroup', { - title: _t('Transform the picture (click twice to reset transformation)'), - event: 'transform', - })); - return $popover; -}; - - -$.summernote.pluginEvents.transform = function (event, editor, layoutInfo, sorted) { - var $selection = layoutInfo.handle().find('.note-control-selection'); - var $image = $($selection.data('target')); - - if($image.data('transfo-destroy')) { - $image.removeData('transfo-destroy'); - return; - } - - $image.transfo(); - - var mouseup = function (event) { - $('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]')); - }; - $(document).on('mouseup', mouseup); - - var mousedown = function (event) { - if (!$(event.target).closest('.transfo-container').length) { - $image.transfo("destroy"); - $(document).off('mousedown', mousedown).off('mouseup', mouseup); - } - if ($(event.target).closest('.note-popover').length) { - $image.data('transfo-destroy', true).attr("style", ($image.attr("style") || '').replace(/[^;]*transform[\w:]*;?/g, '')); - } - $image.trigger('content_changed'); - }; - $(document).on('mousedown', mousedown); -}; - -var fn_boutton_update = eventHandler.modules.popover.button.update; -eventHandler.modules.popover.button.update = function ($container, oStyle) { - fn_boutton_update.call(this, $container, oStyle); - $container.find('button[data-event="transform"]') - .toggleClass('active', $(oStyle.image).is('[style*="transform"]')) - .toggleClass('hidden', !$(oStyle.image).is('img')); -}; -}); diff --git a/addons/website/static/src/js/website.theme.js b/addons/website/static/src/js/website.theme.js deleted file mode 100644 index 2db3d1c0e5d..00000000000 --- a/addons/website/static/src/js/website.theme.js +++ /dev/null @@ -1,311 +0,0 @@ -odoo.define('website.theme', function (require) { -'use strict'; - -var ajax = require('web.ajax'); -var core = require('web.core'); -var session = require('web.session'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var website = require('website.website'); - -var QWeb = core.qweb; - -ajax.jsonRpc('/web/dataset/call', 'call', { - 'model': 'ir.ui.view', - 'method': 'read_template', - 'args': ['website.theme_customize', base.get_context()] - }).done(function (data) { - QWeb.add_template(data); -}); - -ajax.jsonRpc('/web/dataset/call', 'call', { - 'model': 'ir.ui.view', - 'method': 'read_template', - 'args': ['web_editor.colorpicker', base.get_context()] - }).done(function (data) { - QWeb.add_template(data); -}); - -var Theme = Widget.extend({ - template: 'website.theme_customize', - events: { - 'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection', - 'mousedown label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) { - var self = this; - this.time_select = _.defer(function () { - var input = $(event.target).find('input').length ? $(event.target).find('input') : $(event.target).parent().find('input'); - self.on_select(input, event); - }); - }, - 'click .close': 'close', - 'click': 'click', - }, - start: function () { - var self = this; - this.timer = null; - this.reload = false; - this.flag = false; - this.active_select_tags(); - this.$inputs = this.$("input[data-xmlid],input[data-enable],input[data-disable]"); - setTimeout(function () {self.$el.addClass('in');}, 0); - this.keydown_escape = function (event) { - if (event.keyCode === 27) { - self.close(); - } - }; - $(document).on('keydown', this.keydown_escape); - return this.load_xml_data().then(function () { - self.flag = true; - }); - }, - active_select_tags: function () { - var uniqueID = 0; - var self = this; - var $selects = this.$('select:has(option[data-xmlid],option[data-enable],option[data-disable])'); - $selects.each(function () { - uniqueID++; - var $select = $(this); - var $options = $select.find('option[data-xmlid], option[data-enable], option[data-disable]'); - $options.each(function () { - var $option = $(this); - var $input = $(''); - $input.attr('id', $option.attr('id')); - $input.attr('data-xmlid', $option.data('xmlid')); - $input.attr('data-enable', $option.data('enable')); - $input.attr('data-disable', $option.data('disable')); - $option.removeAttr('id'); - $option.data('input', $input); - $input.on('update', function () { - $option.attr('selected', $(this).prop("checked")); - }); - self.$el.append($input); - }); - $select.data("value", $options.first()); - $options.first().attr("selected", true); - }); - $selects.change(function () { - var $option = $(this).find('option:selected'); - $(this).data("value").data("input").prop("checked", true).change(); - $(this).data("value", $option); - $option.data("input").change(); - }); - }, - load_xml_data: function () { - var self = this; - $('#theme_error').remove(); - return ajax.jsonRpc('/website/theme_customize_get', 'call', { - 'xml_ids': this.get_xml_ids(this.$inputs) - }).done(function (data) { - self.$inputs.filter('[data-xmlid=""]').prop("checked", true).change(); - self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])').each(function () { - if (!_.difference(self.get_xml_ids($(this)), data[1]).length) { - $(this).prop("checked", false).trigger("change", true); - } - if (!_.difference(self.get_xml_ids($(this)), data[0]).length) { - $(this).prop("checked", true).trigger("change", true); - } - }); - }).fail(function (d, error) { - $('body').prepend($('
    ').text(error.data.message)); - }); - }, - get_inputs: function (string) { - return this.$inputs.filter('#'+string.split(/\s*,\s*/).join(", #")); - }, - get_xml_ids: function ($inputs) { - var xml_ids = []; - $inputs.each(function () { - if ($(this).data('xmlid') && $(this).data('xmlid').length) { - xml_ids = xml_ids.concat($(this).data('xmlid').split(/\s*,\s*/)); - } - }); - return xml_ids; - }, - update_style: function (enable, disable, reload) { - if (this.$el.hasClass("loading")) { - return; - } - this.$el.addClass('loading'); - - if (!reload && session.debug !== "assets") { - var self = this; - return ajax.jsonRpc('/website/theme_customize', 'call', { - enable: enable, - disable: disable, - get_bundle: true, - }).then(function (bundleHTML) { - var $links = $('link[href*=".assets_frontend"]'); - var $newLinks = $(bundleHTML).filter('link'); - - var linksLoaded = $.Deferred(); - var nbLoaded = 0; - $newLinks.on('load', function (e) { - if (++nbLoaded >= $newLinks.length) { - linksLoaded.resolve(); - } - }); - $newLinks.on('error', function (e) { - linksLoaded.reject(); - window.location.hash = "theme=true"; - window.location.reload(); - }); - - $links.last().after($newLinks); - return linksLoaded.then(function () { - $links.remove(); - self.$el.removeClass('loading'); - }); - }); - } else { - var href = '/website/theme_customize_reload'+ - '?href='+encodeURIComponent(window.location.href)+ - '&enable='+encodeURIComponent(enable.join(","))+ - '&disable='+encodeURIComponent(disable.join(",")); - window.location.href = href; - return $.Deferred(); - } - }, - enable_disable: function ($inputs, enable) { - $inputs.each(function () { - var check = $(this).prop("checked"); - var $label = $(this).closest("label"); - $(this).prop("checked", enable); - if (enable) $label.addClass("checked"); - else $label.removeClass("checked"); - if (check != enable) { - $(this).change(); - } - }); - }, - change_selection: function (event, init_mode) { - var self = this; - clearTimeout(this.time_select); - - if (this.$el.hasClass("loading")) return; // prevent to change selection when css is loading - - var $option = $(event.target).is('input') ? $(event.target) : $("input", event.target), - $options = $option, - checked = $option.prop("checked"); - - if (checked) { - if($option.data('enable')) { - var $inputs = this.get_inputs($option.data('enable')); - $options = $options.add($inputs.filter(':not(:checked)')); - this.enable_disable($inputs, true); - } - if($option.data('disable')) { - var $inputs = this.get_inputs($option.data('disable')); - $options = $options.add($inputs.filter(':checked')); - this.enable_disable($inputs, false); - } - $option.closest("label").addClass("checked"); - } else { - $option.closest("label").removeClass("checked"); - } - - var $enable = this.$inputs.filter('[data-xmlid]:checked'); - $enable.closest("label").addClass("checked"); - var $disable = this.$inputs.filter('[data-xmlid]:not(:checked)'); - $disable.closest("label").removeClass("checked"); - - var $sets = this.$inputs.filter('input[data-enable]:not([data-xmlid]), input[data-disable]:not([data-xmlid])'); - $sets.each(function () { - var $set = $(this); - var checked = true; - if ($set.data("enable")) { - self.get_inputs($(this).data("enable")).each(function () { - if (!$(this).prop("checked")) checked = false; - }); - } - if ($set.data("disable")) { - self.get_inputs($(this).data("disable")).each(function () { - if ($(this).prop("checked")) checked = false; - }); - } - if (checked) { - $set.prop("checked", true).closest("label").addClass("checked"); - } else { - $set.prop("checked", false).closest("label").removeClass("checked"); - } - $set.trigger('update'); - }); - - if (this.flag && $option.data('reload') && document.location.href.match(new RegExp( $option.data('reload') ))) { - this.reload = true; - } - - clearTimeout(this.timer); - if (this.flag) { - this.timer = _.defer(function () { - if (!init_mode) self.on_select($options, event); - self.update_style(self.get_xml_ids($enable), self.get_xml_ids($disable), self.reload); - self.reload = false; - }); - } else { - this.timer = _.defer(function () { - if (!init_mode) self.on_select($options, event); - self.reload = false; - }); - } - }, - /* Method call when the user change the selection or click on an input - * @values: all changed inputs - */ - on_select: function ($inputs, event) { - clearTimeout(this.time_select); - }, - click: function (event) { - if (!$(event.target).closest("#theme_customize_modal > *").length) { - this.close(); - } - }, - close: function () { - var self = this; - $(document).off('keydown', this.keydown_escape); - $('#theme_error').remove(); - $('link[href*=".assets_"]').removeAttr('data-loading'); - this.$el.removeClass('in'); - this.$el.addClass('out'); - setTimeout(function () {self.destroy();}, 500); - } -}); - -function themeError(message) { - var _t = core._t; - - if (message.indexOf('lessc')) { - message = '' + message + "

    " + _t("Please install or update node-less"); - } - - website.error(_t("Theme Error"), message); -} - -function theme_customize() { - if (Theme.open && !Theme.open.isDestroyed()) return; - Theme.open = new Theme(); - Theme.open.appendTo("body"); - - var error = window.getComputedStyle(document.body, ':before').getPropertyValue('content'); - if (error && error !== 'none') { - themeError(eval(error)); - } -} - -website.TopBar.include({ - start: function () { - var self = this; - base.ready().then(function () { - self.$el.on('click', "#theme_customize a", theme_customize); - if ((window.location.hash || "").indexOf("theme=true") !== -1) { - theme_customize(); - window.location.hash = ""; - } - }); - - return this._super(); - } -}); - -return Theme; - -}); diff --git a/addons/website/static/src/js/website.tour.banner.js b/addons/website/static/src/js/website.tour.banner.js deleted file mode 100644 index 8c61c78a5ab..00000000000 --- a/addons/website/static/src/js/website.tour.banner.js +++ /dev/null @@ -1,81 +0,0 @@ -odoo.define("website.tour.banner", function (require) { - "use strict"; - - var core = require("web.core"); - var tour = require("web_tour.tour"); - var base = require("web_editor.base"); - - var _t = core._t; - - tour.register("banner", { - url: "/", - wait_for: base.ready(), - }, [{ - trigger: "a[data-action=edit]", - content: _t("Click Edit to start designing your homepage."), - extra_trigger: ".homepage", - position: "bottom", - }, { - trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail", - content: _t("Drag the Cover block and drop it in your page."), - position: "bottom", - run: "drag_and_drop", - }, { - trigger: "#wrapwrap .s_text_block_image_fw h2", - content: _t("Click on a text to start editing it. It's that easy to edit your content!"), - position: "left", - width: 150, - run: "text", - }, { - trigger: ".oe_overlay_options .oe_options", - extra_trigger: "#wrapwrap .s_text_block_image_fw h2:not(:containsExact(\"Headline\"))", - content: _t("Customize any block through this menu. Try to change the background of the banner."), - position: "bottom", - }, { - trigger: "#snippet_structure .oe_snippet:eq(3) .oe_snippet_thumbnail", - content: _t("Drag another block in your page, below the cover."), - position: "bottom", - run: "drag_and_drop", - }, { - trigger: "button[data-action=save]", - extra_trigger: ".oe_overlay_options .oe_options", - content: _t("Publish your page by clicking on the Save button."), - position: "bottom", - }, { - trigger: "a[data-action=show-mobile-preview]", - content: _t("Good Job! You created your first page. Let's check how this page looks like on mobile devices."), - position: "bottom", - }, { - trigger: ".modal-dialog:has(#mobile-viewport) button[data-dismiss=modal]", - content: _t("After having checked how it looks on mobile, close the preview."), - position: "right", - }, { - trigger: "#oe_main_menu_navbar a[data-action=new_page]", - content: _t("

    That's it. Your homepage is live.

    Continue adding more pages to your site or edit this page to make it even more awesome.

    "), - position: "bottom", - }]); -}); - - -odoo.define("website.tour.contact", function (require) { - "use strict"; - - var core = require("web.core"); - var tour = require("web_tour.tour"); - var base = require("web_editor.base"); - var _t = core._t; - - tour.register("contact", { - url: "/page/contactus", - wait_for: base.ready(), - }, [{ - trigger: "li#customize-menu", - content: _t("Install a contact form to improve this page."), - extra_trigger: "#o_contact_mail", - position: "bottom", - }, { - trigger: "li#install_apps", - content: _t("Install new apps to get more features. Let's install the 'Contact form' app."), - position: "bottom", - }]); -}); diff --git a/addons/website/static/src/js/website.tour.rte.js b/addons/website/static/src/js/website.tour.rte.js deleted file mode 100644 index d428c46fa7f..00000000000 --- a/addons/website/static/src/js/website.tour.rte.js +++ /dev/null @@ -1,159 +0,0 @@ -odoo.define('website.tour.rte', function (require) { -'use strict'; - -var tour = require("web_tour.tour"); -var base = require("web_editor.base"); - -tour.register('rte_translator', { - test: true, - wait_for: base.ready() -}, - [ - { - content: "click on Add a language", - trigger: '.js_language_selector a:has(i.fa)', - }, { - content: "select french", - trigger: 'select[name="lang"]', - run: 'text "fr_BE"', - }, { - content: "load french", - trigger: '.modal-footer button:first', - extra_trigger: '.modal select[name="lang"]:propValueContains(fr_BE)', - }, { - content: "go to english version", - trigger: '.js_language_selector a[data-lang="en_US"]', - extra_trigger: 'html[lang*="fr"]', - timeout: 60000, - }, { - content: "Open new page menu", - trigger: '#oe_main_menu_navbar a[data-action=new_page]', - extra_trigger: 'a[data-action="edit"]', - }, { - content: "click on new page", - trigger: 'a[data-action="new_page"]', - }, { - content: "insert page name", - trigger: '#editor_new_page input[type="text"]', - run: 'text rte_translator', - }, { - content: "create page", - trigger: 'button.btn-continue', - extra_trigger: 'input[type="text"]:propValue(rte_translator)', - }, { - content: "drop a snippet", - trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail", - run: 'drag_and_drop', - }, { - content: "change content", - trigger: '.oe_overlay_options .oe_options:visible', - run: function () { - $("#wrap p:first").replaceWith('

    Write one or two paragraphs describing your product or\ - services. To be successful your content needs to be\ - useful to your readers.

    \ -

    <b></b> is an HTML tag & is empty

    '); - $("#wrap img").attr("title", "test translate image title"); - } - }, { - content: "save", - trigger: 'button[data-action=save]', - extra_trigger: '#wrap p:first b', - }, { - content: "click on french version", - trigger: '.js_language_selector a[data-lang="fr_BE"]', - extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', - }, { - content: "translate", - trigger: 'html:not(:has(#wrap p span)) .o_menu_systray a[data-action="translate"]', - }, { - content: "close modal", - trigger: 'button[data-action="activate"]', - }, { - content: "check if translation is activate", - trigger: '[data-oe-translation-id]', - }, { - content: "translate text", - trigger: '#wrap p font:first', - run: function (action_helper) { - action_helper.text('translated french text'); - this.$anchor.trigger('keydown'); - }, - }, { - content: "translate text with special char", - trigger: '#wrap input + p span:first', - run: function (action_helper) { - action_helper.click(); - this.$anchor.prepend('<{translated}>').trigger('keydown'); - }, - }, { - content: "click on input", - trigger: 'input:first', - extra_trigger: '#wrap .o_dirty font:first:contains(translated french text)', - run: 'click', - }, { - content: "translate placeholder", - trigger: 'input:first', - run: 'text test french placeholder', - }, { - content: "close modal", - trigger: 'button.o_save_button', - extra_trigger: '.modal input:propValue(test french placeholder)', - }, { - content: "save translation", - trigger: 'button[data-action=save]', - }, { - content: "check: content is translated", - trigger: '#wrap p font:first:contains(translated french text)', - run: function () {}, // it's a check - }, { - content: "check: content with special char is translated", - trigger: "#wrap input + p:contains(<{translated}> is an HTML\xa0tag & )", - run: function () {}, // it's a check - - }, { - content: "check: placeholder translation", - trigger: 'input[placeholder="test french placeholder"]', - run: function () {}, // it's a check - }, { - content: "return to english version", - trigger: '.js_language_selector a[data-lang="en_US"]', - extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))', - }, { - content: "edit english version", - trigger: 'a[data-action=edit]', - extra_trigger: 'body:not(:has(#wrap p font:first:containsExact(paragraphs describing)))', - }, { - content: "select text", - extra_trigger: 'button[data-action=save]', - trigger: '#wrap p', - run: function (action_helper) { - action_helper.click(); - var el = this.$anchor[0]; - var evt = document.createEvent("MouseEvents"); - $.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select(); - evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el); - el.dispatchEvent(evt); - }, - }, { - content: "underline", - trigger: '.note-air-popover button[data-event="underline"]', - }, { - content: "save new change", - trigger: 'button[data-action=save]', - extra_trigger: '#wrap p u', - }, { - content: "return in french", - trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]', - extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))', - }, { - content: "check bis: content is translated", - trigger: '#wrap p font:first:contains(translated french text)', - extra_trigger: 'body:not(:has(button[data-action=save]))', - }, { - content: "check bis: placeholder translation", - trigger: 'input[placeholder="test french placeholder"]', - }, - ] -); - -}); diff --git a/addons/website/static/src/js/website.translator.js b/addons/website/static/src/js/website.translator.js deleted file mode 100644 index c9b7de4ed8b..00000000000 --- a/addons/website/static/src/js/website.translator.js +++ /dev/null @@ -1,104 +0,0 @@ -odoo.define('website.translator', function (require) { -'use strict'; - -var core = require('web.core'); -var ajax = require('web.ajax'); -var Widget = require('web.Widget'); -var base = require('web_editor.base'); -var translate = require('web_editor.translate'); -var website = require('website.website'); -var local_storage = require('web.local_storage'); -var utils = require('web.utils'); - -var qweb = core.qweb; - -if (!translate.translatable) { - return; -} - - -website.TopBar.include({ - events: _.extend({}, website.TopBar.prototype.events, { - 'click [data-action="edit_master"]': 'edit_master', - 'click [data-action="translate"]': 'translate', - }), - translate: function (ev) { - ev.preventDefault(); - if (translate.edit_translations) { - translate.instance.edit(); - } else { - location.search += '&edit_translations'; - } - }, - edit_master: function (ev) { - ev.preventDefault(); - - var lang = '/' + utils.get_cookie('frontend_lang'); - - var current = document.createElement('a'); - current.href = window.location.toString(); - current.search += (current.search ? '&' : '?') + 'enable_editor=1'; - if (current.pathname.indexOf(lang) === 0) { - current.pathname = current.pathname.replace(lang, ''); - } - - var link = document.createElement('a'); - link.href = '/website/lang/default'; - link.search += (link.search ? '&' : '?') + 'r=' + encodeURIComponent(current.pathname + current.search + current.hash); - - window.location = link.href; - }, -}); - - -if (!translate.edit_translations) { - return; -} - -ajax.loadXML('/website/static/src/xml/website.translator.xml', qweb); - -var nodialog = 'website_translator_nodialog'; - -var Translate = translate.Class.include({ - onTranslateReady: function () { - if(this.gengo_translate) { - this.translation_gengo_display(); - } - this._super(); - }, - edit: function () { - $("#oe_main_menu_navbar").hide(); - if (!local_storage.getItem(nodialog)) { - var dialog = new TranslatorDialog(); - dialog.appendTo($(document.body)); - dialog.on('activate', this, function () { - if (dialog.$('input[name=do_not_show]').prop('checked')) { - local_storage.removeItem(nodialog); - } else { - local_storage.setItem(nodialog, true); - } - dialog.$el.modal('hide'); - }); - } - return this._super(); - }, - cancel: function () { - $("#oe_main_menu_navbar").show(); - return this._super(); - } -}); - -var TranslatorDialog = Widget.extend({ - events: _.extend({}, website.TopBar.prototype.events, { - 'hidden.bs.modal': 'destroy', - 'click button[data-action=activate]': function (ev) { - this.trigger('activate'); - }, - }), - template: 'website.TranslatorDialog', - start: function () { - this.$el.modal(); - }, -}); - -}); diff --git a/addons/website/static/src/js/website.zoomodoo.js b/addons/website/static/src/js/website.zoomodoo.js deleted file mode 100644 index 0e152d0e6de..00000000000 --- a/addons/website/static/src/js/website.zoomodoo.js +++ /dev/null @@ -1,292 +0,0 @@ -(function ($) { - /** - This code has been more that widely inspired by easyZoom library. - - Copyright 2013 Matt Hinchliffe - - Permission is hereby granted, free of charge, to any person obtaining - a copy of this software and associated documentation files (the - "Software"), to deal in the Software without restriction, including - without limitation the rights to use, copy, modify, merge, publish, - distribute, sublicense, and/or sell copies of the Software, and to - permit persons to whom the Software is furnished to do so, subject to - the following conditions: - - The above copyright notice and this permission notice shall be - included in all copies or substantial portions of the Software. - **/ - - 'use strict'; - - var dw, dh, rw, rh, lx, ly; - - var defaults = { - - // Attribute to retrieve the zoom image URL from. - linkTag: 'a', - linkAttribute: 'data-zoom-image', - - // event to trigger zoom - event: 'click', //or mousenter - - // Prevent clicks on the zoom image link. - preventClicks: true, - - // disable on mobile - disabledOnMobile: true, - - // Callback function to execute before the flyout is displayed. - beforeShow: $.noop, - - // Callback function to execute before the flyout is removed. - beforeHide: $.noop, - - // Callback function to execute when the flyout is displayed. - onShow: $.noop, - - // Callback function to execute when the flyout is removed. - onHide: $.noop, - - // Callback function to execute when the cursor is moved while over the image. - onMove: $.noop - - }; - - /** - * ZoomOdoo - * @constructor - * @param {Object} target - * @param {Object} options (Optional) - */ - function ZoomOdoo(target, options) { - this.$target = $(target); - this.opts = $.extend({}, defaults, options, this.$target.data()); - - if (this.isOpen === undefined) { - this._init(); - } - } - - /** - * Init - * @private - */ - ZoomOdoo.prototype._init = function() { - if (window.outerWidth > 467 || !this.opts.disabledOnMobile) { - this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target; - this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target; - this.$flyout = $('
    '); - - var $attach = this.$target; - if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { - $attach = this.$target.parents(this.opts.attach); - } - $attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this)); - $attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this)); - this.$target.parent().on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this)); - - if (this.opts.preventClicks) { - this.$target.on('click.zoomodoo', function(e) { e.preventDefault(); }); - } - } - }; - - /** - * Show - * @param {MouseEvent|TouchEvent} e - * @param {Boolean} testMouseOver (Optional) - */ - ZoomOdoo.prototype.show = function(e, testMouseOver) { - var w1, h1, w2, h2; - var self = this; - - if (this.opts.beforeShow.call(this) === false) return; - - if (!this.isReady) { - return this._loadImage(this.$link.attr(this.opts.linkAttribute), function() { - if (self.isMouseOver || !testMouseOver) { - self.show(e); - } - }); - } - - var $attach = this.$target; - if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) { - $attach = this.$target.parents(this.opts.attach); - } - $attach.parent().append(this.$flyout); - - w1 = this.$target.width(); - h1 = this.$target.height(); - - w2 = this.$flyout.width(); - h2 = this.$flyout.height(); - - dw = this.$zoom.width() - w2; - dh = this.$zoom.height() - h2; - - // For the case where the zoom image is actually smaller than - // the flyout. - if (dw < 0) dw = 0; - if (dh < 0) dh = 0; - - rw = dw / w1; - rh = dh / h1; - - this.isOpen = true; - - this.opts.onShow.call(this); - - if (e) { - this._move(e); - } - }; - - /** - * On enter - * @private - * @param {Event} e - */ - ZoomOdoo.prototype._onEnter = function(e) { - var touches = e.originalEvent.touches; - - this.isMouseOver = true; - if (!touches || touches.length === 1) { - e.preventDefault(); - this.show(e, true); - } - }; - - /** - * On move - * @private - * @param {Event} e - */ - ZoomOdoo.prototype._onMove = function(e) { - if (!this.isOpen) return; - - e.preventDefault(); - this._move(e); - }; - - /** - * On leave - * @private - */ - ZoomOdoo.prototype._onLeave = function() { - this.isMouseOver = false; - if (this.isOpen) { - this.hide(); - } - }; - - /** - * On load - * @private - * @param {Event} e - */ - ZoomOdoo.prototype._onLoad = function(e) { - // IE may fire a load event even on error so test the image dimensions - if (!e.currentTarget.width) return; - - this.isReady = true; - - this.$flyout.html(this.$zoom); - - if (e.data.call) { - e.data(); - } - }; - - - - /** - * Load image - * @private - * @param {String} href - * @param {Function} callback - */ - ZoomOdoo.prototype._loadImage = function(href, callback) { - var zoom = new Image(); - - this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this)); - - zoom.style.position = 'absolute'; - zoom.src = href; - }; - - /** - * Move - * @private - * @param {Event} e - */ - ZoomOdoo.prototype._move = function(e) { - if (e.type.indexOf('touch') === 0) { - var touchlist = e.touches || e.originalEvent.touches; - lx = touchlist[0].pageX; - ly = touchlist[0].pageY; - } else { - lx = e.pageX || lx; - ly = e.pageY || ly; - } - - var offset = this.$target.offset(); - var pt = ly - offset.top; - var pl = lx - offset.left; - var xt = Math.ceil(pt * rh); - var xl = Math.ceil(pl * rw); - - // Close if outside - if (xl < 0 || xt < 0 || xl > dw || xt > dh) { - this.hide(); - } else { - var top = xt * -1; - var left = xl * -1; - - this.$zoom.css({ - top: top, - left: left - }); - - this.opts.onMove.call(this, top, left); - } - - }; - - /** - * Hide - */ - ZoomOdoo.prototype.hide = function() { - if (!this.isOpen) return; - if (this.opts.beforeHide.call(this) === false) return; - - this.$flyout.detach(); - this.isOpen = false; - - this.opts.onHide.call(this); - }; - - - // jQuery plugin wrapper - $.fn.zoomOdoo = function(options) { - return this.each(function() { - var api = $.data(this, 'zoomOdoo'); - - if (!api) { - $.data(this, 'zoomOdoo', new ZoomOdoo(this, options)); - } else if (api.isOpen === undefined) { - api._init(); - } - }); - }; - - // AMD and CommonJS module compatibility - if (typeof define === 'function' && define.amd){ - define(function() { - return ZoomOdoo; - }); - } else if (typeof module !== 'undefined' && module.exports) { - module.exports = ZoomOdoo; - } - -})(jQuery); diff --git a/addons/website/static/src/js/widgets/ace.js b/addons/website/static/src/js/widgets/ace.js new file mode 100644 index 00000000000..2ce486fc1c8 --- /dev/null +++ b/addons/website/static/src/js/widgets/ace.js @@ -0,0 +1,96 @@ +odoo.define('website.ace', function (require) { +'use strict'; + +var Widget = require('web.Widget'); +var website = require('website.website'); +var mixins = require('web.mixins'); + +var ViewEditor = require('web_editor.ace'); + +var hash = "#advanced-view-editor"; + +var Ace = Widget.extend(mixins.ServiceProvider, { + events: { + "click a[data-action=ace]": function (e) { + e.preventDefault(); + this.launchAce(); + }, + }, + init: function () { + mixins.ServiceProvider.init.apply(this, arguments); + this._super.apply(this, arguments); + }, + start: function () { + if (window.location.hash.substr(0, hash.length) === hash) { + this.launchAce(); + } + return this._super.apply(this, arguments); + }, + launchAce: function () { + if (this.globalEditor) { + this.globalEditor.do_show(); + } else { + var currentHash = window.location.hash; + var indexOfView = currentHash.indexOf("?res="); + var initialResID = undefined; + if (indexOfView >= 0) { + initialResID = currentHash.substr(indexOfView + ("?res=".length)); + var parsedResID = parseInt(initialResID, 10); + if (parsedResID) { + initialResID = parsedResID; + } + } + + this.globalEditor = new ViewEditor(this, $(document.documentElement).data('view-xmlid'), { + initialResID: initialResID, + defaultBundlesRestriction: [ + "web.assets_frontend", + "website.assets_frontend", + ], + }); + this.globalEditor.appendTo(document.body); + + $("a[data-action=edit]").on("click", this.globalEditor.do_hide.bind(this.globalEditor)); + } + }, +}); + +/** + * Extend the default view editor so that the URL hash is updated with view id. + */ +ViewEditor = ViewEditor.extend({ + displayResource: function () { + this._super.apply(this, arguments); + this._updateHash(); + }, + saveResources: function () { + return this._super.apply(this, arguments).then((function () { + this._updateHash(); + window.location.reload(); + }).bind(this)); + }, + resetResource: function () { + return this._super.apply(this, arguments).then((function () { + window.location.reload(); + }).bind(this)); + }, + do_hide: function () { + this._super.apply(this, arguments); + window.location.hash = ""; + }, + _updateHash: function () { + window.location.hash = hash + "?res=" + this.selectedResource(); + }, +}); + +website.TopBar.include({ + start: function () { + this.ace = new Ace(); + return $.when( + this._super.apply(this, arguments), + this.ace.attachTo(this.$('#html_editor')) + ); + }, +}); + +}); diff --git a/addons/website/static/src/js/widgets/theme.js b/addons/website/static/src/js/widgets/theme.js new file mode 100644 index 00000000000..2db3d1c0e5d --- /dev/null +++ b/addons/website/static/src/js/widgets/theme.js @@ -0,0 +1,311 @@ +odoo.define('website.theme', function (require) { +'use strict'; + +var ajax = require('web.ajax'); +var core = require('web.core'); +var session = require('web.session'); +var Widget = require('web.Widget'); +var base = require('web_editor.base'); +var website = require('website.website'); + +var QWeb = core.qweb; + +ajax.jsonRpc('/web/dataset/call', 'call', { + 'model': 'ir.ui.view', + 'method': 'read_template', + 'args': ['website.theme_customize', base.get_context()] + }).done(function (data) { + QWeb.add_template(data); +}); + +ajax.jsonRpc('/web/dataset/call', 'call', { + 'model': 'ir.ui.view', + 'method': 'read_template', + 'args': ['web_editor.colorpicker', base.get_context()] + }).done(function (data) { + QWeb.add_template(data); +}); + +var Theme = Widget.extend({ + template: 'website.theme_customize', + events: { + 'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection', + 'mousedown label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) { + var self = this; + this.time_select = _.defer(function () { + var input = $(event.target).find('input').length ? $(event.target).find('input') : $(event.target).parent().find('input'); + self.on_select(input, event); + }); + }, + 'click .close': 'close', + 'click': 'click', + }, + start: function () { + var self = this; + this.timer = null; + this.reload = false; + this.flag = false; + this.active_select_tags(); + this.$inputs = this.$("input[data-xmlid],input[data-enable],input[data-disable]"); + setTimeout(function () {self.$el.addClass('in');}, 0); + this.keydown_escape = function (event) { + if (event.keyCode === 27) { + self.close(); + } + }; + $(document).on('keydown', this.keydown_escape); + return this.load_xml_data().then(function () { + self.flag = true; + }); + }, + active_select_tags: function () { + var uniqueID = 0; + var self = this; + var $selects = this.$('select:has(option[data-xmlid],option[data-enable],option[data-disable])'); + $selects.each(function () { + uniqueID++; + var $select = $(this); + var $options = $select.find('option[data-xmlid], option[data-enable], option[data-disable]'); + $options.each(function () { + var $option = $(this); + var $input = $(''); + $input.attr('id', $option.attr('id')); + $input.attr('data-xmlid', $option.data('xmlid')); + $input.attr('data-enable', $option.data('enable')); + $input.attr('data-disable', $option.data('disable')); + $option.removeAttr('id'); + $option.data('input', $input); + $input.on('update', function () { + $option.attr('selected', $(this).prop("checked")); + }); + self.$el.append($input); + }); + $select.data("value", $options.first()); + $options.first().attr("selected", true); + }); + $selects.change(function () { + var $option = $(this).find('option:selected'); + $(this).data("value").data("input").prop("checked", true).change(); + $(this).data("value", $option); + $option.data("input").change(); + }); + }, + load_xml_data: function () { + var self = this; + $('#theme_error').remove(); + return ajax.jsonRpc('/website/theme_customize_get', 'call', { + 'xml_ids': this.get_xml_ids(this.$inputs) + }).done(function (data) { + self.$inputs.filter('[data-xmlid=""]').prop("checked", true).change(); + self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])').each(function () { + if (!_.difference(self.get_xml_ids($(this)), data[1]).length) { + $(this).prop("checked", false).trigger("change", true); + } + if (!_.difference(self.get_xml_ids($(this)), data[0]).length) { + $(this).prop("checked", true).trigger("change", true); + } + }); + }).fail(function (d, error) { + $('body').prepend($('
    ').text(error.data.message)); + }); + }, + get_inputs: function (string) { + return this.$inputs.filter('#'+string.split(/\s*,\s*/).join(", #")); + }, + get_xml_ids: function ($inputs) { + var xml_ids = []; + $inputs.each(function () { + if ($(this).data('xmlid') && $(this).data('xmlid').length) { + xml_ids = xml_ids.concat($(this).data('xmlid').split(/\s*,\s*/)); + } + }); + return xml_ids; + }, + update_style: function (enable, disable, reload) { + if (this.$el.hasClass("loading")) { + return; + } + this.$el.addClass('loading'); + + if (!reload && session.debug !== "assets") { + var self = this; + return ajax.jsonRpc('/website/theme_customize', 'call', { + enable: enable, + disable: disable, + get_bundle: true, + }).then(function (bundleHTML) { + var $links = $('link[href*=".assets_frontend"]'); + var $newLinks = $(bundleHTML).filter('link'); + + var linksLoaded = $.Deferred(); + var nbLoaded = 0; + $newLinks.on('load', function (e) { + if (++nbLoaded >= $newLinks.length) { + linksLoaded.resolve(); + } + }); + $newLinks.on('error', function (e) { + linksLoaded.reject(); + window.location.hash = "theme=true"; + window.location.reload(); + }); + + $links.last().after($newLinks); + return linksLoaded.then(function () { + $links.remove(); + self.$el.removeClass('loading'); + }); + }); + } else { + var href = '/website/theme_customize_reload'+ + '?href='+encodeURIComponent(window.location.href)+ + '&enable='+encodeURIComponent(enable.join(","))+ + '&disable='+encodeURIComponent(disable.join(",")); + window.location.href = href; + return $.Deferred(); + } + }, + enable_disable: function ($inputs, enable) { + $inputs.each(function () { + var check = $(this).prop("checked"); + var $label = $(this).closest("label"); + $(this).prop("checked", enable); + if (enable) $label.addClass("checked"); + else $label.removeClass("checked"); + if (check != enable) { + $(this).change(); + } + }); + }, + change_selection: function (event, init_mode) { + var self = this; + clearTimeout(this.time_select); + + if (this.$el.hasClass("loading")) return; // prevent to change selection when css is loading + + var $option = $(event.target).is('input') ? $(event.target) : $("input", event.target), + $options = $option, + checked = $option.prop("checked"); + + if (checked) { + if($option.data('enable')) { + var $inputs = this.get_inputs($option.data('enable')); + $options = $options.add($inputs.filter(':not(:checked)')); + this.enable_disable($inputs, true); + } + if($option.data('disable')) { + var $inputs = this.get_inputs($option.data('disable')); + $options = $options.add($inputs.filter(':checked')); + this.enable_disable($inputs, false); + } + $option.closest("label").addClass("checked"); + } else { + $option.closest("label").removeClass("checked"); + } + + var $enable = this.$inputs.filter('[data-xmlid]:checked'); + $enable.closest("label").addClass("checked"); + var $disable = this.$inputs.filter('[data-xmlid]:not(:checked)'); + $disable.closest("label").removeClass("checked"); + + var $sets = this.$inputs.filter('input[data-enable]:not([data-xmlid]), input[data-disable]:not([data-xmlid])'); + $sets.each(function () { + var $set = $(this); + var checked = true; + if ($set.data("enable")) { + self.get_inputs($(this).data("enable")).each(function () { + if (!$(this).prop("checked")) checked = false; + }); + } + if ($set.data("disable")) { + self.get_inputs($(this).data("disable")).each(function () { + if ($(this).prop("checked")) checked = false; + }); + } + if (checked) { + $set.prop("checked", true).closest("label").addClass("checked"); + } else { + $set.prop("checked", false).closest("label").removeClass("checked"); + } + $set.trigger('update'); + }); + + if (this.flag && $option.data('reload') && document.location.href.match(new RegExp( $option.data('reload') ))) { + this.reload = true; + } + + clearTimeout(this.timer); + if (this.flag) { + this.timer = _.defer(function () { + if (!init_mode) self.on_select($options, event); + self.update_style(self.get_xml_ids($enable), self.get_xml_ids($disable), self.reload); + self.reload = false; + }); + } else { + this.timer = _.defer(function () { + if (!init_mode) self.on_select($options, event); + self.reload = false; + }); + } + }, + /* Method call when the user change the selection or click on an input + * @values: all changed inputs + */ + on_select: function ($inputs, event) { + clearTimeout(this.time_select); + }, + click: function (event) { + if (!$(event.target).closest("#theme_customize_modal > *").length) { + this.close(); + } + }, + close: function () { + var self = this; + $(document).off('keydown', this.keydown_escape); + $('#theme_error').remove(); + $('link[href*=".assets_"]').removeAttr('data-loading'); + this.$el.removeClass('in'); + this.$el.addClass('out'); + setTimeout(function () {self.destroy();}, 500); + } +}); + +function themeError(message) { + var _t = core._t; + + if (message.indexOf('lessc')) { + message = '' + message + "

    " + _t("Please install or update node-less"); + } + + website.error(_t("Theme Error"), message); +} + +function theme_customize() { + if (Theme.open && !Theme.open.isDestroyed()) return; + Theme.open = new Theme(); + Theme.open.appendTo("body"); + + var error = window.getComputedStyle(document.body, ':before').getPropertyValue('content'); + if (error && error !== 'none') { + themeError(eval(error)); + } +} + +website.TopBar.include({ + start: function () { + var self = this; + base.ready().then(function () { + self.$el.on('click', "#theme_customize a", theme_customize); + if ((window.location.hash || "").indexOf("theme=true") !== -1) { + theme_customize(); + window.location.hash = ""; + } + }); + + return this._super(); + } +}); + +return Theme; + +}); -- cgit v1.2.3