Example #1
1
odoo.define('website.theme', 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 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 () {
                self.on_select($(event.target).find('input'), event);
            });
        },
        'click .close': 'close',
        'click': 'click',
    },
    start: function () {
        var self = this;
        this.timer = null;
        this.reload = false;
        this.flag = false;
        this.$el.addClass("theme_customize_modal");
        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 style="display: none;" type="radio" name="theme_customize_modal-select-'+uniqueID+'"/>');
                $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($('<div id="theme_error"/>').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;
    },
    compute_stylesheets: function () {
        var self = this;
        self.has_error = false;
        $('link[href*=".assets_"]').attr('data-loading', true);
        function theme_customize_css_onload() {
            if ($('link[data-loading]').size()) {
                $('body').toggleClass('theme_customize_css_loading');
                setTimeout(theme_customize_css_onload, 50);
            } else {
                $('body').removeClass('theme_customize_css_loading');
                self.$el.removeClass("loading");

                if (window.getComputedStyle($('button[data-toggle="collapse"]:first')[0]).getPropertyValue('position') === 'static' ||
                    window.getComputedStyle($('#theme_customize_modal')[0]).getPropertyValue('display') === 'none') {
                    if (self.has_error) {
                        window.location.hash = "theme=true";
                        window.location.reload();
                    } else {
                        self.has_error = true;
                        $('link[href*=".assets_"][data-error]').removeAttr('data-error').attr('data-loading', true);
                        self.update_stylesheets();
                        setTimeout(theme_customize_css_onload, 50);
                    }
                }
            }
        }
        theme_customize_css_onload();
    },
    update_stylesheets: function () {
        $('link[href*=".assets_"]').each(function update () {
            var $style = $(this);
            var href = $style.attr("href").replace(/[^\/]+$/, new Date().getTime());
            var $asset = $('<link rel="stylesheet" href="'+href+'"/>');
            $asset.attr("onload", "$(this).prev().attr('disable', true).remove(); $(this).removeAttr('onload').removeAttr('onerror');");
            $asset.attr("onerror", "$(this).prev().removeAttr('data-loading').attr('data-error','loading'); $(this).attr('disable', true).remove();");
            $style.after($asset);
        });
    },
    update_style: function (enable, disable, reload) {
        var self = this;
        if (this.$el.hasClass("loading")) return;
        this.$el.addClass("loading");

        if (!reload && $('link[href*=".assets_"]').size()) {
            this.compute_stylesheets();
            return ajax.jsonRpc('/website/theme_customize', 'call', {
                    'enable': enable,
                    'disable': disable
                }).then(function () {
                    self.update_stylesheets();
                });
        } 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 = '<span class="text-muted">' + message + "</span><br/><br/>" + _t("Please install or update node-less");
    }

    var $error = $( QWeb.render('website.error_dialog', {
        title: _t("Theme Error"),
        message: message
    }));
    $error.appendTo("body").modal();
    $error.on('hidden.bs.modal', function () {
        $(this).remove();
    });
}

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 () {
        this.$el.on('click', "#theme_customize a", theme_customize);
        if ((window.location.hash || "").indexOf("theme=true") !== -1) {
            theme_customize();
            window.location.hash = "";
        }
        return this._super();
    }
});

});
Example #2
0
odoo.define('web.test_utils', function (require) {
"use strict";

/**
 * Test Utils
 *
 * In this module, we define various utility functions to help simulate a mock
 * environment as close as possible as a real environment.  The main function is
 * certainly createView, which takes a bunch of parameters and give you back an
 * instance of a view, appended in the dom, ready to be tested.
 */

var ActionManager = require('web.ActionManager');
var ajax = require('web.ajax');
var basic_fields = require('web.basic_fields');
var config = require('web.config');
var ControlPanel = require('web.ControlPanel');
var core = require('web.core');
var DebugManager = require('web.DebugManager');
var dom = require('web.dom');
var session = require('web.session');
var MockServer = require('web.MockServer');
var Widget = require('web.Widget');

var DebouncedField = basic_fields.DebouncedField;

/**
 * intercepts an event bubbling up the widget hierarchy. The event intercepted
 * must be a "custom event", i.e. an event generated by the method 'trigger_up'.
 *
 * Note that this method really intercepts the event if @propagate is not set.
 * It will not be propagated further, and even the handlers on the target will
 * not fire.
 *
 * @param {Widget} widget the target widget (any Odoo widget)
 * @param {string} eventName description of the event
 * @param {function} fn callback executed when the even is intercepted
 * @param {boolean} [propagate=false]
 */
function intercept(widget, eventName, fn, propagate) {
    var _trigger_up = widget._trigger_up.bind(widget);
    widget._trigger_up = function (event) {
        if (event.name === eventName) {
            fn(event);
            if (!propagate) { return; }
        }
        _trigger_up(event);
    };
}

/**
 * logs all event going through the target widget.
 *
 * @param {Widget} widget
 */
function observe(widget) {
    var _trigger_up = widget._trigger_up.bind(widget);
    widget._trigger_up = function (event) {
        console.log('%c[event] ' + event.name, 'color: blue; font-weight: bold;', event);
        _trigger_up(event);
    };
}

/**
 * create and return an instance of ActionManager with all rpcs going through a
 * mock method using the data, actions and archs objects as sources.
 *
 * @param {Object} [params]
 * @param {Object} [params.actions] the actions given to the mock server
 * @param {Object} [params.archs] this archs given to the mock server
 * @param {Object} [params.data] the business data given to the mock server
 * @param {boolean} [params.debug]
 * @param {function} [params.mockRPC]
 * @returns {ActionManager}
 */
var createActionManager = function (params) {
    params = params || {};
    var $target = $('#qunit-fixture');
    if (params.debug) {
        $target = $('body');
        $target.addClass('debug');
    }

    var widget = new Widget();
    // when 'document' addon is installed, the sidebar does a 'search_read' on
    // model 'ir_attachment' each time a record is open, so we monkey-patch
    // 'mockRPC' to mute those RPCs, so that the tests can be written uniformly,
    // whether or not 'document' is installed
    var mockRPC = params.mockRPC;
    _.extend(params, {
        mockRPC: function (route, args) {
            if (args.model === 'ir.attachment') {
                return $.when([]);
            }
            if (mockRPC) {
                return mockRPC.apply(this, arguments);
            }
            return this._super.apply(this, arguments);
        },
    });
    addMockEnvironment(widget, _.defaults(params, { debounce: false }));
    widget.prependTo($target);
    widget.$el.addClass('o_web_client');
    if (config.device.isMobile) {
        widget.$el.addClass('o_touch_device');
    }

    var userContext = params.context && params.context.user_context || {};
    var actionManager = new ActionManager(widget, userContext);

    var originalDestroy = ActionManager.prototype.destroy;
    actionManager.destroy = function () {
        actionManager.destroy = originalDestroy;
        widget.destroy();
    };
    actionManager.appendTo(widget.$el);

    return actionManager;
};
/**
 * Create and return an instance of DebugManager with all rpcs going through a
 * mock method, assuming that the user has access rights, and is an admin.
 *
 * @param {Object} [params={}]
 */
var createDebugManager = function (params) {
    params = params || {};
    var mockRPC = params.mockRPC;
    _.extend(params, {
        mockRPC: function (route, args) {
            if (args.method === 'check_access_rights') {
                return $.when(true);
            }
            if (args.method === 'xmlid_to_res_id') {
                return $.when(true);
            }
            if (mockRPC) {
                return mockRPC.apply(this, arguments);
            }
            return this._super.apply(this, arguments);
        },
        session: {
            user_has_group: function (group) {
                if (group === 'base.group_no_one') {
                    return $.when(true);
                }
                return this._super.apply(this, arguments);
            },
        },
    });
    var debugManager = new DebugManager();
    addMockEnvironment(debugManager, params);
    return debugManager;
};

/**
 * performs a fields_view_get, and mocks the postprocessing done by the
 * data_manager to return an equivalent structure.
 *
 * @param {MockServer} server
 * @param {Object} params
 * @param {string} params.model
 * @returns {Object} an object with 3 keys: arch, fields and viewFields
 */
function fieldsViewGet(server, params) {
    var fieldsView = server.fieldsViewGet(params);
    // mock the structure produced by the DataManager
    fieldsView.viewFields = fieldsView.fields;
    fieldsView.fields = server.fieldsGet(params.model);
    return fieldsView;
}

/**
 * create a view synchronously.  This method uses the createAsyncView method.
 * Most views are synchronous, so the deferred can be resolved immediately and
 * this method will work.
 *
 * Be careful, if for some reason a view is async, this method will crash.
 * @see createAsyncView
 *
 * @param {Object} params will be given to createAsyncView
 * @returns {AbstractView}
 */
function createView(params) {
    var view;
    createAsyncView(params).then(function (result) {
        view = result;
    });
    if (!view) {
        throw "The view that you are trying to create is async. Please use createAsyncView instead";
    }
    return view;
}

/**
 * create a view from various parameters.  Here, a view means a javascript
 * instance of an AbstractView class, such as a form view, a list view or a
 * kanban view.
 *
 * It returns the instance of the view, properly created, with all rpcs going
 * through a mock method using the data object as source, and already loaded/
 * started.
 *
 * Most views can be tested synchronously (@see createView), but some view have
 * external dependencies (like lazy loaded libraries). In that case, it is
 * necessary to use this method.
 *
 * @param {Object} params
 * @param {string} params.arch the xml (arch) of the view to be instantiated
 * @param {any[]} [params.domain] the initial domain for the view
 * @param {Object} [params.context] the initial context for the view
 * @param {Object} [params.debug=false] if true, the widget will be appended in
 *   the DOM. Also, RPCs and uncaught OdooEvent will be logged
 * @param {string[]} [params.groupBy] the initial groupBy for the view
 * @param {integer} [params.fieldDebounce=0] the debounce value to use for the
 *   duration of the test.
 * @param {AbstractView} params.View the class that will be instantiated
 * @param {string} params.model a model name, will be given to the view
 * @param {Object} params.intercepts an object with event names as key, and
 *   callback as value.  Each key,value will be used to intercept the event.
 *   Note that this is particularly useful if you want to intercept events going
 *   up in the init process of the view, because there are no other way to do it
 *   after this method returns
 * @returns {Deferred<AbstractView>} resolves with the instance of the view
 */
function createAsyncView(params) {
    var $target = $('#qunit-fixture');
    var widget = new Widget();
    if (params.debug) {
        $target = $('body');
        $target.addClass('debug');
    }

    // add mock environment: mock server, session, fieldviewget, ...
    var mockServer = addMockEnvironment(widget, params);
    var viewInfo = fieldsViewGet(mockServer, params);
    // create the view
    var viewOptions = {
        modelName: params.model || 'foo',
        ids: 'res_id' in params ? [params.res_id] : undefined,
        currentId: 'res_id' in params ? params.res_id : undefined,
        domain: params.domain || [],
        context: params.context || {},
        groupBy: params.groupBy || [],
    };
    if (params.hasSelectors) {
        viewOptions.hasSelectors = params.hasSelectors;
    }

    _.extend(viewOptions, params.viewOptions);

    var view = new params.View(viewInfo, viewOptions);

    // reproduce the DOM environment of views
    var $web_client = $('<div>').addClass('o_web_client').prependTo($target);
    var controlPanel = new ControlPanel(widget);
    controlPanel.appendTo($web_client);
    var $content = $('<div>').addClass('o_content').appendTo($web_client);

    if (params.interceptsPropagate) {
        _.each(params.interceptsPropagate, function (cb, name) {
            intercept(widget, name, cb, true);
        });
    }

    return view.getController(widget).then(function (view) {
        // override the view's 'destroy' so that it calls 'destroy' on the widget
        // instead, as the widget is the parent of the view and the mockServer.
        view.__destroy = view.destroy;
        view.destroy = function () {
            // remove the override to properly destroy the view and its children
            // when it will be called the second time (by its parent)
            delete view.destroy;
            widget.destroy();
        };

        // link the view to the control panel
        view.set_cp_bus(controlPanel.get_bus());

        // render the view in a fragment as they must be able to render correctly
        // without being in the DOM
        var fragment = document.createDocumentFragment();
        return view.appendTo(fragment).then(function () {
            dom.append($content, fragment, {
                callbacks: [{ widget: view }],
                in_DOM: true,
            });
            view.$el.on('click', 'a', function (ev) {
                ev.preventDefault();
            });

            return view;
        });
    });
}

/**
 * Patch window.Date so that the time starts its flow from the provided Date.
 *
 * Usage:
 *
 *  ```
 *  var unpatchDate = testUtils.patchDate(2018, 0, 10, 17, 59, 30)
 *  new window.Date(); // "Wed Jan 10 2018 17:59:30 GMT+0100 (Central European Standard Time)"
 *  ... // 5 hours delay
 *  new window.Date(); // "Wed Jan 10 2018 22:59:30 GMT+0100 (Central European Standard Time)"
 *  ...
 *  unpatchDate();
 *  new window.Date(); // actual current date time
 *  ```
 *
 * @param {integer} year
 * @param {integer} month index of the month, starting from zero.
 * @param {integer} day the day of the month.
 * @param {integer} hours the digits for hours (24h)
 * @param {integer} minutes
 * @param {integer} seconds
 * @returns {function} a callback to unpatch window.Date.
 */
function patchDate(year, month, day, hours, minutes, seconds) {
    var RealDate = window.Date;
    var actualDate = new RealDate();
    var fakeDate = new RealDate(year, month, day, hours, minutes, seconds);
    var timeInterval = actualDate.getTime() - (fakeDate.getTime());

    Date = (function (NativeDate) {
        function Date(Y, M, D, h, m, s, ms) {
            var length = arguments.length;
            if (arguments.length > 0) {
                var date = length == 1 && String(Y) === Y ? // isString(Y)
                    // We explicitly pass it through parse:
                    new NativeDate(Date.parse(Y)) :
                    // We have to manually make calls depending on argument
                    // length here
                    length >= 7 ? new NativeDate(Y, M, D, h, m, s, ms) :
                    length >= 6 ? new NativeDate(Y, M, D, h, m, s) :
                    length >= 5 ? new NativeDate(Y, M, D, h, m) :
                    length >= 4 ? new NativeDate(Y, M, D, h) :
                    length >= 3 ? new NativeDate(Y, M, D) :
                    length >= 2 ? new NativeDate(Y, M) :
                    length >= 1 ? new NativeDate(Y) :
                                  new NativeDate();
                // Prevent mixups with unfixed Date object
                date.constructor = Date;
                return date;
            } else {
                var date = new NativeDate();
                var time = date.getTime();
                time -= timeInterval;
                date.setTime(time);
                return date;
            }
        }

        // Copy any custom methods a 3rd party library may have added
        for (var key in NativeDate) {
            Date[key] = NativeDate[key];
        }

        // Copy "native" methods explicitly; they may be non-enumerable
        Date.now = NativeDate.now;
        Date.UTC = NativeDate.UTC;
        Date.prototype = NativeDate.prototype;
        Date.prototype.constructor = Date;

        // Upgrade Date.parse to handle simplified ISO 8601 strings
        Date.parse = NativeDate.parse;
        return Date;
    })(Date);

    return function () { window.Date = RealDate; };
}

/**
 * Add a mock environment to a widget.  This helper function can simulate
 * various kind of side effects, such as mocking RPCs, changing the session,
 * or the translation settings.
 *
 * The simulated environment lasts for the lifecycle of the widget, meaning it
 * disappears when the widget is destroyed.  It is particularly relevant for the
 * session mocks, because the previous session is restored during the destroy
 * call.  So, it means that you have to be careful and make sure that it is
 * properly destroyed before another test is run, otherwise you risk having
 * interferences between tests.
 *
 * @param {Widget} widget
 * @param {Object} params
 * @param {Object} [params.archs] a map of string [model,view_id,view_type] to
 *   a arch object. It is used to mock answers to 'load_views' custom events.
 *   This is useful when the widget instantiate a formview dialog that needs
 *   to load a particular arch.
 * @param {string} [params.currentDate] a string representation of the current
 *   date. It is given to the mock server.
 * @param {Object} params.data the data given to the created mock server. It is
 *   used to generate mock answers for every kind of routes supported by odoo
 * @param {number} [params.debug] if set to true, logs RPCs and uncaught Odoo
 *   events.
 * @param {function} [params.mockRPC] a function that will be used to override
 *   the _performRpc method from the mock server. It is really useful to add
 *   some custom rpc mocks, or to check some assertions.
 * @param {Object} [params.session] if it is given, it will be used as answer
 *   for all calls to this.getSession() by the widget, of its children.  Also,
 *   it will be used to extend the current, real session. This side effect is
 *   undone when the widget is destroyed.
 * @param {Object} [params.translateParameters] if given, it will be used to
 *   extend the core._t.database.parameters object. After the widget
 *   destruction, the original parameters will be restored.
 * @param {Object} [params.intercepts] an object with event names as key, and
 *   callback as value.  Each key,value will be used to intercept the event.
 *   Note that this is particularly useful if you want to intercept events going
 *   up in the init process of the view, because there are no other way to do it
 *   after this method returns. Some events ('call_service', "load_views",
 *   "get_session", "load_filters") have a special treatment beforehand.
 * @param {Object} [params.services={}] list of services to load in
 *   addition to the ajax service. For instance, if a test needs the local
 *   storage service in order to work, it can provide a mock version of it.
 * @param {boolean} [debounce=true] set to false to completely remove the
 *   debouncing, forcing the handler to be called directly (not on the next
 *   execution stack, like it does with delay=0).
 * @param {boolean} [throttle=false] set to true to keep the throttling, which
 *   is completely removed by default.
 *
 * @returns {MockServer} the instance of the mock server, created by this
 *   function. It is necessary for createAsyncView so that method can call some
 *   other methods on it.
 */
function addMockEnvironment(widget, params) {
    var Server = MockServer;
    params.services = params.services || {};
    if (params.mockRPC) {
        Server = MockServer.extend({ _performRpc: params.mockRPC });
    }
    if (params.debug) {
        observe(widget);
        var separator = window.location.href.indexOf('?') !== -1 ? "&" : "?";
        var url = window.location.href + separator + 'testId=' + QUnit.config.current.testId;
        console.log('%c[debug] debug mode activated', 'color: blue; font-weight: bold;', url);
    }

    var mockServer = new Server(params.data, {
        actions: params.actions,
        archs: params.archs,
        currentDate: params.currentDate,
        debug: params.debug,
        widget: widget,
    });

    // make sure images do not trigger a GET on the server
    $('body').on('DOMNodeInserted.removeSRC', function (event) {
        removeSrcAttribute(event.target, widget);
    });

    // make sure the debounce value for input fields is set to 0
    var initialDebounceValue = DebouncedField.prototype.DEBOUNCE;
    DebouncedField.prototype.DEBOUNCE = params.fieldDebounce || 0;
    var initialSession, initialConfig, initialParameters, initialDebounce, initialThrottle;
    initialSession = _.extend({}, session);
    session.getTZOffset = function () {
        return 0; // by default, but may be overriden in specific tests
    };
    if ('session' in params) {
        _.extend(session, params.session);
    }
    if ('config' in params) {
        initialConfig = _.clone(config);
        initialConfig.device = _.clone(config.device);
        if ('device' in params.config) {
            _.extend(config.device, params.config.device);
            config.device.isMobile = config.device.size_class <= config.device.SIZES.VSM;
        }
        if ('debug' in params.config) {
            config.debug = params.config.debug;
        }
    }
    if ('translateParameters' in params) {
        initialParameters = _.extend({}, core._t.database.parameters);
        _.extend(core._t.database.parameters, params.translateParameters);
    }
    if (params.debounce === false) {
        initialDebounce = _.debounce;
        _.debounce = function (func) {
            return func;
        };
    }
    if (!('throttle' in params) || !params.throttle) {
        initialThrottle = _.throttle;
        _.throttle = function (func) {
            return func;
        };
    }

    var widgetDestroy = widget.destroy;
    widget.destroy = function () {
        // clear the caches (e.g. data_manager, ModelFieldSelector) when the
        // widget is destroyed, at the end of each test to avoid collisions
        core.bus.trigger('clear_cache');

        DebouncedField.prototype.DEBOUNCE = initialDebounceValue;
        if (params.debounce === false) {
            _.debounce = initialDebounce;
        }
        if (!('throttle' in params) || !params.throttle) {
            _.throttle = initialThrottle;
        }

        var key;
        if ('session' in params) {
            for (key in session) {
                delete session[key];
            }
        }
        _.extend(session, initialSession);
        if ('config' in params) {
            for (key in config) {
                delete config[key];
            }
            _.extend(config, initialConfig);
        }
        if ('translateParameters' in params) {
            for (key in core._t.database.parameters) {
                delete core._t.database.parameters[key];
            }
            _.extend(core._t.database.parameters, initialParameters);
        }

        $('body').off('DOMNodeInserted.removeSRC');
        $('.blockUI').remove();

        widgetDestroy.call(this);
    };

    // Dispatch service calls
    // Note: some services could call other services at init,
    // Which is why we have to init services after that
    var services = {};
    intercept(widget, 'call_service', function (ev) {
        var args, result;
        if (services[ev.data.service]) {
            var service = services[ev.data.service];
            args = (ev.data.args || []);
            result = service[ev.data.method].apply(service, args);
        } else if (ev.data.service === 'ajax') {
            // use ajax service that is mocked by the server
            var route = ev.data.args[0];
            args = ev.data.args[1];
            result = mockServer.performRpc(route, args);
        }
        ev.data.callback(result);
    });

    intercept(widget, 'load_action', function (event) {
        mockServer.performRpc('/web/action/load', {
            kwargs: {
                action_id: event.data.actionID,
                additional_context: event.data.context,
            },
        }).then(function (action) {
            event.data.on_success(action);
        });
    });

    intercept(widget, "load_views", function (event) {
        mockServer.performRpc('/web/dataset/call_kw/' + event.data.modelName, {
            args: [],
            kwargs: {
                context: event.data.context,
                options: event.data.options,
                views: event.data.views,
            },
            method: 'load_views',
            model: event.data.modelName,
        }).then(function (views) {
            views = _.mapObject(views, function (viewParams) {
                return fieldsViewGet(mockServer, viewParams);
            });
            event.data.on_success(views);
        });
    });

    intercept(widget, "get_session", function (event) {
        event.data.callback(session);
    });

    intercept(widget, "load_filters", function (event) {
        if (params.debug) {
            console.log('[mock] load_filters', event.data);
        }
        event.data.on_success([]);
    });

    // make sure all Odoo events bubbling up are intercepted
    if ('intercepts' in params) {
        _.each(params.intercepts, function (cb, name) {
            intercept(widget, name, cb);
        });
    }

    // Deploy services
    var done = false;
    var servicesToDeploy = _.clone(params.services);
    if (!servicesToDeploy.ajax) {
        services.ajax = null; // use mocked ajax from mocked server
    }
    while (!done) {
        var serviceName = _.findKey(servicesToDeploy, function (Service) {
            return !_.some(Service.prototype.dependencies, function (depName) {
                return !_.has(services, depName);
            });
        });
        if (serviceName) {
            var Service = servicesToDeploy[serviceName];
            var service = services[serviceName] = new Service(widget);
            delete servicesToDeploy[serviceName];

            intercept(service, "get_session", function (event) {
                event.data.callback(session);
            });

            service.start();
        } else {
            var serviceNames = _.keys(servicesToDeploy);
            if (serviceNames.length) {
                console.warn("Non loaded services:", serviceNames);
            }
            done = true;
        }
    }

    return mockServer;
}

/**
 * create a model from given parameters.
 *
 * @param {Object} params This object will be given to addMockEnvironment, so
 *   any parameters from that method applies
 * @param {Class} params.Model the model class to use
 * @returns {Model}
 */
function createModel(params) {
    var widget = new Widget();

    var model = new params.Model(widget);

    addMockEnvironment(widget, params);

    // override the model's 'destroy' so that it calls 'destroy' on the widget
    // instead, as the widget is the parent of the model and the mockServer.
    model.destroy = function () {
        // remove the override to properly destroy the model when it will be
        // called the second time (by its parent)
        delete model.destroy;
        widget.destroy();
    };

    return model;
}

/**
 * create a widget parent from given parameters.
 *
 * @param {Object} params This object will be given to addMockEnvironment, so
 *   any parameters from that method applies
 * @returns {Widget}
 */
function createParent(params) {
    var widget = new Widget();
    addMockEnvironment(widget, params);
    return widget;
}

/**
 * simulate a drag and drop operation between 2 jquery nodes: $el and $to.
 * This is a crude simulation, with only the mousedown, mousemove and mouseup
 * events, but it is enough to help test drag and drop operations with jqueryUI
 * sortable.
 *
 * @param {jqueryElement} $el
 * @param {jqueryElement} $to
 * @param {Object} [options]
 * @param {string|Object} [options.position='center'] target position:
 *   can either be one of {'top', 'bottom', 'left', 'right'} or
 *   an object with two attributes (top and left))
 * @param {boolean} [options.disableDrop=false] whether to trigger the drop action
 * @param {boolean} [options.continueMove=false] whether to trigger the
 *   mousedown action (will only work after another call of this function with
 *   without this option)
 */
function dragAndDrop($el, $to, options) {
    var position = (options && options.position) || 'center';
    var elementCenter = $el.offset();
    var toOffset = $to.offset();

    if (_.isObject(position)) {
        toOffset.top += position.top;
        toOffset.left += position.left;
    } else {
        toOffset.top += $to.outerHeight() / 2;
        toOffset.left += $to.outerWidth() / 2;
        var vertical_offset = (toOffset.top < elementCenter.top) ? -1 : 1;
        if (position === 'top') {
            toOffset.top -= $to.outerHeight() / 2 + vertical_offset;
        } else if (position === 'bottom') {
            toOffset.top += $to.outerHeight() / 2 - vertical_offset;
        } else if (position === 'left') {
            toOffset.left -= $to.outerWidth() / 2;
        } else if (position === 'right') {
            toOffset.left += $to.outerWidth() / 2;
        }
    }

    if ($to[0].ownerDocument !== document) {
        // we are in an iframe
        var bound = $('iframe')[0].getBoundingClientRect();
        toOffset.left += bound.left;
        toOffset.top += bound.top;
    }
    $el.trigger($.Event("mouseenter"));
    if (!(options && options.continueMove)) {
        elementCenter.left += $el.outerWidth() / 2;
        elementCenter.top += $el.outerHeight() / 2;

        $el.trigger($.Event("mousedown", {
            which: 1,
            pageX: elementCenter.left,
            pageY: elementCenter.top
        }));
    }

    $el.trigger($.Event("mousemove", {
        which: 1,
        pageX: toOffset.left,
        pageY: toOffset.top
    }));

    if (!(options && options.disableDrop)) {
        $el.trigger($.Event("mouseup", {
            which: 1,
            pageX: toOffset.left,
            pageY: toOffset.top
        }));
    } else {
        // It's impossible to drag another element when one is already
        // being dragged. So it's necessary to finish the drop when the test is
        // over otherwise it's impossible for the next tests to drag and
        // drop elements.
        $el.on("remove", function () {
            $el.trigger($.Event("mouseup"));
        });
    }
}

/**
 * simulate a mouse event with a custom event who add the item position. This is
 * sometimes necessary because the basic way to trigger an event (such as
 * $el.trigger('mousemove')); ) is too crude for some uses.
 *
 * @param {jqueryElement} $el
 * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
 */
function triggerMouseEvent($el, type) {
    var pos = $el.offset();
    var e = new $.Event(type);
    e.pageX = e.layerX = e.screenX = pos.left;
    e.pageY = e.layerY = e.screenY = pos.top;
    e.which = 1;
    $el.trigger(e);
}

/**
 * simulate a mouse event with a custom event on a position x and y. This is
 * sometimes necessary because the basic way to trigger an event (such as
 * $el.trigger('mousemove')); ) is too crude for some uses.
 *
 * @param {integer} x
 * @param {integer} y
 * @param {string} type a mouse event type, such as 'mousedown' or 'mousemove'
 */
function triggerPositionalMouseEvent(x, y, type) {
    var ev = document.createEvent("MouseEvent");
    var el = document.elementFromPoint(x, y);
    ev.initMouseEvent(
        type,
        true /* bubble */,
        true /* cancelable */,
        window, null,
        x, y, x, y, /* coordinates */
        false, false, false, false, /* modifier keys */
        0 /*left button*/, null
    );
    el.dispatchEvent(ev);
    return el;
}

/**
 * simulate a keypress event for a given character
 * @param {string} the character
 */
function triggerKeypressEvent(char) {
    var keycode;
    if (char === "Enter") {
        keycode = $.ui.keyCode.ENTER;
    } else {
        keycode = char.charCodeAt(0);
    }
    return $('body').trigger($.Event('keypress', { which: keycode, keyCode: keycode }));
}

/**
 * Removes the src attribute on images and iframes to prevent not found errors,
 * and optionally triggers an rpc with the src url as route on a widget.
 * This method is critical and must be fastest (=> no jQuery, no underscore)
 *
 * @param {DOM Node} el
 * @param {[Widget]} widget the widget on which the rpc should be performed
 */
function removeSrcAttribute(el, widget) {
    var nodes;
    if (el.nodeName === 'IMG' || el.nodeName === 'IFRAME') {
        nodes = [el];
    } else {
        nodes = Array.prototype.slice.call(el.getElementsByTagName('img'))
            .concat(Array.prototype.slice.call(el.getElementsByTagName('iframe')));
    }
    var node;
    while (node = nodes.pop()) {
        var src = node.attributes.src && node.attributes.src.value;
        if (src && src !== 'about:blank') {
            var $el = $(node);
            node.setAttribute('data-src', src);
            if (node.nodeName === 'IMG') {
                node.attributes.removeNamedItem('src');
            } else {
                node.setAttribute('src', 'about:blank');
            }
            if (widget) {
                widget._rpc({ route: src });
            }
        }
    }
}

var patches = {};
/**
 * Patches a given Class or Object with the given properties.
 *
 * @param {Class|Object} target
 * @param {Object} props
 */
function patch(target, props) {
    var patchID = _.uniqueId('patch_');
    target.__patchID = patchID;
    patches[patchID] = {
        target: target,
        otherPatchedProps: [],
        ownPatchedProps: [],
    };
    if (target.prototype) {
        _.each(props, function (value, key) {
            if (target.prototype.hasOwnProperty(key)) {
                patches[patchID].ownPatchedProps.push({
                    key: key,
                    initialValue: target.prototype[key],
                });
            } else {
                patches[patchID].otherPatchedProps.push(key);
            }
        });
        target.include(props);
    } else {
        _.each(props, function (value, key) {
            if (key in target) {
                var oldValue = target[key];
                patches[patchID].ownPatchedProps.push({
                    key: key,
                    initialValue: oldValue,
                });
                if (typeof value === 'function') {
                    target[key] = function () {
                        var oldSuper = this._super;
                        this._super = oldValue;
                        var result = value.apply(this, arguments);
                        if (oldSuper === undefined) {
                            delete this._super;
                        } else {
                            this._super = oldSuper;
                        }
                        return result;
                    };
                } else {
                    target[key] = value;
                }
            } else {
                patches[patchID].otherPatchedProps.push(key);
                target[key] = value;
            }
        });
    }
}

/**
 * Unpatches a given Class or Object.
 *
 * @param {Class|Object} target
 */
function unpatch(target) {
    var patchID = target.__patchID;
    var patch = patches[patchID];
    if (target.prototype) {
        _.each(patch.ownPatchedProps, function (p) {
            target.prototype[p.key] = p.initialValue;
        });
        _.each(patch.otherPatchedProps, function (key) {
            delete target.prototype[key];
        });
    } else {
        _.each(patch.ownPatchedProps, function (p) {
            target[p.key] = p.initialValue;
        });
        _.each(patch.otherPatchedProps, function (key) {
            delete target[key];
        });
    }
    delete patches[patchID];
    delete target.__patchID;
}

/**
 * Opens the datepicker of a given element.
 *
 * @param {jQuery} $datepickerEl element to which a datepicker is attached
 */
function openDatepicker($datepickerEl) {
    $datepickerEl.find('.o_datepicker_input').trigger('focus.datetimepicker');
}

// Loading static files cannot be properly simulated when their real content is
// really needed. This is the case for static XML files so we load them here,
// before starting the qunit test suite.
// (session.js is in charge of loading the static xml bundle and we also have
// to load xml files that are normally lazy loaded by specific widgets).
return $.when(
    session.is_bound,
    ajax.loadXML('/web/static/src/xml/dialog.xml', core.qweb)
).then(function () {
    setTimeout(function () {
        // this is done with the hope that tests are
        // only started all together...
        QUnit.start();
    }, 0);
    return {
        addMockEnvironment: addMockEnvironment,
        createActionManager: createActionManager,
        createDebugManager: createDebugManager,
        createAsyncView: createAsyncView,
        createModel: createModel,
        createParent: createParent,
        createView: createView,
        dragAndDrop: dragAndDrop,
        fieldsViewGet: fieldsViewGet,
        intercept: intercept,
        observe: observe,
        openDatepicker: openDatepicker,
        patch: patch,
        patchDate: patchDate,
        removeSrcAttribute: removeSrcAttribute,
        triggerKeypressEvent: triggerKeypressEvent,
        triggerMouseEvent: triggerMouseEvent,
        triggerPositionalMouseEvent: triggerPositionalMouseEvent,
        unpatch: unpatch,
    };
});

});
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 = $('<div/>', {'class': 'form-group mb0'})
                            .append($('<span/>', {'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")
        }, 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, {}, 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;
        return this._super.apply(this, arguments);
    },
    start: function () {
        var self = this;

        this.$(".link-style").remove();
        this.$("label[for=link-new]").text("Menu Label");

        return $.when(this._super.apply(this, arguments)).then(function () {
            var $link_text = self.$('#link-text').focus();
            self.$('#link-page').change(function (e) {
                if ($link_text.val()) { return; }
                var data = $(this).select2('data');
                $link_text.val(data.create ? data.id : data.text);
                $link_text.focus();
            });
        });
    },
    save: function () {
        var $e = this.$('#link-text');
        if (!$e.val() || !$e[0].checkValidity()) {
            $e.closest('.form-group').addClass('has-error');
            $e.focus();
            return;
        }
        return this._super.apply(this, arguments);
    }
});

return {
    'TopBar': TopBarContent,
};

});
Example #4
0
odoo.define('mass_mailing.editor', function (require) {
"use strict";

require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var rte = require('web_editor.rte');
var options = require('web_editor.snippets.options');
var snippets_editor = require('web_editor.snippet.editor');

var $editable_area = $('#editable_area');
var odoo_top = window.top.odoo;

// Snippet option for resizing  image and column width inline like excel
options.registry["width-x"] = options.Class.extend({
    start: function () {
        this.container_width = this.$target.parent().closest("td, table, div").width();

        var self = this;
        var offset, sib_offset, target_width, sib_width;
        var $body = $(document.body);
        this.is_image = false;
        this._super.apply(this, arguments);

        this.$overlay.find(".oe_handle.e, .oe_handle.w").removeClass("readonly");
        if (this.$target.is("img")) {
            this.$overlay.find(".oe_handle.w").addClass("readonly");
            this.$overlay.find(".oe_snippet_move, .oe_snippet_clone").addClass("hidden");
            this.is_image=true;
        }

        this.$overlay.find(".oe_handle").on('mousedown', function (event) {
            event.preventDefault();
            var $handle = $(this);
            var compass = false;

            _.each(['n', 's', 'e', 'w' ], function (handler) {
                if ($handle.hasClass(handler)) { compass = handler; }
            });
            if (self.is_image) { compass = "******"; }

            $body.on("mousemove.mass_mailing_width_x", function (event) {
                event.preventDefault();
                offset = self.$target.offset().left;
                target_width = self.get_max_width(self.$target);
                if (compass === 'e' && self.$target.next().offset()) {
                    sib_width = self.get_max_width(self.$target.next());
                    sib_offset = self.$target.next().offset().left;
                    self.change_width(event, self.$target, target_width, offset, true);
                    self.change_width(event, self.$target.next(), sib_width, sib_offset, false);
                }
                if (compass === 'w' && self.$target.prev().offset()) {
                    sib_width = self.get_max_width(self.$target.prev());
                    sib_offset = self.$target.prev().offset().left;
                    self.change_width(event, self.$target, target_width, offset, false);
                    self.change_width(event, self.$target.prev(), sib_width, sib_offset, true);
                }
                if (compass === 'image') {
                    self.change_width(event, self.$target, target_width, offset, true);
                }
            });

            $body.one("mouseup", function(){
                $body.off('.mass_mailing_width_x');
            });
        });
    },
    change_width: function (event, target, target_width, offset, grow) {
        target.css("width", grow ? (event.pageX - offset) : (offset + target_width - event.pageX));
        this.trigger_up('cover_update');
    },
    get_int_width: function (el) {
        return parseInt($(el).css("width"), 10);
    },
    get_max_width: function ($el) {
        return this.container_width - _.reduce(_.map($el.siblings(), this.get_int_width), function (memo, w) { return memo + w; });
    },
    onFocus: function () {
        this._super.apply(this, arguments);

        if (this.$target.is("td, th")) {
            this.$overlay.find(".oe_handle.e, .oe_handle.w").toggleClass("readonly", this.$target.siblings().length === 0);
        }
    },
});

options.registry.table_item = options.Class.extend({
    onClone: function () {
        this._super.apply(this, arguments);

        // If we cloned a td or th element...
        if (this.$target.is("td, th")) {
            // ... and that the td or th element was alone on its row ...
            if (this.$target.siblings().length === 1) {
                var $tr = this.$target.parent();
                $tr.clone().empty().insertAfter($tr).append(this.$target); // ... move the clone in a new row instead
                return;
            }

            // ... if not, if the clone neighbor is an empty cell, remove this empty cell (like if the clone content had been put in that cell)
            var $next = this.$target.next();
            if ($next.length && $next.text().trim() === "") {
                $next.remove();
                return;
            }

            // ... if not, insert an empty col in each other row, at the index of the clone
            var width = this.$target.width();
            var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
            _.each($trs.children(":nth-child(" + this.$target.index() + ")"), function (col) {
                $(col).after($("<td/>", {style: "width: " + width + "px;"}));
            });
        }
    },
    onRemove: function () {
        this._super.apply(this, arguments);

        // If we are removing a td or th element which was not alone on its row ...
        if (this.$target.is("td, th") && this.$target.siblings().length > 0) {
            var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
            if ($trs.length) { // ... if there are other rows in the table ...
                var $last_tds = $trs.children(":last-child");
                if (_.reduce($last_tds, function (memo, td) { return memo + (td.innerHTML || ""); }, "").trim() === "") {
                    $last_tds.remove(); // ... remove the potential full empty column in the table
                } else {
                    this.$target.parent().append("<td/>"); // ... else, if there is no full empty column, append an empty col in the current row
                }
            }
        }
    },
});

var fn_popover_update = $.summernote.eventHandler.modules.popover.update;
$.summernote.eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
    fn_popover_update.call(this, $popover, oStyle, isAirMode);
    $("span.o_table_handler, div.note-table").remove();
};

ajax.loadXML("/mass_mailing/static/src/xml/mass_mailing.xml", core.qweb);

snippets_editor.Class.include({
    _computeSnippetTemplates: function (html) {
        var self = this;
        var ret = this._super.apply(this, arguments);

        var $themes = this.$("#email_designer_themes").children();
        if ($themes.length === 0) return ret;

        /**
         * Initialize theme parameters.
         */
        var all_classes = "";
        var themes_params = _.map($themes, function (theme) {
            var $theme = $(theme);
            var name = $theme.data("name");
            var classname = "o_" + name + "_theme";
            all_classes += " " + classname;
            var images_info = _.defaults($theme.data("imagesInfo") || {}, {all: {}});
            _.each(images_info, function (info) {
                info = _.defaults(info, images_info.all, {module: "mass_mailing", format: "jpg"});
            });
            return {
                name: name,
                className: classname || "",
                img: $theme.data("img") || "",
                template: $theme.html().trim(),
                nowrap: !!$theme.data('nowrap'),
                get_image_info: function (filename) {
                    if (images_info[filename]) {
                        return images_info[filename];
                    }
                    return images_info.all;
                }
            };
        });
        $themes.parent().remove();

        var $body = $(document.body);
        var $snippets = this.$(".oe_snippet");
        var $snippets_menu = this.$el.find("#snippets_menu");

        /**
         * Create theme selection screen and check if it must be forced opened.
         * Reforce it opened if the last snippet is removed.
         */
        var $dropdown = $(core.qweb.render("mass_mailing.theme_selector", {
            themes: themes_params
        }));
        var first_choice;
        check_if_must_force_theme_choice();

        /**
         * Add proposition to install enterprise themes if not installed.
         */
        var $mail_themes_upgrade = $dropdown.find(".o_mass_mailing_themes_upgrade");
        $mail_themes_upgrade.on("click", "> a", function (e) {
            e.stopImmediatePropagation();
            e.preventDefault();
            odoo_top[window.callback+"_do_action"]("mass_mailing.action_mass_mailing_configuration");
        });

        /**
         * Switch theme when a theme button is hovered. Confirm change if the theme button
         * is pressed.
         */
        var selected_theme = false;
        $dropdown.on("mouseenter", "li > a", function (e) {
            if (first_choice) return;
            e.preventDefault();
            var theme_params = themes_params[$(e.currentTarget).parent().index()];
            switch_theme(theme_params);
        });
        $dropdown.on("click", "li > a", function (e) {
            e.preventDefault();
            var theme_params = themes_params[$(e.currentTarget).parent().index()];
            if (first_choice) {
                switch_theme(theme_params);
                $body.removeClass("o_force_mail_theme_choice");
                first_choice = false;

                if ($mail_themes_upgrade.length) {
                    $dropdown.remove();
                    $snippets_menu.empty();
                }
            }

            switch_images(theme_params, $snippets);

            selected_theme = theme_params;

            // Notify form view
            odoo_top[window.callback+"_downup"]($editable_area.addClass("o_dirty").html());
        });

        /**
         * If the user opens the theme selection screen, indicates which one is active and
         * saves the information...
         * ... then when the user closes check if the user confirmed its choice and restore
         * previous state if this is not the case.
         */
        $dropdown.on("shown.bs.dropdown", function () {
            check_selected_theme();
            $dropdown.find("li").removeClass("selected").filter(function () {
                return ($(this).has(".o_thumb[style=\""+ "background-image: url(" + (selected_theme && selected_theme.img) + "_small.png)"+ "\"]").length > 0);
            }).addClass("selected");
        });
        $dropdown.on("hidden.bs.dropdown", function () {
            switch_theme(selected_theme);
        });

        /**
         * On page load, check the selected theme and force switching to it (body needs the
         * theme style for its edition toolbar).
         */
        check_selected_theme();
        $body.addClass(selected_theme.className);
        switch_images(selected_theme, $snippets);

        $dropdown.insertAfter($snippets_menu);

        return ret;

        function check_if_must_force_theme_choice() {
            first_choice = editable_area_is_empty();
            $body.toggleClass("o_force_mail_theme_choice", first_choice);
        }

        function editable_area_is_empty($layout) {
            $layout = $layout || $editable_area.find(".o_layout");
            var $mail_wrapper = $layout.children(".o_mail_wrapper");
            var $mail_wrapper_content = $mail_wrapper.find('.o_mail_wrapper_td');
            if (!$mail_wrapper_content.length) { // compatibility
                $mail_wrapper_content = $mail_wrapper;
            }
            return (
                $editable_area.html().trim() === ""
                || ($layout.length > 0 && ($layout.html().trim() === "" || $mail_wrapper_content.length > 0 && $mail_wrapper_content.html().trim() === ""))
            );
        }

        function check_selected_theme() {
            var $layout = $editable_area.find(".o_layout");
            if ($layout.length === 0) {
                selected_theme = false;
            } else {
                _.each(themes_params, function (theme_params) {
                    if ($layout.hasClass(theme_params.className)) {
                        selected_theme = theme_params;
                    }
                });
            }
        }

        function switch_images(theme_params, $container) {
            if (!theme_params) return;
            $container.find("img").each(function () {
                var $img = $(this);
                var src = $img.attr("src");

                var m = src.match(/^\/web\/image\/\w+\.s_default_image_(?:theme_[a-z]+_)?(.+)$/);
                if (!m) {
                    m = src.match(/^\/\w+\/static\/src\/img\/(?:theme_[a-z]+\/)?s_default_image_(.+)\.[a-z]+$/);
                }
                if (!m) return;

                var file = m[1];
                var img_info = theme_params.get_image_info(file);

                if (img_info.format) {
                    src = "/" + img_info.module + "/static/src/img/theme_" + theme_params.name + "/s_default_image_" + file + "." + img_info.format;
                } else {
                    src = "/web/image/" + img_info.module + ".s_default_image_theme_" + theme_params.name + "_" + file;
                }

                $img.attr("src", src);
            });
        }

        function switch_theme(theme_params) {
            if (!theme_params || switch_theme.last === theme_params) return;
            switch_theme.last = theme_params;

            $body.removeClass(all_classes).addClass(theme_params.className);
            switch_images(theme_params, $editable_area);

            var $old_layout = $editable_area.find(".o_layout");
            // This wrapper structure is the only way to have a responsive and
            // centered fixed-width content column on all mail clients
            var $new_wrapper, $new_wrapper_content;

            if (theme_params.nowrap) {
                $new_wrapper = $new_wrapper_content = $("<div/>", {"class": "oe_structure"});
            }
            else {
                $new_wrapper = $('<table/>', {class: 'o_mail_wrapper'});
                $new_wrapper_content = $("<td/>", {class: 'o_mail_no_resize o_mail_wrapper_td oe_structure'});
                $new_wrapper.append($('<tr/>').append(
                    $("<td/>", {class: 'o_mail_no_resize'}),
                    $new_wrapper_content,
                    $("<td/>", {class: 'o_mail_no_resize'})
                ));
            }
            var $new_layout = $("<div/>", {"class": "o_layout " + theme_params.className}).append($new_wrapper);

            var $contents;
            if (first_choice) {
                $contents = theme_params.template;
            } else if ($old_layout.length) {
                $contents = ($old_layout.hasClass("oe_structure") ? $old_layout : $old_layout.find(".oe_structure").first()).contents();
            } else {
                $contents = $editable_area.contents();
            }

            $editable_area.empty().append($new_layout);
            $new_wrapper_content.append($contents);
            $old_layout.remove();

            if (first_choice) {
                self._registerDefaultTexts($new_wrapper_content);
            }
            self._disableUndroppableSnippets();
        }
    },
});

var callback = window ? window["callback"] : undefined;
odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
    if (!window || window.closed) {
        delete odoo_top[callback+"_updown"];
        return;
    }

    var $editable = $("#editable_area");
    var _val = $editable.prop("innerHTML");
    var editor_enable = $('body').hasClass('editor_enable');
    value = value || "";

    if (value !==_val) {
        if (editor_enable) {
            if (value !== fields_values[field_name]) {
                rte.history.recordUndo($editable);
            }
            core.bus.trigger('deactivate_snippet');
        }

        if (value.indexOf('on_change_model_and_list') === -1) {
            $editable.html(value);

            if (editor_enable) {
                if (value !== fields_values[field_name]) {
                    $editable.trigger("content_changed");
                }
            }
        }
    }

    if (fields_values.mailing_model && editor_enable) {
        if (value.indexOf('on_change_model_and_list') !== -1) {
            odoo_top[callback+"_downup"](_val);
        }
    }
};

if ($editable_area.html().indexOf('on_change_model_and_list') !== -1) {
    $editable_area.empty();
}
});
Example #5
0
File: utils.js Project: 10537/odoo
/**
 * @deprecated
 * @todo create Dialog.prompt instead of this
 */
function prompt(options, _qweb) {
    /**
     * A bootstrapped version of prompt() albeit asynchronous
     * This was built to quickly prompt the user with a single field.
     * For anything more complex, please use editor.Dialog class
     *
     * Usage Ex:
     *
     * website.prompt("What... is your quest ?").then(function (answer) {
     *     arthur.reply(answer || "To seek the Holy Grail.");
     * });
     *
     * website.prompt({
     *     select: "Please choose your destiny",
     *     init: function () {
     *         return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ];
     *     }
     * }).then(function (answer) {
     *     mame_station.loadCharacter(answer);
     * });
     *
     * @param {Object|String} options A set of options used to configure the prompt or the text field name if string
     * @param {String} [options.window_title=''] title of the prompt modal
     * @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title
     * @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title
     * @param {String} [options.select] tell the modal to use a select box, the given value will be the field title
     * @param {Object} [options.default=''] default value of the field
     * @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a deferred]
     */
    if (typeof options === 'string') {
        options = {
            text: options
        };
    }
    var xmlDef;
    if (_.isUndefined(_qweb)) {
        _qweb = 'website.prompt';
        xmlDef = ajax.loadXML('/website/static/src/xml/website.xml', core.qweb);
    }
    options = _.extend({
        window_title: '',
        field_name: '',
        'default': '', // dict notation for IE<9
        init: function () {},
    }, options || {});

    var type = _.intersection(Object.keys(options), ['input', 'textarea', 'select']);
    type = type.length ? type[0] : 'input';
    options.field_type = type;
    options.field_name = options.field_name || options[type];

    var def = $.Deferred();

    $.when(xmlDef).then(function () {
        var dialog = $(qweb.render(_qweb, options)).appendTo('body');
        options.$dialog = dialog;
        var field = dialog.find(options.field_type).first();
        field.val(options['default']); // dict notation for IE<9
        field.fillWith = function (data) {
            if (field.is('select')) {
                var select = field[0];
                data.forEach(function (item) {
                    select.options[select.options.length] = new window.Option(item[1], item[0]);
                });
            } else {
                field.val(data);
            }
        };
        var init = options.init(field, dialog);
        $.when(init).then(function (fill) {
            if (fill) {
                field.fillWith(fill);
            }
            dialog.modal('show');
            field.focus();
            dialog.on('click', '.btn-primary', function () {
                    var backdrop = $('.modal-backdrop');
                def.resolve(field.val(), field, dialog);
                dialog.modal('hide').remove();
                    backdrop.remove();
            });
        });
        dialog.on('hidden.bs.modal', function () {
                var backdrop = $('.modal-backdrop');
            def.reject();
            dialog.remove();
                backdrop.remove();
        });
        if (field.is('input[type="text"], select')) {
            field.keypress(function (e) {
                if (e.which === 13) {
                    e.preventDefault();
                    dialog.find('.btn-primary').trigger('click');
                }
            });
        }
    });

    return def;
}
Example #6
0
odoo.define('website.snippets.editor.gallery', function (require) {
'use strict';

var ajax = require('web.ajax');
var core = require('web.core');
var base = require('web_editor.base');
var widget = require('web_editor.widget');
var animation = require('web_editor.snippets.animation');
var options = require('web_editor.snippets.options');
var snippet_editor = require('web_editor.snippet.editor');

var _t = core._t;
var qweb = core.qweb;

/*--------------------------------------------------------------------------
 Template files to load
 --------------------------------------------------------------------------*/
ajax.loadXML('/website/static/src/xml/website.gallery.xml', qweb);

/*--------------------------------------------------------------------------
  Gallery Snippet

  This is the snippet responsible for configuring the image galleries.
  Look at /website/views/snippets.xml for the available options
  ------------------------------------------------------------------------*/
options.registry.gallery = options.Class.extend({
    start  : function () {
        this._super();
        this.bind_change();
        var index = Math.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; }));
        this.$target.find("img:not([data-index])").each(function () {
            index++;
            $(this).attr('data-index', index).data('index', index);
        });
        this.$target.attr("contentEditable", false);

        this._temp_mode = this.$el.find("data-mode").data("mode");
        this._temp_col = this.$el.find("data-columns").data("columns");
    },
    drop_and_build_snippet: function () {
        var uuid = new Date().getTime();
        this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
        this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid);
    },
    styling  : function (type, value) {
        var classes = this.$el.find('li[data-styling]').map(function () {
            return $(this).data('styling');
        }).get().join(' ');
        this.$target.find("img").removeClass(classes).addClass(value);
    },
    interval : function (type, value) {
        this.$target.find('.carousel:first').attr("data-interval", value);
    },
    reapply : function () {
        var self    = this,
            modes   = [ 'o_nomode', 'o_grid', 'o_masonry', 'o_slideshow' ],
            classes = this.$target.attr("class").split(/\s+/);
        this.cancel_masonry();

        modes.forEach(function (mode) {
            if (classes.indexOf(mode) != -1) {
                self.mode("reapply", mode.slice(2, Infinity));
                return;
            }
        });
        this.$target.attr("contentEditable", false);
    },
    bind_change: function () {
        var self = this;
        return this.$target.find("img").off('save').on('save', function (event, img) {
                var $parent = $(img).parent();
                $parent.addClass("saved_active");
                var index = self.$target.find(".item.saved_active").index();
                $parent.removeClass("saved_active");
                self.$target.find(".carousel:first li[data-target]:eq("+index+")").css("background-image", "url("+$(img).attr("src")+")");
            });
    },
    get_imgs: function () {
        var imgs = this.$target.find("img").addClass("img img-thumbnail img-responsive mb8 mt8").detach().get();
        imgs.sort(function (a,b) { return $(a).data('index')-$(b).data('index'); });
        return imgs;
    },
    mode: function (type, value, $li) {
        if (type !== "reapply" && type !== "click" && this._temp_mode === value) {
            return;
        }
        this._temp_mode = value;

        this.cancel_masonry();

        if (!value) value = 'nomode';
        this[value](type);
        this.$target.removeClass('o_nomode o_masonry o_grid o_slideshow').addClass("o_"+value);
        this.bind_change();
    },
    replace: function ($content) {
        var $container = this.$target.find(".container:first");
        $container.empty().append($content);
        return $container;
    },
    nomode : function (type) {
        if (type !== "reapply" && !this.$target.attr('class').match(/o_grid|o_masonry|o_slideshow/)) return;

        var self = this,
            $row     = $('<div class="row"></div>'),
            $imgs = $(this.get_imgs());

        this.replace($row);

        $imgs.each(function () {
            var $wrap = $(this).wrap('<div>').parent();
            var img = this;
            if (img.width >= img.height * 2) {
                $wrap.addClass("col-md-6");
            } else if (img.width > 600) {
                $wrap.addClass("col-md-6");
            } else {
                $wrap.addClass("col-md-3");
            }
            $row.append($wrap);
        });
        this.$target.css("height", "");
    },
    cancel_masonry: function () {
        clearTimeout(this.timer);
        $(this.masonry_imgs).appendTo(this.$target);
        this.masonry_imgs = [];
    },
    masonry : function (type) {
        var self     = this,
            imgs    = this.get_imgs(),
            columns  = this.get_columns(),
            colClass = undefined,
            $cols    = [];

        var $row = $("<div class='row'/>");
        this.replace($row);

        // if no columns let's default to 3, here we must update the DOM accordingly :'(
        if (columns === 0) {
            columns = 3;
            this.$target.attr("data-columns", columns);
        }
        colClass = "col-md-"+(12/columns);

        // create columns
        for (var c = 0; c < columns; c++) {
            var $col = $('<div class="col o_snippet_not_selectable"></div>').addClass(colClass);
            $row.append($col);
            $cols.push($col.get()[0]);
        }

        imgs.reverse();
        $cols = $($cols);
        function add() {
            self.lowest($cols).append(imgs.pop());
            if (imgs.length) self.timer = setTimeout(add, 0);
        }
        this.masonry_imgs = imgs;
        if (imgs.length) add();
        this.$target.css("height", "");
    },
    grid : function (type) {
        if (type !== "reapply" && this.$target.hasClass('o_grid')) return;

        var self     = this,
            $imgs    = $(this.get_imgs()),
            $col, $img,
            $row     = $('<div class="row"></div>'),
            columns  = this.get_columns() || 3,
            colClass = "col-md-"+(12/columns),
            $container = this.replace($row);

        $imgs.each(function (index) { // 0 based index
            $img = $(this);
            $col = $img.wrap('<div>').parent();
            self.img_preserve_styles($img);
            self.img_responsive($img);
            $col.addClass(colClass);
            $col.appendTo($row);
            if ( (index+1) % columns === 0) {
                $row = $('<div class="row"></div>');
                $row.appendTo($container);
            }
        });
        this.$target.css("height", "");
    },
    slideshow :function (type) {
        if (type !== "reapply" && this.$target.hasClass('o_slideshow')) return;

        var self = this,
            $imgs    = $(this.get_imgs()),
            urls = $imgs.map(function () { return $(this).attr("src"); } ).get();
        var params = {
                srcs : urls,
                index: 0,
                title: "",
                interval : this.$target.data("interval") || false,
                id: "slideshow_" + new Date().getTime()
            },
            $slideshow = $(qweb.render('website.gallery.slideshow', params));
        this.replace($slideshow);
        this.$target.find(".item img").each(function (index) {
            $(this).attr('data-index', index).data('index', index);
        });
        this.$target.css("height", Math.round(window.innerHeight*0.7));

        // apply layout animation
        this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
        this.$target.find('li.fa').off('click');
        if (this.$target.data("snippet-view", view)) {
            var view = new animation.registry.gallery_slider(this.$target, true);
            this.$target.data("snippet-view", view);
        } else {
            this.$target.data("snippet-view").start(true);
        }
    },
    columns : function (type, value) {
        this.$target.attr("data-columns", value);
        if (this._temp_col !== value) {
            this._temp_col = value;
            this.reapply();
        }
    },
    images_add : function (type) {
        if(type !== "click") return;
        var self = this;
        var $container = this.$target.find(".container:first");
        var editor = new widget.MediaDialog(null, {select_images: true}, this.$target.closest('.o_editable'), null).open();
        var index = Math.max(0, _.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; })) + 1);
        editor.on('save', this, function (attachments) {
            for (var i = 0 ; i < attachments.length; i++) {
                $('<img class="img img-responsive mb8 mt8"/>')
                    .attr("src", attachments[i].src)
                    .attr('data-index', index+i)
                    .data('index', index+i)
                    .appendTo($container);
            }
            self.reapply(); // refresh the $target
            setTimeout(function () {
                self.buildingBlock.make_active(self.$target);
            },0);
        });
    },
    images_rm   : function (type) {
        if(type !== "click") return;
        this.replace($('<div class="alert alert-info css_editable_mode_display"/>').text(_t("Add Images from the 'Customize' menu")));
    },
    sizing : function () { // done via css, keep it to avoid undefined error
    },
    /*
     *  helpers
     */
    styles_to_preserve : function ($img) {
        var styles = [ 'img-rounded', 'img-thumbnail', 'img-circle', 'shadow', 'fa-spin' ];
        var preserved = [];

        for (var style in styles) {
            if ($img.hasClass(style)) {
                preserved.push(style);
            }
        }
        return preserved.join(' ');
    },
    img_preserve_styles : function ($img) {
        var classes = this.styles_to_preserve($img);
        $img.removeAttr("class");
        $img.addClass(classes);
        return $img;
    },
    img_responsive : function (img) {
        img.addClass("img img-responsive");
        return img;
    },
    lowest : function ($cols) {
        var height = 0, min = -1, col=0, lowest = undefined;
        $cols.each(function () {
            var $col = $(this);
            height = $col.height();
            if (min === -1 || height < min) {
                min = height;
                lowest = $col;
            }
        });
        return lowest;
    },
    get_columns : function () {
        return parseInt(this.$target.attr("data-columns") || 3);
    },

    clean_for_save: function () {
        var self = this;
        if (this.$target.hasClass("slideshow")) {
            this.$target.removeAttr("style");
        }
    },

    set_active: function () {
        this._super();
        var classes = _.uniq((this.$target.attr("class").replace(/(^|\s)o_/g, ' ') || '').split(/\s+/));
        var $li = this.$el.find('[data-mode]')
            .removeClass("active")
            .filter('[data-mode="' + classes.join('"], [data-mode="') + '"]').addClass("active");
        var mode = this.$el.find('[data-mode].active').data('mode');

        var classes = _.uniq((this.$target.find("img:first").attr("class") || '').split(/\s+/));
        var $li = this.$el.find('[data-styling]')
            .removeClass("active")
            .filter('[data-styling="' + classes.join('"], [data-styling="') + '"]').addClass("active");

        this.$el.find('li[data-interval]').removeClass("active")
            .filter('li[data-interval='+this.$target.find(".carousel:first").attr("data-interval")+']')
            .addClass("active");

        var interval = this.$target.find('.carousel:first').attr("data-interval");
        var $li = this.$el.find('[data-interval]')
            .removeClass("active")
            .filter('[data-interval=' + interval + ']').addClass("active");

        var columns = this.get_columns();
        var $li = this.$el.find('[data-columns]')
            .removeClass("active")
            .filter('[data-columns=' + columns + ']').addClass("active");

        this.$el.find('[data-columns]:first, [data-select_class="spc-none"]')
            .parent().parent().toggle(["grid", "masonry"].indexOf(mode) !== -1);
        this.$el.find('[data-interval]:first').parent().parent().toggle(mode === "slideshow");
    },
}); // options.Class.extend


options.registry.gallery_img = options.Class.extend({
    position: function (type, value) {
        if (type !== "click") return;

        var $parent = this.$target.closest("section");
        var editor = $parent.data('snippet-editor').styles.gallery;
        var imgs = $parent.find('img').get();
        imgs.sort(function (a,b) { return $(a).data('index')-$(b).data('index'); });

        var index = imgs.indexOf(this.$target[0]);

        switch (value) {
            case 'first': index = $(imgs.shift()).data('index')-1; break;
            case 'prev': index = index <= 1  ? $(imgs.shift()).data('index')-1 : ($(imgs[index-2]).data('index') + $(imgs[index-1]).data('index'))/2; break;
            case 'next': index = index >= imgs.length-2  ? $(imgs.pop()).data('index')+1 : ($(imgs[index+2]).data('index') + $(imgs[index+1]).data('index'))/2; break;
            case 'last': index = $(imgs.pop()).data('index')+1; break;
        }

        this.$target.data('index',index);

        this.buildingBlock.make_active(false);
        setTimeout(function () {
            editor.reapply();
        },0);
    },
    on_remove: function () {
        var $parent = snippet_editor.globalSelector.closest(this.$target.parent());
        _.defer((function () {
            this.buildingBlock.make_active($parent);
            $parent.data('snippet-editor').styles.gallery.reapply();
        }).bind(this));
    }
});


});
Example #7
0
odoo.define('website_gengo.website_gengo', 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 weContext = require('web_editor.context');
require('web_editor.editor');
var translate = require('web_editor.translate');

var qweb = core.qweb;
var _t = core._t;

if (!weContext.getExtra().edit_translations) {
    // Temporary hack until the editor bar is moved to the web client
    return;
}

ajax.loadXML('/website_gengo/static/src/xml/website.gengo.xml', qweb);

translate.Class.include({
    events: _.extend({}, translate.Class.prototype.events, {
        'click a[data-action=translation_gengo_post]': 'translation_gengo_post',
        'click a[data-action=translation_gengo_info]': 'translation_gengo_info',
    }),
    start: function () {
        var def = this._super.apply(this, arguments);

        var gengo_langs = ["ar_SY","id_ID","nl_NL","fr_CA","pl_PL","zh_TW","sv_SE","ko_KR","pt_PT","en_US","ja_JP","es_ES","zh_CN","de_DE","fr_FR","fr_BE","ru_RU","it_IT","pt_BR","pt_BR","th_TH","nb_NO","ro_RO","tr_TR","bg_BG","da_DK","en_GB","el_GR","vi_VN","he_IL","hu_HU","fi_FI"];
        if (gengo_langs.indexOf(weContext.get().lang) >= 0) {
            this.$('.gengo_post,.gengo_wait,.gengo_inprogress,.gengo_info').remove();
            this.$('button[data-action=save]')
                .after(qweb.render('website.ButtonGengoTranslator'));
        }

        this.translation_gengo_display();

        return def;
    },
    translation_gengo_display: function () {
        var self = this;
        if ($('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').length === 0){
            self.$el.find('.gengo_post').addClass("hidden");
            self.$el.find('.gengo_inprogress').removeClass("hidden");
        }
    },
    translation_gengo_post: function () {
        var self = this;
        this.new_words =  0;
        $('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').each(function () {
            self.new_words += $(this).text().trim().replace(/ +/g," ").split(" ").length;
        });
        ajax.jsonRpc('/website/check_gengo_set', 'call', {
        }).then(function (res) {
            var dialog;
            if (res === 0){
                dialog = new GengoTranslatorPostDialog(self.new_words);
                dialog.appendTo($(document.body));
                dialog.on('service_level', this, function () {
                    var gengo_service_level = dialog.$el.find(".form-control").val();
                    dialog.$el.modal('hide');
                    self.$el.find('.gengo_post').addClass("hidden");
                    self.$el.find('.gengo_wait').removeClass("hidden");
                    var trans = [];
                    $('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').each(function () {
                        var $node = $(this);
                        var data = $node.data();

                        var val = ($node.is('img')) ? $node.attr('alt') : $node.text();
                        trans.push({
                            initial_content: qweb.tools.html_escape(val),
                            translation_id: data.oeTranslationId || null,
                            gengo_translation: gengo_service_level,
                            gengo_comment:"\nOriginal Page: " + document.URL
                        });
                    });
                    ajax.jsonRpc('/website_gengo/set_translations', 'call', {
                        'data': trans,
                        'lang': weContext.get().lang,
                    }).then(function () {
                        ajax.jsonRpc('/website/post_gengo_jobs', 'call', {});
                        self._save();
                    }).fail(function () {
                        Dialog.alert(null, _t("Could not Post translation"));
                    });
                });
            } else {
                dialog = new GengoApiConfigDialog(res);
                dialog.appendTo($(document.body));
                dialog.on('set_config', this, function () {
                    dialog.$el.modal('hide');
                });
            }
        });
    },
    translation_gengo_info: function () {
        var translated_ids = [];
        $('[data-oe-translation-state="translated"]').each(function () {
            translated_ids.push($(this).attr('data-oe-translation-id'));
        });
        ajax.jsonRpc('/website/get_translated_length', 'call', {
            'translated_ids': translated_ids,
            'lang': weContext.get().lang,
        }).done(function (res){
            var dialog = new GengoTranslatorStatisticDialog(res);
            dialog.appendTo($(document.body));
        });
    },
});

var GengoTranslatorPostDialog = Widget.extend({
    events: {
        'hidden.bs.modal': 'destroy',
        'click button[data-action=service_level]': function () {
            this.trigger('service_level');
        },
    },
    template: 'website.GengoTranslatorPostDialog',
    init: function (new_words){
        this.new_words = new_words;
        return this._super.apply(this, arguments);
    },
    start: function () {
        this.$el.modal();
    },
});

var GengoTranslatorStatisticDialog = Widget.extend({
    events: {
        'hidden.bs.modal': 'destroy',
    },
    template: 'website.GengoTranslatorStatisticDialog',
    init: function (res) {
        var self = this;
        this.inprogess =  0;
        this.new_words =  0;
        this.done =  res.done;
        $('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').each(function () {
            self.new_words += $(this).text().trim().replace(/ +/g," ").split(" ").length;
        });
        $('[data-oe-translation-state="inprogress"]').each(function () {
            self.inprogess += $(this).text().trim().replace(/ +/g," ").split(" ").length;
        });
        this.total = this.done + this.inprogess;
        return this._super.apply(this, arguments);
    },
    start: function (res) {
        this.$el.modal(this.res);
    },
});

var GengoApiConfigDialog = Widget.extend({
    events: {
        'hidden.bs.modal': 'destroy',
        'click button[data-action=set_config]': 'set_config'
    },
    template: 'website.GengoApiConfigDialog',
    init:function (company_id){
        this.company_id =  company_id;
        return this._super.apply(this, arguments);
    },
    start: function (res) {
        this.$el.modal(this.res);
    },
    set_config: function () {
       var self = this;
       var public_key = this.$el.find("#gengo_public_key")[0].value;
       var private_key = this.$el.find("#gengo_private_key")[0].value;
       var auto_approve = this.$el.find("#gengo_auto_approve")[0].checked;
       var sandbox = this.$el.find("#gengo_sandbox")[0].checked;
       var pub_el = this.$el.find(".gengo_group_public")[0];
       var pri_el = this.$el.find(".gengo_group_private")[0];
       if (! public_key){
           $(pub_el).addClass("has-error");
       }
       else {
           $(pub_el).removeClass("has-error");
       }
       if (! private_key){
           $(pri_el).addClass("has-error");
       }
       else {
           $(pri_el).removeClass("has-error");
       }
       if (public_key && private_key){
           ajax.jsonRpc('/website/set_gengo_config', 'call', {
               'config': {'gengo_public_key':public_key,'gengo_private_key':private_key,'gengo_auto_approve':auto_approve,'gengo_sandbox':sandbox},
           }).then(function () {
               self.trigger('set_config');
           }).fail(function () {
               Dialog.alert(null, _t("Could not submit ! Try Again"));
           });
       }
    }
});

});
Example #8
0
      clickwatch(function(){

        $dom_optional.each(function(){
            product_ids.push($(this).find('span[data-product-id]').data('product-id'));
        });
        if (isNaN(value)) value = 0;
        $input.data('update_change', true);
        if ($(this).hasClass('js_no_gup')) { // if get_unit_price (gup) not needed
            var gup = $.when();
        }
        else {
            var gup = ajax.jsonRpc("/shop/get_unit_price", 'call', {
                'product_ids': product_ids,
                'add_qty': value,
                'use_order_pricelist': true})
            .then(function (res) {
                //basic case
                $dom.find('span.oe_currency_value').last().text(res[product_id].toFixed(2));
                $dom.find('.text-danger').toggle(res[product_id]<default_price && (default_price-res[product_id] > default_price/100));
                //optional case
                $dom_optional.each(function(){
                    var id = $(this).find('span[data-product-id]').data('product-id');
                    var price = parseFloat($(this).find(".text-danger > span.oe_currency_value").text());
                    $(this).find("span.oe_currency_value").last().text(res[id].toFixed(2));
                    $(this).find('.text-danger').toggle(res[id]<price && (price-res[id]>price/100));
                });
            });
        }
        gup.then(function(res) {
            ajax.jsonRpc("/shop/cart/update_json", 'call', {
            'line_id': line_id,
            'product_id': parseInt($input.data('product-id'),10),
            'set_qty': value})
            .then(function (data) {
                $input.data('update_change', false);
                if (value !== parseInt($input.val(), 10)) {
                    $input.trigger('change');
                    return;
                }
                if (!data.quantity) {
                    location.reload(true);
                    return;
                }
                var $q = $(".my_cart_quantity");
                $q.parent().parent().removeClass("hidden", !data.quantity);
                $q.html(data.cart_quantity).hide().fadeIn(600);

                $input.val(data.quantity);
                $('.js_quantity[data-line-id='+line_id+']').val(data.quantity).html(data.quantity);
                $("#cart_total").replaceWith(data['website_sale.total']);
                if (data.warning) {
                    var cart_alert = $('.oe_cart').parent().find('#data_warning');
                    if (cart_alert.length === 0) {
                        $('.oe_cart').prepend('<div class="alert alert-danger alert-dismissable" role="alert" id="data_warning">'+
                                '<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button> ' + data.warning + '</div>');
                    }
                    else {
                        cart_alert.html('<button type="button" class="close" data-dismiss="alert" aria-hidden="true">&times;</button> ' + data.warning);
                    }
                    $input.val(data.quantity);
                }
            });
        });
      }, 500);
odoo.define('website_sale_comparison.comparison', function (require) {
"use strict";

require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var _t = core._t;
var utils = require('web.utils');
var Widget = require('web.Widget');
var website = require('web_editor.base');
var website_sale_utils = require('website_sale.utils');

if(!$('.oe_website_sale').length) {
    return $.Deferred().reject("DOM doesn't contain '.oe_website_sale'");
}

var qweb = core.qweb;
ajax.loadXML('/website_sale_comparison/static/src/xml/comparison.xml', qweb);

var ProductComparison = Widget.extend({
    template:"product_comparison_template",
    events: {
        'click .o_product_panel_header': 'toggle_panel',
    },
    product_data: {},
    init: function(){
        this.comparelist_product_ids = JSON.parse(utils.get_cookie('comparelist_product_ids') || '[]');
        this.product_compare_limit = 4;
    },
    start:function(){
        var self = this;
        self.load_products(this.comparelist_product_ids).then(function() {
            self.update_content(self.comparelist_product_ids, true);
            if (self.comparelist_product_ids.length) {
                $('.o_product_feature_panel').show();
                self.update_comparelist_view();
            }
        });

        self.popover = $('#comparelist .o_product_panel_header').popover({
            trigger: 'manual',
            animation: true,
            html: true,
            title: function () {
                return _t("Compare Products");
            },
            container: '.o_product_feature_panel',
            placement: 'top',
            template: '<div style="width:600px;" class="popover comparator-popover" role="tooltip"><div class="arrow"></div><h3 class="popover-title"></h3><div class="popover-content"></div></div>',
            content: function() {
                return $('#comparelist .o_product_panel_content').html();
            }
        });
        $('.oe_website_sale .o_add_compare, .oe_website_sale .o_add_compare_dyn').click(function (e){
            if (self.comparelist_product_ids.length < self.product_compare_limit) {
                var prod = $(this).data('product-product-id');
                if (e.currentTarget.classList.contains('o_add_compare_dyn')) {
                    prod = $(this).parent().find('.product_id').val();
                    if (!prod) { // case List View Variants
                        prod = $(this).parent().find('input:checked').first().val();
                    }
                    prod = parseInt(prod, 10);
                }
                self.add_new_products(prod);
                website_sale_utils.animate_clone($('#comparelist .o_product_panel_header'), $(this).closest('form'), -50, 10);
            } else {
                self.$('.o_comparelist_limit_warning').show();
                self.show_panel(true);
            }
        });

        $('body').on('click', '.comparator-popover .o_comparelist_products .o_remove', function (e){
            self.rm_from_comparelist(e);
        });
        $('body').on('click', '.o_comparelist_remove', function (e){
            self.rm_from_comparelist(e);
            var new_link = '/shop/compare/?products=' + self.comparelist_product_ids.toString();
            window.location = _.isEmpty(self.comparelist_product_ids) ? '/shop' : new_link;
        });

        $("#o_comparelist_table tr").click(function(){
            $($(this).data('target')).children().slideToggle(100);
            $(this).find('.fa-chevron-circle-down, .fa-chevron-circle-right').toggleClass('fa-chevron-circle-down fa-chevron-circle-right');
        });
    },
    load_products:function(product_ids) {
        var self = this;
        return ajax.jsonRpc('/shop/get_product_data', 'call', {
            'product_ids': product_ids,
            'cookies': JSON.parse(utils.get_cookie('comparelist_product_ids') || '[]'),
        }).then(function (data) {
            self.comparelist_product_ids = JSON.parse(data.cookies);
            delete data.cookies;
            _.each(data, function(product) {
                self.product_data[product.product.id] = product;
            });
        });
    },
    toggle_panel: function() {
        $('#comparelist .o_product_panel_header').popover('toggle');
    },
    show_panel: function(force) {
        if ((!$('.comparator-popover').length) || force) {
            $('#comparelist .o_product_panel_header').popover('show');
        }
    },
    refresh_panel: function() {
        if ($('.comparator-popover').length) {
            $('#comparelist .o_product_panel_header').popover('show');
        }
    },
    add_new_products:function(product_id){
        var self = this;
        $('.o_product_feature_panel').show();
        if (!_.contains(self.comparelist_product_ids, product_id)) {
            self.comparelist_product_ids.push(product_id);
            if(_.has(self.product_data, product_id)){
                self.update_content([product_id], false);
            } else {
                self.load_products([product_id]).then(function(){
                    self.update_content([product_id], false);
                });
            }
        }
        self.update_cookie();
    },
    update_content:function(product_ids, reset) {
        var self = this;
        if (reset) {
            self.$('.o_comparelist_products .o_product_row').remove();
        }
        _.each(product_ids, function(res) {
            var $template = self.product_data[res].render;
            self.$('.o_comparelist_products').append($template);
        });
        this.refresh_panel();
    },
    rm_from_comparelist: function(e){
        this.comparelist_product_ids = _.without(this.comparelist_product_ids, $(e.currentTarget).data('product_product_id'));
        $(e.currentTarget).parents('.o_product_row').remove();
        this.update_cookie();
        $('.o_comparelist_limit_warning').hide();
        // force refresh to reposition popover
        this.update_content(this.comparelist_product_ids, true);
    },
    update_cookie: function(){
        document.cookie = 'comparelist_product_ids=' + JSON.stringify(this.comparelist_product_ids) + '; path=/';
        this.update_comparelist_view();
    },
    update_comparelist_view: function() {
        this.$('.o_product_circle').text(this.comparelist_product_ids.length);
        this.$('.o_comparelist_button').hide();
        if (_.isEmpty(this.comparelist_product_ids)) {
            $('.o_product_feature_panel').hide();
            this.toggle_panel();
        } else {
            this.$('.o_comparelist_products').show();
            if (this.comparelist_product_ids.length >=2) {
                this.$('.o_comparelist_button').show();
                this.$('.o_comparelist_button a').attr('href', '/shop/compare/?products='+this.comparelist_product_ids.toString());
            }
        }
    }
});

website.ready().done(function() {
    new ProductComparison().appendTo('.oe_website_sale');
});

});
Example #10
0
 _.each(files, function (file) {
     ajax.loadCSS(self.url(file, null));
 });
Example #11
0
odoo.define('web_editor.snippet.editor', function (require) {
'use strict';

var Class = require('web.Class');
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 animation = require('web_editor.snippets.animation');
var options = require('web_editor.snippets.options');

var qweb = core.qweb;
var dummy = editor.dummy;

/* ----- SNIPPET SELECTOR ---- */

ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb);

editor.Class.include({
    init: function() {
        var self = this;
        var res = this._super.apply(this, arguments);
        var $editable = this.rte.editable();
        this.buildingBlock = new BuildingBlock(this, $editable);
        this.buildingBlock.on("snippets:ready", this, function () {
            self.trigger("snippets:ready");
        });
        return res;
    },
    start: function () {
        var self = this;
        animation.stop();
        this.buildingBlock.insertAfter(this.$el);
        animation.start(true);

        this.rte.editable().find("*").off('mousedown mouseup click');
        return this._super();
    },
    save: function () {
        this.buildingBlock.clean_for_save();
        this._super();
    },
});

/* ----- SNIPPET SELECTOR ---- */

$.extend($.expr[':'],{
    hasData: function(node,i,m){
        return !!_.toArray(node.dataset).length;
    },
    data: function(node,i,m){
        return $(node).data(m[3]);
    }
});

var globalSelector = {
    closest: function () { return $(); },
    all: function () { return $(); },
    is: function () { return false; },
};

/* ----- Jquery activate block ---- */

$.fn.extend({
    activateBlock: function () {
        var target = globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this));
        var evt = document.createEvent("MouseEvents");
        evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
        target.dispatchEvent(evt);
        return this;
    }
});

/* ----- BuildingBlock (managment of drag&drop and focus/blur snippets) ---- */

var BuildingBlock = Widget.extend({
    template: 'web_editor.snippets',
    activeSnippets: [],
    init: function (parent, $editable) {
        this.parent = parent;
        this.$editable = $editable;

        this._super.apply(this, arguments);
        if(!$('#oe_manipulators').length){
            $("<div id='oe_manipulators' class='o_css_editor'></div>").appendTo('body');
        }
        this.$active_snipped_id = false;
        this.snippets = [];
        
        data.instance = this;
    },
    start: function() {
        var self = this;
        this.$snippet = $("#oe_snippets");

        this.$el
            .on("mouseenter", function () { self.show(); })
            .on("mouseleave", function (event) { if (event.clientX>0 && event.clientY>0) self.hide(); });

        $(window).resize(function () {
            setTimeout('$(document).click()',0);
        });

        this.fetch_snippet_templates();
        this.bind_snippet_click_editor();

        $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
            e.preventDefault();
        });

        var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
        $.summernote.core.dom.isNotBreakable = function (node) {
            return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
        };

        $(window).on('resize', function () {
            if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
                self.cover_target(self.$active_snipped_id.data("snippet-editor").$overlay, self.$active_snipped_id);
            }
        });

        $(document).on('mousemove', function () {
            if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
                self.$active_snipped_id.data("snippet-editor").$overlay.removeClass('o_keypress');
            }
        });
        $(document).on('keydown', function (event) {
            if (self.$active_snipped_id && self.$active_snipped_id.data("snippet-editor")) {
                self.$active_snipped_id.data("snippet-editor").$overlay.addClass('o_keypress');
            }
            if ((event.metaKey || (event.ctrlKey && !event.altKey)) && event.shiftKey && event.keyCode >= 48 && event.keyCode <= 57) {
                self.$snippet.find('.scroll:first > ul li:eq('+(event.keyCode-49)+') a').trigger("click");
                self.show();
                event.preventDefault();
            }
        });
    },

    _get_snippet_url: function () {
        return '/web_editor/snippets';
    },
    _add_check_selector : function (selector, no_check) {
        var self = this;
        var selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)";

        if (no_check) {
            return {
                closest: function ($from, parentNode) {
                    return $from.closest(selector, parentNode);
                },
                all: function ($from) {
                    return $from ? $from.find(selector) : $(selector);
                },
                is: function ($from) {
                    return $from.is(selector);
                }
            };
        } else {
            var selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable";
            return {
                closest: function ($from, parentNode) {
                    var parents = self.$editable.get();
                    return $from.closest(selector, parentNode).filter(function () {
                        var node = this;
                        while (node.parentNode) {
                            if (parents.indexOf(node)!==-1) {
                                return true;
                            }
                            node = node.parentNode;
                        }
                        return false;
                    });
                },
                all: function ($from) {
                    return $from ? $from.find(selector) : self.$editable.filter(selector).add(self.$editable.find(selector));
                },
                is: function ($from) {
                    return $from.is(selector);
                }
            };
        }
    },

    fetch_snippet_templates: function () {
        var self = this;
        var url = this._get_snippet_url();
        if (!url || !url.length) {
            this.$snippet.detach();
            return;
        }
        this.$el.find("#o_left_bar").addClass("hidden");
        return ajax.jsonRpc(url, 'call', {}).then(function (html) {
            self.compute_snippet_templates(html);
            self.trigger("snippets:ready");
        }, function () {
            self.$snippet.hide();
            console.warn('Snippets template not found:', url);
        });
    },
    compute_snippet_templates: function (html) {
        var self = this;
        var $html = $(html);
        var $left_bar = this.$el.find("#o_left_bar");
        var $ul = $html.siblings("ul");
        var $scroll = $html.siblings("#o_scroll");

        if (!$scroll.length) {
            throw new Error("Wrong snippets xml definition");
        }

        $ul.children().tooltip({
                delay: { "show": 500, "hide": 100 },
                container: 'body',
                title: function () {
                    return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1);
                },
                trigger: 'hover',
                placement: 'top'
            }).on('click', function () {$(this).tooltip('hide');});

        // t-snippet
        $html.find('[data-oe-type="snippet"][data-oe-name]').each(function () {
            var $div = $('<div/>').insertAfter(this).append(this).attr('name', $(this).data('oe-name'));
        });
        // end

        self.templateOptions = [];
        var selector = [];
        var $styles = $html.find("[data-js], [data-selector]");
        $styles.each(function () {
            var $style = $(this);
            var no_check = $style.data('no-check');
            var option_id = $style.data('js');
            var option = {
                'option' : option_id,
                'base_selector': $style.data('selector'),
                'selector': self._add_check_selector($style.data('selector'), no_check),
                '$el': $style,
                'drop-near': $style.data('drop-near') && self._add_check_selector($style.data('drop-near'), no_check),
                'drop-in': $style.data('drop-in') && self._add_check_selector($style.data('drop-in'), no_check),
                'data': $style.data()
            };
            self.templateOptions.push(option);
            selector.push(option.selector);
        });

        $styles.addClass("hidden");
        globalSelector = {
            closest: function ($from) {
                var $temp;
                var $target;
                var len = selector.length;
                for (var i = 0; i<len; i++) {
                    $temp = selector[i].closest($from, $target && $target[0]);
                    if (!$target || $temp.length) {
                        $target = $temp;
                    }
                }
                return $target;
            },
            all: function ($from) {
                var $target;
                var len = selector.length;
                for (var i = 0; i<len; i++) {
                    if (!$target) $target = selector[i].all($from);
                    else $target = $target.add(selector[i].all($from));
                }
                return $target;
            },
            is: function ($from) {
                var len = selector.length;
                for (var i = 0; i<len; i++) {
                    if (selector[i].is($from)) {
                        return true;
                    }
                }
                return false;
            },
        };

        var number = 0;

        // oe_snippet_body
        self.$snippets = $scroll.find(".o_panel_body").children()
            .addClass("oe_snippet")
            .each(function () {
                if (!$('.oe_snippet_thumbnail', this).size()) {
                    var $div = $(
                        '<div class="oe_snippet_thumbnail">'+
                            '<div class="oe_snippet_thumbnail_img"/>'+
                            '<span class="oe_snippet_thumbnail_title"></span>'+
                        '</div>');
                    $div.find('span').text($(this).attr("name"));
                    $(this).prepend($div);

                    // from t-snippet
                    var thumbnail = $("[data-oe-thumbnail]", this).data("oe-thumbnail");
                    if (thumbnail) {
                        $div.find('.oe_snippet_thumbnail_img').css('background-image', 'url(' + thumbnail + ')');
                    }
                    // end
                }
                if (!$(this).data("selector")) {
                    $("> *:not(.oe_snippet_thumbnail)", this).addClass('oe_snippet_body');
                }
                number++;
            });

        // hide scroll if no snippets defined
        if (!number) {
            this.$snippet.detach();
        } else {
            this.$el.find("#o_left_bar").removeClass("hidden");
        }
        $("body").toggleClass("editor_has_snippets", !!number);

        // select all default text to edit (if snippet default text)
        /*self.$snippets.find('.oe_snippet_body, .oe_snippet_body *')
            .contents()
            .filter(function() {
                return this.nodeType === 3 && this.textContent.match(/\S/);
            }).parent().addClass("o_default_snippet_text");*/
        $(document).on("mouseup", ".o_default_snippet_text", function (event) {
            $(event.target).selectContent();
        });
        $(document).on("keyup", function (event) {
            var r = $.summernote.core.range.create();
            $(r && r.sc).closest(".o_default_snippet_text").removeClass("o_default_snippet_text");
        });
        // end

        // clean t-oe
        $html.find('[data-oe-model], [data-oe-type]').each(function () {
            for (var k=0; k<this.attributes.length; k++) {
                if (this.attributes[k].name.indexOf('data-oe-') === 0) {
                    $(this).removeAttr(this.attributes[k].name);
                    k--;
                }
            }
        });
        // end

        $html.find('.o_not_editable').attr("contentEditable", false);

        $left_bar.append($html);

        // animate for list of snippet blocks
        $left_bar.on('click', '.scroll-link', function (event) {
            event.preventDefault();
            var targetOffset =  $($(this).attr("href")).position().top - $ul.outerHeight() + $scroll[0].scrollTop;
            $scroll.animate({'scrollTop': targetOffset}, 750);
        });
        $scroll.on('scroll', function () {
            var middle = $scroll.height()/4;
            var $li = $ul.find("a").parent().removeClass('active');
            var last;
            for (var k=0; k<$li.length; k++) {
                var li = $($li[k]);
                if (!li.data('target')) {
                    li.data('target', $($("a", li).attr("href")));
                }
                if (li.data('target').position().top > middle) {
                    break;
                }
                last = $li[k];
            }
            $(last).addClass("active");
        });
        // end

        // display scrollbar
        $(window).on('resize', function () {
            $scroll.css("overflow", "");
            var height = $left_bar.height() - $ul.outerHeight();
            $scroll.css("height", height);
            var $last = $scroll.children(":visible").last().children(".o_panel_body");
            $last.css({'min-height': (height-$last.prev().outerHeight())+'px'});
            if ($scroll[0].scrollHeight + $ul[0].scrollHeight > document.body.clientHeight) {
                $scroll.css("overflow", "auto").css("width", "226px");
            } else {
                $scroll.css("width", "");
            }
        }).trigger('resize');
        // end

        self.make_snippet_draggable(self.$snippets);
    },

    cover_target: function ($el, $target){
        if($el.data('not-cover_target')) return;
        var pos = $target.offset();
        var mt = parseInt($target.css("margin-top") || 0);
        var mb = parseInt($target.css("margin-bottom") || 0);
        var width = $target.outerWidth();
        var bigger = pos.left+width > $("body").outerWidth()-8;
        $el.css({
            'width': width,
            'top': pos.top - mt - 5,
            'left': pos.left-1
        });
        $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1});
        if (bigger) {
            $el.find(".oe_handle.e").css({right: 0, margin: 0})
                .find("div").css({right: 0, left: 'auto'});
        } else {
            $el.find(".oe_handle.e").css({right: "", margin: ""})
                .find("div").css({right: "", left: ""});
        }
        $el.find(".oe_handle.s").css({'top': $target.outerHeight() + mt + mb});
        $el.find(".oe_handle.size").css({'top': $target.outerHeight() + mt});
        $el.find(".oe_handle.s,.oe_handle.n").css({'width': width-2});
    },

    show_blocks: function () {
        var self = this;
        var cache = {};
        this.$snippet.find(".o_panel").each(function () {
            var catcheck = false;
            var $category = $(this);
            $category.find(".oe_snippet_body").each(function () {
                var $snippet = $(this);

                var check = false;

                for (var k in self.templateOptions) {
                    var option = self.templateOptions[k];
                    if ($snippet.is(option.base_selector)) {

                        cache[k] = cache[k] || {
                            'drop-near': option['drop-near'] ? option['drop-near'].all() : [],
                            'drop-in': option['drop-in'] ? option['drop-in'].all() : []
                        };

                        if (cache[k]['drop-near'].length || cache[k]['drop-in'].length) {
                            catcheck = true;
                            check = true;
                            break;
                        }
                    }
                }

                if (check) {
                    $snippet.closest(".oe_snippet").removeClass("disable");
                } else {
                    $snippet.closest(".oe_snippet").addClass("disable");
                }
            });

            $('#oe_snippets .scroll a[data-toggle="tab"][href="#' + $category.attr("id") + '"]').toggle(catcheck);
        });
    },
    show: function () {
        var self = this;
        this.make_active(false);
        this.$el.addClass("o_open");
        this.show_blocks();
    },
    hide: function () {
        this.$el.removeClass("o_open");
    },
    bind_snippet_click_editor: function () {
        var self = this;
        var snipped_event_flag;
        $(document).on('click', '*', function (event) {
            var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target) || event.target);
            if (self.editor_busy || snipped_event_flag===srcElement || !srcElement) {
                return;
            }
            snipped_event_flag = srcElement;

            setTimeout(function () {snipped_event_flag = false;}, 0);
            var $target = $(srcElement);

            if ($target.closest(".oe_overlay, .note-popover").length) {
                return;
            }

            if (!globalSelector.is($target)) {
                $target = globalSelector.closest($target);
            }

            if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
                return;
            }
            self.make_active($target);
        });
    },
    snippet_blur: function ($snippet) {
        if ($snippet) {
            if ($snippet.data("snippet-editor")) {
                $snippet.data("snippet-editor").on_blur();
            }
        }
    },
    snippet_focus: function ($snippet) {
        if ($snippet) {
            if ($snippet.data("snippet-editor")) {
                $snippet.data("snippet-editor").on_focus();
            }
        }
    },
    clean_for_save: function () {
        var self = this;
        var opt = options.registry;
        var template = self.templateOptions;
        for (var k in template) {
            var Option = opt[template[k]['option']];
            if (Option && Option.prototype.clean_for_save !== dummy) {
                template[k].selector.all().filter(".o_dirty").each(function () {
                    new Option(self, null, $(this), k).clean_for_save();
                });
            }
        }
        this.$editable.find("*[contentEditable], *[attributeEditable]")
            .removeAttr('contentEditable')
            .removeProp('contentEditable')
            .removeAttr('attributeEditable')
            .removeProp('attributeEditable');
    },
    make_active: function ($snippet) {
        if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
            return;
        }
        if (this.$active_snipped_id) {
            this.snippet_blur(this.$active_snipped_id);
            this.$active_snipped_id = false;
        }
        if ($snippet && $snippet.length) {
            if(_.indexOf(this.snippets, $snippet.get(0)) === -1) {
                this.snippets.push($snippet.get(0));
            }
            this.$active_snipped_id = $snippet;
            this.create_overlay(this.$active_snipped_id);
            this.snippet_focus($snippet);
        }
        this.$snippet.trigger('snippet-activated', $snippet);
        if ($snippet) {
            $snippet.trigger('snippet-activated', $snippet);
        }
    },
    create_overlay: function ($snippet) {
        if (typeof $snippet.data("snippet-editor") === 'undefined') {
            var $targets = this.activate_overlay_zones($snippet);
            if (!$targets.length) return;
            $snippet.data("snippet-editor", new Editor(this, $snippet));
        }
        this.cover_target($snippet.data('overlay'), $snippet);
    },

    // activate drag and drop for the snippets in the snippet toolbar
    make_snippet_draggable: function($snippets){
        var self = this;
        var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first");
        var left = $tumb.outerWidth()/2;
        var top = $tumb.outerHeight()/2;
        var $toInsert, dropped, $snippet, action, snipped_id;

        $snippets.draggable({
            greedy: true,
            helper: 'clone',
            zIndex: '1000',
            appendTo: 'body',
            cursor: "move",
            handle: ".oe_snippet_thumbnail",
            cursorAt: {
                'left': left,
                'top': top
            },
            start: function(){
                self.hide();
                dropped = false;
                // snippet_selectors => to get drop-near, drop-in
                $snippet = $(this);
                var $base_body = $snippet.find('.oe_snippet_body');
                var $selector_siblings = $();
                var $selector_children = $();
                var vertical = false;
                var temp = self.templateOptions;
                for (var k in temp) {
                    if ($base_body.is(temp[k].base_selector)) {
                        if (temp[k]['drop-near']) {
                            if (!$selector_siblings) $selector_siblings = temp[k]['drop-near'].all();
                            else $selector_siblings = $selector_siblings.add(temp[k]['drop-near'].all());
                        }
                        if (temp[k]['drop-in']) {
                            if (!$selector_children) $selector_children = temp[k]['drop-in'].all();
                            else $selector_children = $selector_children.add(temp[k]['drop-in'].all());
                        }
                    }
                }

                $toInsert = $base_body.clone();

                if (!$selector_siblings.length && !$selector_children.length) {
                    console.debug($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in");
                    return;
                }

                self.activate_insertion_zones($selector_siblings, $selector_children);

                $('.oe_drop_zone').droppable({
                    over:   function(){
                        dropped = true;
                        $(this).first().after($toInsert);
                    },
                    out:    function(){
                        var prev = $toInsert.prev();
                        if(this === prev[0]){
                            dropped = false;
                            $toInsert.detach();
                        }
                    }
                });
            },
            stop: function(ev, ui){
                $toInsert.removeClass('oe_snippet_body');
                
                if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) {
                    var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
                    if (el.length) {
                        el.after($toInsert);
                        dropped = true;
                    }
                }

                self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
                
                if (dropped) {

                    var prev = $toInsert.first()[0].previousSibling;
                    var next = $toInsert.last()[0].nextSibling;
                    var rte = self.parent.rte;

                    if (prev) {
                        $toInsert.detach();
                        rte.historyRecordUndo($(prev));
                        $toInsert.insertAfter(prev);
                    } else if (next) {
                        $toInsert.detach();
                        rte.historyRecordUndo($(next));
                        $toInsert.insertBefore(next);
                    } else {
                        var $parent = $toInsert.parent();
                        $toInsert.detach();
                        rte.historyRecordUndo($parent);
                        $parent.prepend($toInsert);
                    }

                    $toInsert.closest(".o_editable").trigger("content_changed");

                    var $target = false;
                    $target = $toInsert;

                    setTimeout(function () {
                        self.$snippet.trigger('snippet-dropped', $target);

                        animation.start(true, $target);

                        // drop_and_build_snippet
                        self.create_overlay($target);
                        if ($target.data("snippet-editor")) {
                            $target.data("snippet-editor").drop_and_build_snippet();
                        }
                        for (var k in self.templateOptions) {
                            self.templateOptions[k].selector.all($target).each(function () {
                                var $snippet = $(this);
                                self.create_overlay($snippet);
                                if ($snippet.data("snippet-editor")) {
                                    $snippet.data("snippet-editor").drop_and_build_snippet();
                                }
                            });
                        }
                        $target.closest(".o_editable").trigger("content_changed");
                        // end

                        self.make_active($target);
                    },0);
                } else {
                    $toInsert.remove();
                }
            },
        });
    },

    // return the original snippet in the editor bar from a snippet id (string)
    get_snippet_from_id: function(id){
        return $('.oe_snippet').filter(function(){
                return $(this).data('option') === id;
            }).first();
    },

    // Create element insertion drop zones. two css selectors can be provided
    // selector.children -> will insert drop zones as direct child of the selected elements
    //   in case the selected elements have children themselves, dropzones will be interleaved
    //   with them.
    // selector.siblings -> will insert drop zones after and before selected elements
    activate_insertion_zones: function($selector_siblings, $selector_children){
        var self = this;
        var zone_template = $("<div class='oe_drop_zone oe_insert'></div>");

        if ($selector_children) {
            $selector_children.each(function (){
                var $zone = $(this);
                var css = window.getComputedStyle(this);
                var float = css.float || css.cssFloat;
                var $drop = zone_template.clone();

                $zone.append($drop);
                var node = $drop[0].previousSibling;
                var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) ||  node.tagName === "BR"));
                if (test) {
                    $drop.addClass("oe_vertical oe_vertical_text").css({
                            'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
                            'float': 'none',
                            'display': 'inline-block'
                        });
                } else if (float === "left" || float === "right") {
                    $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30));
                }

                $drop = $drop.clone();

                $zone.prepend($drop);
                var node = $drop[0].nextSibling;
                var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) ||  node.tagName === "BR"));
                if (test) {
                    $drop.addClass("oe_vertical oe_vertical_text").css({
                            'height': parseInt(window.getComputedStyle($zone[0]).lineHeight),
                            'float': 'none',
                            'display': 'inline-block'
                        });
                } else if (float === "left" || float === "right") {
                    $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30));
                }
                if (test) {
                    $drop.css({'float': 'none', 'display': 'inline-block'});
                }
            });

            // add children near drop zone
            $selector_siblings = $(_.uniq(($selector_siblings || $()).add($selector_children.children()).get()));
        }

        if ($selector_siblings) {
            $selector_siblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function (){
                var $zone = $(this);
                var $drop;
                var css = window.getComputedStyle(this);
                var float = css.float || css.cssFloat;

                if($zone.prev('.oe_drop_zone:visible').length === 0){
                    $drop = zone_template.clone();
                    if (float === "left" || float === "right") {
                        $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.prev().outerHeight() || Infinity), 30));
                    }
                    $zone.before($drop);
                }
                if($zone.next('.oe_drop_zone:visible').length === 0){
                    $drop = zone_template.clone();
                    if (float === "left" || float === "right") {
                        $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.next().outerHeight() || Infinity), 30));
                    }
                    $zone.after($drop);
                }
            });
        }

        var count;
        do {
            count = 0;
            $zones = self.$editable.find('.oe_drop_zone > .oe_drop_zone').remove();   // no recursive zones
            count += $zones.length;
            $zones.remove();
        } while (count > 0);

        // Cleaning consecutive zone and up zones placed between floating or inline elements. We do not like these kind of zones.
        var $zones = self.$editable.find('.oe_drop_zone:not(.oe_vertical)');
        $zones.each(function (){
            var zone = $(this);
            var prev = zone.prev();
            var next = zone.next();
            // remove consecutive zone
            if (prev.is('.oe_drop_zone') || next.is('.oe_drop_zone')) {
                zone.remove();
                return;
            }
            var float_prev = prev.css('float')   || 'none';
            var float_next = next.css('float')   || 'none';
            var disp_prev  = prev.css('display') ||  null;
            var disp_next  = next.css('display') ||  null;
            if(     (float_prev === 'left' || float_prev === 'right')
                &&  (float_next === 'left' || float_next === 'right')  ){
                zone.remove();
            }else if( !( disp_prev === null
                      || disp_next === null
                      || disp_prev === 'block'
                      || disp_next === 'block' )){
                zone.remove();
            }
        });
    },

    // generate drop zones covering the elements selected by the selector
    // we generate overlay drop zones only to get an idea of where the snippet are, the drop
    activate_overlay_zones: function($targets){
        var self = this;

        function is_visible($el){
            return     $el.css('display')    != 'none'
                    && $el.css('opacity')    != '0'
                    && $el.css('visibility') != 'hidden';
        }

        // filter out invisible elements
        $targets = $targets.filter(function(){ return is_visible($(this)); });

        // filter out elements with invisible parents
        $targets = $targets.filter(function(){
            var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
            return parents.length === 0;
        });

        $targets.each(function () {
            var $target = $(this);
            if (!$target.data('overlay')) {
                var $zone = $(qweb.render('web_editor.snippet_overlay'));

                // fix for pointer-events: none with ie9
                if (document.body && document.body.addEventListener) {
                    $zone.on("click mousedown mousedown", function passThrough(event) {
                        event.preventDefault();
                        $target.each(function() {
                           // check if clicked point (taken from event) is inside element
                            event.srcElement = this;
                            $(this).trigger(event.type);
                        });
                        return false;
                    });
                }

                $zone.appendTo('#oe_manipulators');
                $zone.data('target',$target);
                $target.data('overlay',$zone);

                var timer;
                $target.closest('.o_editable').on("content_changed", function (event) {
                    if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) {
                        clearTimeout(timer);
                        timer = setTimeout(function () {
                            if ($target.data('overlay')) {
                                self.cover_target($target.data('overlay'), $target);
                            }
                        },50);
                    }
                 });

                var resize = function () {
                    if ($zone.parent().length) {
                        self.cover_target($zone, $target);
                    } else {
                        $('body').off("resize", resize);
                    }
                };
                $('body').on("resize", resize);
            }
            self.cover_target($target.data('overlay'), $target);
        });
        return $targets;
    }
});

/* ----- Editor (object who contain overlay and the option list) ---- */

var Editor = Class.extend({
    init: function (BuildingBlock, dom) {
        this.buildingBlock = BuildingBlock;
        this.$target = $(dom);
        this.$overlay = this.$target.data('overlay');
        this.load_style_options();
        this.get_parent_block();
        this.start();
    },

    // activate drag and drop for the snippets in the snippet toolbar
    _drag_and_drop: function(){
        var self = this;
        this.dropped = false;
        this.$overlay.draggable({
            greedy: true,
            appendTo: 'body',
            cursor: "move",
            handle: ".oe_snippet_move",
            cursorAt: {
                left: 18,
                top: 14
            },
            helper: function() {
                var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
                $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
                $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
                $clone.appendTo("body").removeClass("hidden");
                return $clone;
            },
            start: _.bind(self._drag_and_drop_start, self),
            stop: _.bind(self._drag_and_drop_stop, self)
        });
    },
    _drag_and_drop_after_insert_dropzone: function (){},
    _drag_and_drop_active_drop_zone: function ($zones){
        var self = this;
        $zones.droppable({
            over:   function(){
                $(".oe_drop_zone.hide").removeClass("hide");
                $(this).addClass("hide").first().after(self.$target);
                self.dropped = true;
            },
            out:    function(){
                $(this).removeClass("hide");
                self.$target.detach();
                self.dropped = false;
            },
        });
    },
    _drag_and_drop_start: function (){
        var self = this;
        self.buildingBlock.hide();
        self.buildingBlock.editor_busy = true;
        self.size = {
            width: self.$target.width(),
            height: self.$target.height()
        };
        self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
        self.$target.detach();
        self.$overlay.addClass("hidden");

        var $selector_siblings;
        for (var i=0; i<self.selector_siblings.length; i++) {
            if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all();
            else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all());
        }
        var $selector_children;
        for (var i=0; i<self.selector_children.length; i++) {
            if (!$selector_children) $selector_children = self.selector_children[i].all();
            else $selector_children = $selector_children.add(self.selector_children[i].all());
        }

        self.buildingBlock.activate_insertion_zones($selector_siblings, $selector_children);

        $("body").addClass('move-important');

        self._drag_and_drop_after_insert_dropzone();
        self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
    },
    _drag_and_drop_stop: function (){
        var self = this;
        var $dropzone = this.$target.prev();
        var prev = $dropzone.length && $dropzone[0].previousSibling;
        var next = this.$target.last()[0].nextSibling;
        var $parent = this.$target.parent();

        $(".oe_drop_clone").after(this.$target);

        this.$overlay.removeClass("hidden");
        $("body").removeClass('move-important');
        $('.oe_drop_zone').droppable('destroy').remove();
        $(".oe_drop_clone, .oe_drop_to_remove").remove();

        if (this.dropped) {
            this.buildingBlock.parent.rte.historyRecordUndo(this.$target);

            if (prev) {
                this.$target.insertAfter(prev);
            } else if (next) {
                this.$target.insertBefore(next);
            } else {
                $parent.prepend(this.$target);
            }

            for (var i in this.styles){
                this.styles[i].on_move();
            }
        }

        self.buildingBlock.editor_busy = false;

        self.get_parent_block();
        setTimeout(function () {self.buildingBlock.create_overlay(self.$target);},0);
    },

    load_style_options: function () {
        var self = this;
        var $styles = this.$overlay.find('.oe_options');
        var $ul = $styles.find('ul:first');
        this.styles = {};
        this.selector_siblings = [];
        this.selector_children = [];
        _.each(this.buildingBlock.templateOptions, function (val, option_id) {
            if (!val.selector.is(self.$target)) {
                return;
            }
            if (val['drop-near']) self.selector_siblings.push(val['drop-near']);
            if (val['drop-in']) self.selector_children.push(val['drop-in']);

            var option = val['option'];
            var Editor = options.registry[option] || options.Class;
            var editor = self.styles[option] = new Editor(self.buildingBlock, self, self.$target, option_id);
            $ul.append(editor.$el.addClass("snippet-option-" + option));
            editor.start();
        });

        if (!this.selector_siblings.length && !this.selector_children.length) {
            this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden');
        }

        if ($ul.find("li").length) {
            $styles.removeClass("hidden");
        }
        this.$overlay.find('[data-toggle="dropdown"]').dropdown();
    },

    get_parent_block: function () {
        var self = this;
        var $button = this.$overlay.find('.oe_snippet_parent');
        var $parent = globalSelector.closest(this.$target.parent());
        if ($parent.length) {
            $button.removeClass("hidden");
            $button.off("click").on('click', function (event) {
                event.preventDefault();
                setTimeout(function () {
                    self.buildingBlock.make_active($parent);
                }, 0);
            });
        } else {
            $button.addClass("hidden");
        }
    },

    /*
    *  start
    *  This method is called after init and _readXMLData
    */
    start: function () {
        var self = this;
        if (!this.$target.parent().is(':o_editable')) {
            this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
        } else {
            this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
            this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
            this._drag_and_drop();
        }
    },

    on_clone: function (event) {
        event.preventDefault();
        var $clone = this.$target.clone(false);

        this.buildingBlock.parent.rte.historyRecordUndo(this.$target);
        
        this.$target.after($clone);
        for (var i in this.styles){
            this.styles[i].on_clone($clone);
        }
        this.buildingBlock.create_overlay(this.$target);
        return false;
    },

    on_remove: function (event) {
        event.preventDefault();
        this.on_blur();

        this.buildingBlock.parent.rte.historyRecordUndo(this.$target);

        var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0));
        for (var i in this.styles){
            this.styles[i].on_remove();
        }
        delete this.buildingBlock.snippets[index];

        var $editable = this.$target.closest(".o_editable");

        // remove node and his empty
        var node = this.$target.parent()[0];

        this.$target.remove();
        this.$overlay.remove();

        if (node && node.firstChild) {
            $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
            if (!node.firstChild.tagName && node.firstChild.textContent === " ") {
                node.firstChild.parentNode.removeChild(node.firstChild);
            }
        }

        // clean editor if they are image or table in deleted content
        $(".note-control-selection").hide();
        $('.o_table_handler').remove();

        return false;
    },

    /*
    *  drop_and_build_snippet
    *  This method is called just after that a thumbnail is drag and dropped into a drop zone
    *  (after the insertion of this.$body, if this.$body exists)
    */
    drop_and_build_snippet: function () {
        for (var i in this.styles){
            this.styles[i].drop_and_build_snippet();
        }
    },

    /* on_focus
    *  This method is called when the user click inside the snippet in the dom
    */
    on_focus : function () {
        this.$overlay.addClass('oe_active');
        for (var i in this.styles){
            this.styles[i].on_focus();
        }
    },

    /* on_focus
    *  This method is called when the user click outside the snippet in the dom, after a focus
    */
    on_blur : function () {
        for (var i in this.styles){
            this.styles[i].on_blur();
        }
        this.$overlay.removeClass('oe_active');
    },
});

var data = {
    'Class': BuildingBlock,
    'globalSelector': globalSelector,
};
return data;

});
Example #12
0
File: ace.js Project: Choumy/odoo
    loadResources: function () {
        // Reset resources
        this.resources = {xml: {}, less: {}};
        this.editingSessions = {xml: {}, less: {}};
        this.views = this.resources.xml;
        this.less = this.resources.less;

        // Load resources
        return ajax.jsonRpc("/web_editor/get_assets_editor_resources", "call", {
            key: this.viewKey,
            get_views: !this.options.doNotLoadViews,
            get_less: !this.options.doNotLoadLess,
            bundles: this.options.includeBundles,
            bundles_restriction: this.options.includeAllLess ? [] : this.options.defaultBundlesRestriction,
        }).then((function (resources) {
            _process_views.call(this, resources.views);
            _process_less.call(this, resources.less);
        }).bind(this));

        function _process_views(views) {
            // Only keep the active views and index them by ID.
            _.extend(this.views, _.indexBy(_.filter(views, function (view) {
                return view.active;
            }), "id"));

            // Initialize a 0 level for each view and assign them an array containing their children.
            var self = this;
            var roots = [];
            _.each(this.views, function (view) {
                view.level = 0;
                view.children = [];
            });
            _.each(this.views, function (view) {
                var parentId = view.inherit_id[0];
                var parent = parentId && self.views[parentId];
                if (parent) {
                    parent.children.push(view);
                } else {
                    roots.push(view);
                }
            });

            // Assign the correct level based on children key and save a sorted array where
            // each view is followed by their children.
            this.sorted_views = [];
            function visit(view, level) {
                view.level = level;
                self.sorted_views.push(view);
                _.each(view.children, function (child) {
                    visit(child, level + 1);
                });
            }
            _.each(roots, function (root) {
                visit(root, 0);
            });
        }

        function _process_less(less) {
            // The received less data is already sorted by bundle and DOM order
            this.sorted_less = less;

            // Store the URL ungrouped by bundle and use the URL as key (resource ID)
            var self = this;
            _.each(less, function (bundleInfos) {
                _.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; });
                _.extend(self.less, _.indexBy(bundleInfos[1], "url"));
            });
        }
    },
Example #13
0
File: ace.js Project: Choumy/odoo
odoo.define('web_editor.ace', 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 local_storage = require('web.local_storage');
var session = require("web.session");

var qweb = core.qweb;
var _t = core._t;

ajax.loadXML('/web_editor/static/src/xml/ace.xml', qweb);

function _getCheckReturn(isValid, errorLine, errorMessage) {
    return {
        isValid: isValid,
        error: isValid ? null : {
            line: errorLine,
            message: errorMessage,
        },
    };
}

function checkXML(xml) {
    if (typeof window.DOMParser != "undefined") {
        var xmlDoc = (new window.DOMParser()).parseFromString(xml, "text/xml");
        var error = xmlDoc.getElementsByTagName("parsererror");
        if (error.length > 0) {
            return _getCheckReturn(false, parseInt(error[0].innerHTML.match(/[Ll]ine[^\d]+(\d+)/)[1], 10), error[0].innerHTML);
        }
    } else if (typeof window.ActiveXObject != "undefined" && new window.ActiveXObject("Microsoft.XMLDOM")) {
        var xmlDocIE = new window.ActiveXObject("Microsoft.XMLDOM");
        xmlDocIE.async = "false";
        xmlDocIE.loadXML(xml);
        if (xmlDocIE.parseError.line > 0) {
            return _getCheckReturn(false, xmlDocIE.parseError.line, xmlDocIE.parseError.reason);
        }
    }
    return _getCheckReturn(true);
}

function formatXML(xml) {
    return window.vkbeautify.xml(xml, 4);
}

var checkLESS = (function () {
    var mapping = {
        '{': '}', '}': '{',
        '(': ')', ')': '(',
        '[': ']', ']': '[',
    };
    var openings = ['{', '(', '['];
    var closings = ['}', ')', ']'];

    return function (less) {
        var stack = [];
        var line = 1;
        for (var i = 0 ; i < less.length ; i++) {
            if (_.contains(openings, less[i])) {
                stack.push(less[i]);
            } else if (_.contains(closings, less[i])) {
                if (stack.pop() !== mapping[less[i]]) {
                    return _getCheckReturn(false, line, _t("Unexpected ") + less[i]);
                }
            } else if (less[i] === '\n') {
                line++;
            }
        }
        if (stack.length > 0) {
            return _getCheckReturn(false, line, _t("Expected ") + mapping[stack.pop()]);
        }
        return _getCheckReturn(true);
    };
})();

function formatLESS(less) {
    return less;
}

/**
 * The ViewEditor Widget allow to visualize resources (by default, XML views) and edit them.
 */
var ViewEditor = Widget.extend({
    template: 'web_editor.ace_view_editor',
    events: {
        "click .o_ace_type_switcher_choice": function (e) {
            e.preventDefault();
            this.switchType($(e.target).data("type"));
        },
        'change .o_res_list': function () {
            this.displayResource(this.selectedResource());
        },
        'click .js_include_bundles': function (e) {
            this.options.includeBundles = $(e.target).prop("checked");
            this.loadResources().then(this._updateViewSelectDOM.bind(this));
        },
        'click .js_include_all_less': function (e) {
            this.options.includeAllLess = $(e.target).prop("checked");
            this.loadResources().then(this._updateViewSelectDOM.bind(this));
        },
        'click button[data-action=save]': 'saveResources',
        "click button[data-action=\"reset\"]": function () {
            var self = this;
            Dialog.confirm(this, _t("If you reset this file, all your customizations will be lost as it will be reverted to the default file."), {
                title: _t("Careful !"),
                confirm_callback: function () {
                    self.resetResource(self.selectedResource());
                },
            });
        },
        'click button[data-action=format]': 'formatResource',
        'click button[data-action=close]': 'do_hide',
    },
    /**
     * The init method should initialize the parameters of which information the ace editor will
     * have to load.
     * @param parent: the parent element of the editor widget.
     * @param viewKey: xml_id of the view whose linked resources are to be loaded.
                Also allow to receive the id directly.
     * @param options: an object containing some options
     *          - initialResID: a specific view ID to load on start (otherwise the main view ID
     *              associated with the specified viewKey will be used).
     *          - includeBundles: whether or not the assets bundles templates needs to be loaded.
     */
    init: function (parent, viewKey, options) {
        this._super.apply(this, arguments);

        this.viewKey = viewKey;
        this.options = _.defaults({}, options, {
            position: 'right',
            doNotLoadViews: false,
            doNotLoadLess: false,
            includeBundles: false,
            includeAllLess: false,
            defaultBundlesRestriction: [],
        });

        this.resources = {xml: {}, less: {}};
        this.editingSessions = {xml: {}, less: {}};
        this.currentType = "xml";

        // Alias
        this.views = this.resources.xml;
        this.less = this.resources.less;
    },
    /**
     * The willStart method is in charge of loading everything the ace library needs to work.
     * It also loads the resources to visualize. See @loadResources.
     */
    willStart: function () {
        var js_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () {
            return $.when(
                ajax.loadJS('/web/static/lib/ace/mode-xml.js'),
                ajax.loadJS('/web/static/lib/ace/mode-less.js'),
                ajax.loadJS('/web/static/lib/ace/theme-monokai.js')
            );
        });
        return $.when(this._super.apply(this, arguments), js_def, this.loadResources());
    },
    /**
     * The start method is in charge of initializing the library and initial view once the DOM is
     * ready. It also initializes the resize feature of the ace editor.
     * @return a deferred which is resolved when the widget DOM content is fully loaded.
     */
    start: function () {
        this.$viewEditor = this.$("#ace-view-editor");
        this.$editor = this.$(".ace_editor");

        this.$typeSwitcherChoices = this.$(".o_ace_type_switcher_choice");
        this.$typeSwitcherBtn = this.$(".o_ace_type_switcher > .dropdown-toggle");

        this.$lists = {
            xml: this.$("#ace-view-list"),
            less: this.$("#ace-less-list")
        };
        this.$includeBundlesArea = this.$(".oe_include_bundles");
        this.$includeAllLessArea = this.$(".o_include_all_less");
        this.$viewID = this.$("#ace-view-id > span");

        this.$formatButton = this.$("button[data-action=\"format\"]");
        this.$resetButton = this.$("button[data-action=\"reset\"]");

        this.aceEditor = window.ace.edit(this.$viewEditor[0]);
        this.aceEditor.setTheme("ace/theme/monokai");

        var refX = 0;
        var resizing = false;
        var minWidth = 400;
        var debounceStoreEditorWidth = _.debounce(storeEditorWidth, 500);

        this._updateViewSelectDOM();

        var initResID;
        var initType;
        if (this.options.initialResID) {
            initResID = this.options.initialResID;
            initType = (_.isString(initResID) && initResID[0] === '/') ? "less" : "xml";
        } else {
            if (!this.options.doNotLoadLess) {
                initResID = this.sorted_less[0][1][0].url; // first bundle, less files, first one
                initType = "less";
            }
            if (!this.options.doNotLoadViews) {
                initResID = (typeof this.viewKey === "number" ? this.viewKey : _.findWhere(this.views, {xml_id: this.viewKey}).id);
                initType = "xml";
            }
        }
        if (initResID) {
            this.displayResource(initResID, initType);
        }

        if (!this.sorted_views.length || !this.sorted_less.length) {
            _.defer((function () {
                this.switchType(this.sorted_views.length ? "xml" : "less");
                this.$typeSwitcherBtn.parent(".btn-group").addClass("hidden");
            }).bind(this));
        }

        $(document).on("mouseup.ViewEditor", stopResizing.bind(this)).on("mousemove.ViewEditor", updateWidth.bind(this));
        if (this.options.position === 'left') {
            this.$('.ace_scroller').after($('<div>').addClass('ace_resize_bar'));
            this.$('.ace_gutter').css({'cursor': 'default'});
            this.$el.on("mousedown.ViewEditor", ".ace_resize_bar", startResizing.bind(this));
        } else {
            this.$el.on("mousedown.ViewEditor", ".ace_gutter", startResizing.bind(this));
        }

        resizeEditor.call(this, readEditorWidth.call(this));

        return this._super.apply(this, arguments);

        function resizeEditor(target) {
            var width = Math.min(document.body.clientWidth, Math.max(parseInt(target, 10), minWidth));
            this.$editor.width(width);
            this.aceEditor.resize();
            this.$el.width(width);
        }
        function storeEditorWidth() {
            local_storage.setItem('ace_editor_width', this.$el.width());
        }
        function readEditorWidth() {
            var width = local_storage.getItem('ace_editor_width');
            return parseInt(width || 720, 10);
        }
        function startResizing(e) {
            refX = e.pageX;
            resizing = true;
        }
        function stopResizing() {
            resizing = false;
        }
        function updateWidth(e) {
            if (!resizing) return;

            var offset = e.pageX - refX;
            if (this.options.position === 'left') {
                offset = - offset;
            }
            var width = this.$el.width() - offset;
            refX = e.pageX;
            resizeEditor.call(this, width);
            debounceStoreEditorWidth.call(this);
        }
    },
    /**
     * The destroy method unbinds custom events binded to the document element.
     */
    destroy: function () {
        this._super.apply(this, arguments);
        this.$el.off(".ViewEditor");
        $(document).off(".ViewEditor");
    },
    /**
     * The loadResources method is in charge of loading data the ace editor will vizualize
     * and of processing them.
     * Default behavior is loading the activate views, index them and build their hierarchy.
     * @return a deferred which is resolved once everything is loaded and processed.
     */
    loadResources: function () {
        // Reset resources
        this.resources = {xml: {}, less: {}};
        this.editingSessions = {xml: {}, less: {}};
        this.views = this.resources.xml;
        this.less = this.resources.less;

        // Load resources
        return ajax.jsonRpc("/web_editor/get_assets_editor_resources", "call", {
            key: this.viewKey,
            get_views: !this.options.doNotLoadViews,
            get_less: !this.options.doNotLoadLess,
            bundles: this.options.includeBundles,
            bundles_restriction: this.options.includeAllLess ? [] : this.options.defaultBundlesRestriction,
        }).then((function (resources) {
            _process_views.call(this, resources.views);
            _process_less.call(this, resources.less);
        }).bind(this));

        function _process_views(views) {
            // Only keep the active views and index them by ID.
            _.extend(this.views, _.indexBy(_.filter(views, function (view) {
                return view.active;
            }), "id"));

            // Initialize a 0 level for each view and assign them an array containing their children.
            var self = this;
            var roots = [];
            _.each(this.views, function (view) {
                view.level = 0;
                view.children = [];
            });
            _.each(this.views, function (view) {
                var parentId = view.inherit_id[0];
                var parent = parentId && self.views[parentId];
                if (parent) {
                    parent.children.push(view);
                } else {
                    roots.push(view);
                }
            });

            // Assign the correct level based on children key and save a sorted array where
            // each view is followed by their children.
            this.sorted_views = [];
            function visit(view, level) {
                view.level = level;
                self.sorted_views.push(view);
                _.each(view.children, function (child) {
                    visit(child, level + 1);
                });
            }
            _.each(roots, function (root) {
                visit(root, 0);
            });
        }

        function _process_less(less) {
            // The received less data is already sorted by bundle and DOM order
            this.sorted_less = less;

            // Store the URL ungrouped by bundle and use the URL as key (resource ID)
            var self = this;
            _.each(less, function (bundleInfos) {
                _.each(bundleInfos[1], function (info) { info.bundle_xmlid = bundleInfos[0].xmlid; });
                _.extend(self.less, _.indexBy(bundleInfos[1], "url"));
            });
        }
    },
    /**
     * The private _updateViewSelectDOM method purpose is to render the content of the view/file
     * select DOM element according to current widget data.
     */
    _updateViewSelectDOM: function () {
        var currentId = this.selectedResource();

        var self = this;
        this.$lists.xml.empty();
        _.each(this.sorted_views, function (view) {
            self.$lists.xml.append($("<option/>", {
                value: view.id,
                text: view.name,
                selected: currentId === view.id,
                "data-level": view.level,
                "data-debug": view.xml_id,
            }));
        });

        this.$lists.less.empty();
        _.each(this.sorted_less, function (bundleInfos) {
            var $optgroup = $("<optgroup/>", {
                label: bundleInfos[0].name,
            }).appendTo(self.$lists.less);
            _.each(bundleInfos[1], function (lessInfo) {
                var name = lessInfo.url.substring(_.lastIndexOf(lessInfo.url, "/") + 1, lessInfo.url.length - 5);
                $optgroup.append($("<option/>", {
                    value: lessInfo.url,
                    text: name,
                    selected: currentId === lessInfo.url,
                    "data-debug": lessInfo.url,
                    "data-customized": lessInfo.customized
                }));
            });
        });

        this.$lists.xml.select2("destroy");
        this.$lists.xml.select2({
            formatResult: _formatDisplay,
            formatSelection: _formatDisplay,
        });
        this.$lists.less.select2("destroy");
        this.$lists.less.select2({
            formatResult: _formatDisplay,
            formatSelection: _formatDisplay,
        });

        function _formatDisplay(data) {
            var $elem = $(data.element);

            var $div = $("<div/>",  {
                text: data.text || "",
                style: "padding: 0 0 0 " + (24 * $elem.data("level")) + "px",
            });

            if ($elem.data("dirty") || $elem.data("customized")) {
                $div.prepend($("<span/>", {
                    class: "fa fa-floppy-o " + ($elem.data("dirty") ? "text-warning" : "text-success"),
                    style: "margin-right: 8px;",
                }));
            }

            if (session.debug && $elem.data("debug")) {
                $div.append($("<span/>", {
                    text: " (" + $elem.data("debug") + ")",
                    class: "text-muted",
                    style: "font-size: 80%",
                }));
            }

            return $div;
        }
    },
    /**
     * The switchType method switches to the LESS or XML edition. Calling this method will adapt all DOM elements to
     * keep the editor consistent.
     * @param type: either "xml" or "less"
     */
    switchType: function (type) {
        this.currentType = type;
        this.$typeSwitcherBtn.html(this.$typeSwitcherChoices.filter("[data-type=\"" + type + "\"]").html());
        _.each(this.$lists, function ($list, _type) { $list.toggleClass("hidden", type !== _type); });
        this.$lists[type].change();

        this.$includeBundlesArea.toggleClass("hidden", this.currentType === "less" || !session.debug);
        this.$includeAllLessArea.toggleClass("hidden", this.currentType === "xml" || !session.debug || this.options.defaultBundlesRestriction.length === 0);
        this.$formatButton.toggleClass("hidden", this.currentType === "less");
    },
    /**
     * The selectedResource method returns the currently selected resource id (view ID or less file URL).
     * @return the currently resource id (view ID or less file URL)
     */
    selectedResource: function () {
        return this.$lists[this.currentType].select2("val");
    },
    /**
     * The displayResource method forces the view/less file identified by its ID/URL to be displayed in the editor.
     * The method will update the resource select DOM element as well.
     * @param resID: the ID/URL of the view/less file to display
     * @param type: the type of resource (either "xml" or "less")
     */
    displayResource: function (resID, type) {
        if (type) this.switchType(type);

        var editingSession = this.editingSessions[this.currentType][resID];
        if (!editingSession) {
            editingSession = this.editingSessions[this.currentType][resID] = this._buildEditingSession(resID);
        }
        this.aceEditor.setSession(editingSession);

        if (this.currentType === "xml") {
            this.$viewID.text(_.str.sprintf(_t("Template ID: %s"), this.views[resID].xml_id));
        } else {
            this.$viewID.text(_.str.sprintf(_t("Less file: %s"), resID));
        }
        this.$lists[this.currentType].select2("val", resID);

        this.$resetButton.toggleClass("hidden", this.currentType === "xml" || !this.less[resID].customized);
    },
    /**
     * The resetResource method forces the view/less file identified by its ID/URL to be reset to the way it was before
     * the user started editing it. TODO view reset is not supported yet
     * @param resID: the ID/URL of the view/less file to reset (default to the currently selected one)
     * @param type: the type of the resource to reset (default to the currently selected one)
     * @return a deferred which is resolved once the resource has been reset
     */
    resetResource: function (resID, type) {
        resID = resID || this.selectedResource();
        type = type || this.currentType;

        if (this.currentType === "xml") {
            return $.Defered().reject(_t("Reseting views is not supported yet")); // TODO
        } else {
            return ajax.jsonRpc("/web_editor/reset_less", "call", {
                url: resID,
                bundle_xmlid: this.less[resID].bundle_xmlid,
            });
        }
    },
    /**
     * The private _buildEditingSession method initializes a text editor for the specified resource.
     * @param resID: the ID/URL of the view/less file whose text editor it will be
     * @param type: the type of the given resource (default to the currently selected one)
     * @return an ace.EditSession object linked to the specified resID.
     */
    _buildEditingSession: function(resID, type) {
        var self = this;
        type = type || this.currentType;
        var editingSession = new window.ace.EditSession(this.resources[type][resID].arch);
        editingSession.setUseWorker(false);
        editingSession.setMode("ace/mode/" + (type || this.currentType));
        editingSession.setUndoManager(new window.ace.UndoManager());
        editingSession.on("change", function () {
            _.defer(function () {
                self._toggleDirtyInfo(resID);
                self._showErrorLine();
            });
        });
        return editingSession;
    },
    /**
     * The private _toggleDirtyInfo method update the select option DOM element associated with
     * a particular resID to indicate if the option is dirty or not.
     * @param resID: the ID/URL of the view/less file whose option has to be updated
     * @param type: the type of the given resource (default to the currently selected one)
     * @param isDirty: a boolean to indicate if the view is dirty or not ; default to content of UndoManager
     */
    _toggleDirtyInfo: function (resID, type, isDirty) {
        type = type || this.currentType;

        if (!resID || !this.editingSessions[type][resID]) return;

        var $option = this.$lists[type].find("[value='" + resID + "']");
        if (isDirty === undefined) {
            isDirty = this.editingSessions[type][resID].getUndoManager().hasUndo();
        }
        $option.data("dirty", isDirty);
    },
    /**
     * The formatResource method formats the current resource being vizualized.
     * TODO formatting LESS files is not supported yet.
     */
    formatResource: function () {
        var res = this.aceEditor.getValue();
        var check = (this.currentType === "xml" ? checkXML : checkLESS)(res);
        if (check.isValid) {
            this.aceEditor.setValue((this.currentType === "xml" ? formatXML : formatLESS)(res));
        } else {
            this._showErrorLine(check.error.line, check.error.message, this.selectedResource());
        }
    },
    /**
     * The saveResources method is in charge of saving every resource that has been modified.
     * If one cannot be saved, none is saved and an error message is displayed.
     * @return a deferred whose status indicates if the save is finished or if an error occured.
     */
    saveResources: function () {
        var toSave = {};
        var errorFound = false;
        _.each(this.editingSessions, (function (editingSessions, type) {
            if (errorFound) return;

            var dirtySessions = _.pick(editingSessions, function (session) {
                return session.getUndoManager().hasUndo();
            });
            toSave[type] = _.map(dirtySessions, function (session, resID) {
                return {
                    id: parseInt(resID, 10) || resID,
                    text: session.getValue(),
                };
            });

            this._showErrorLine();
            for (var i = 0 ; i < toSave[type].length && !errorFound ; i++) {
                var check = (type === "xml" ? checkXML : checkLESS)(toSave[type][i].text);
                if (!check.isValid) {
                    this._showErrorLine(check.error.line, check.error.message, toSave[type][i].id, type);
                    errorFound = toSave[type][i];
                }
            }
        }).bind(this));
        if (errorFound) return $.Deferred().reject(errorFound);

        var defs = [];
        _.each(toSave, (function (_toSave, type) {
            defs = defs.concat(_.map(_toSave, (type === "xml" ? this._saveView : this._saveLess).bind(this)));
        }).bind(this));

        return $.when.apply($, defs).fail((function (session, error) {
            Dialog.alert(this, "", {
                title: _t("Server error"),
                $content: $("<div/>").html(
                    _t("A server error occured. Please check you correctly signed in and that the file you are saving is well-formed.")
                    + "<br/>"
                    + error
                )
            });
        }).bind(this));
    },
    /**
     * The private _saveView method is in charge of saving an unique XML view.
     * @param session: an object which contains the "id" and the "text" of the view to save.
     * @return a deferred whose status indicates if the save is finished or if an error occured.
     */
    _saveView: function (session) {
        var def = $.Deferred();

        var self = this;
        ajax.jsonRpc('/web/dataset/call', 'call', {
            model: 'ir.ui.view',
            method: 'write',
            args: [[session.id], {arch: session.text}, _.extend(base.get_context(), {lang: null})],
        }).then(function () {
            self._toggleDirtyInfo(session.id, "xml", false);
            def.resolve();
        }, function (source, error) {
            def.reject(session, error);
        });

        return def;
    },
    /**
     * The private _saveLess method is in charge of saving an unique LESS file.
     * @param session: an object which contains the "id" (url) and the "text" of the view to save.
     * @return a deferred whose status indicates if the save is finished or if an error occured.
     */
    _saveLess: function (session) {
        var def = $.Deferred();

        var self = this;
        ajax.jsonRpc("/web_editor/save_less", "call", {
            url: session.id,
            bundle_xmlid: this.less[session.id].bundle_xmlid,
            content: session.text,
        }).then(function () {
            self._toggleDirtyInfo(session.id, "less", false);
            def.resolve();
        }, function (source, error) {
            def.reject(session, error);
        });

        return def;
    },
    /**
     * The private _showErrorLine method is designed to show a line which produced an error. Red color
     * is added to the editor, the cursor move to the line and a message is opened on click on the line
     * number. If the _showErrorLine is called without argument, the effects are removed.
     * @param line: the line number to highlight
     * @param message: the message to show on click on the line number
     * @param resID: the ID/URL of the view/less file whose line is to highlight
     * @param type: the type of the given resource
     */
    _showErrorLine: function (line, message, resID, type) {
        if (line === undefined || line <= 0) {
            if (this.$errorLine) {
                this.$errorLine.removeClass("o_error");
                this.$errorLine.off(".o_error");
                this.$errorLine = undefined;
                this.$errorContent.removeClass("o_error");
                this.$errorContent = undefined;
            }
            return;
        }

        if (type) this.switchType(type);

        if (this.selectedResource() === resID) {
            __showErrorLine.call(this, line);
        } else {
            var onChangeSession = (function () {
                this.aceEditor.off("changeSession", onChangeSession);
                _.delay(__showErrorLine.bind(this, line), 400);
            }).bind(this);
            this.aceEditor.on('changeSession', onChangeSession);
            this.displayResource(resID, this.currentType);
        }

        function __showErrorLine(line) {
            this.aceEditor.gotoLine(line);
            this.$errorLine = this.$viewEditor.find(".ace_gutter-cell").filter(function () {
                return parseInt($(this).text()) === line;
            }).addClass("o_error");
            this.$errorLine.addClass("o_error").on("click.o_error", function () {
                var $message = $("<div/>").html(message);
                $message.text($message.text());
                Dialog.alert(this, "", {$content: $message});
            });
            this.$errorContent = this.$viewEditor.find(".ace_scroller").addClass("o_error");
        }
    },
});

return ViewEditor;
});
Example #14
0
odoo.define('web_editor.rte.summernote', function (require) {
'use strict';

var ajax = require('web.ajax');
var Class = require('web.Class');
var core = require('web.core');
var ColorpickerDialog = require('web.colorpicker');
var mixins = require('web.mixins');
var base = require('web_editor.base');
var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
var weWidgets = require('web_editor.widget');

var QWeb = core.qweb;
var _t = core._t;

ajax.jsonRpc('/web/dataset/call', 'call', {
    'model': 'ir.ui.view',
    'method': 'read_template',
    'args': ['web_editor.colorpicker', weContext.get()]
}).done(function (data) {
    QWeb.add_template(data);
});

// Summernote Lib (neek change to make accessible: method and object)
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var eventHandler = $.summernote.eventHandler;
var renderer = $.summernote.renderer;

var tplButton = renderer.getTemplate().button;
var tplIconButton = renderer.getTemplate().iconButton;
var tplDropdown = renderer.getTemplate().dropdown;

function _rgbToHex(cssColor) {
    var rgba = cssColor.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+(?:\.\d+)?))?\)$/);
    if (!rgba) {
        return cssColor;
    }
    if (rgba[4]) {
        return cssColor;
    }
    var hex = ColorpickerDialog.prototype.convertRgbToHex(
        parseInt(rgba[1]),
        parseInt(rgba[2]),
        parseInt(rgba[3])
    );
    if (!hex) {
        return cssColor; // TODO handle error
    }
    return hex.hex.toUpperCase();
}

// Update and change the popovers content, and add history button
var fn_createPalette = renderer.createPalette;
renderer.createPalette = function ($container, options) {
    fn_createPalette.call(this, $container, options);

    if (!QWeb.has_template('web_editor.colorpicker')) {
        return;
    }

    var $clpicker = $(QWeb.render('web_editor.colorpicker'));

    var groups;
    if ($clpicker.is("colorpicker")) {
        groups = _.map($clpicker.find('[data-name="theme"]'), function (el) {
            return $(el).find("button").empty();
        });
    } else {
        groups = [$clpicker.find("button").empty()];
    }

    var html = "<h6 class='mt-2'>" + _t("Theme colors") + "</h6>" + _.map(groups, function ($group) {
        var $row = $("<div/>", {"class": "note-color-row mb8"}).append($group);
        var $after_breaks = $row.find(".o_small + :not(.o_small)");
        if ($after_breaks.length === 0) {
            $after_breaks = $row.find(":nth-child(8n+9)");
        }
        $after_breaks.addClass("o_clear");
        return $row[0].outerHTML;
    }).join("") + "<h6 class='mt-2'>" + _t("Common colors") + "</h6>";
    var $palettes = $container.find(".note-color .note-color-palette");
    $palettes.prepend(html);

    // Find the custom colors which are used in the page and add them to the color palette
    var colors = [];
    var $editable = window.__EditorMenuBar_$editable || $();
    _.each($editable.find('[style*="color"]'), function (element) {
        if (element.style.color) {
            colors.push(element.style.color);
        }
        if (element.style.backgroundColor) {
            colors.push(element.style.backgroundColor);
        }
    });

    var $customColorPalettes = $container.find('.note-color .note-custom-color-palette').append($('<div/>', {class: "note-color-row"}));
    var $customColorRows = $customColorPalettes.find('.note-color-row');
    _.each(_.uniq(colors), function (color) {
        var hexColor = _rgbToHex(color);
        if (_.indexOf(_.flatten(options.colors), hexColor) < 0) {
            // Create button for used custom color for backColor and foreColor both and add them into palette
            $customColorRows.append('<button type="button" class="o_custom_color" data-color="' + color + '" style="background-color:' + color + ';" />');
        }
    });

    $palettes.push.apply($palettes, $customColorPalettes);

    var $fore = $palettes.filter(":even").find("button:not(.note-color-btn)").addClass("note-color-btn");
    var $bg = $palettes.filter(":odd").find("button:not(.note-color-btn)").addClass("note-color-btn");
    $fore.each(function () {
        var $el = $(this);
        var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
        $el.attr('data-event', 'foreColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
    });
    $bg.each(function () {
        var $el = $(this);
        var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
        $el.attr('data-event', 'backColor').attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
    });
};

var fn_tplPopovers = renderer.tplPopovers;
renderer.tplPopovers = function (lang, options) {
    var $popover = $(fn_tplPopovers.call(this, lang, options));

    var $imagePopover = $popover.find('.note-image-popover');
    var $linkPopover = $popover.find('.note-link-popover');
    var $airPopover = $popover.find('.note-air-popover');

    //////////////// image popover

    // add center button for images
    $(tplIconButton('fa fa-align-center', {
        title: _t('Center'),
        event: 'floatMe',
        value: 'center'
    })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]'));
    $imagePopover.find('button[data-event="removeMedia"]').parent().remove();
    $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove();

    // padding button
    var $padding = $('<div class="btn-group"/>');
    $padding.insertBefore($imagePopover.find('.btn-group:first'));
    var dropdown_content = [
        '<li><a class="dropdown-item" data-event="padding" href="#" data-value="">'+_t('None')+'</a></li>',
        '<li><a class="dropdown-item" data-event="padding" href="#" data-value="small">'+_t('Small')+'</a></li>',
        '<li><a class="dropdown-item" data-event="padding" href="#" data-value="medium">'+_t('Medium')+'</a></li>',
        '<li><a class="dropdown-item" data-event="padding" href="#" data-value="large">'+_t('Large')+'</a></li>',
        '<li><a class="dropdown-item" data-event="padding" href="#" data-value="xl">'+_t('Xl')+'</a></li>',
    ];
    $(tplIconButton('fa fa-plus-square-o', {
        title: _t('Padding'),
        dropdown: tplDropdown(dropdown_content)
    })).appendTo($padding);

    // circle, boxed... options became toggled
    $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove();
    var $button = $(tplIconButton('fa fa-sun-o', {
        title: _t('Shadow'),
        event: 'imageShape',
        value: 'shadow'
    })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="rounded-circle"]'));

    // add spin for fa
    var $spin = $('<div class="btn-group d-none only_fa"/>').insertAfter($button.parent());
    $(tplIconButton('fa fa-refresh', {
            title: _t('Spin'),
            event: 'imageShape',
            value: 'fa-spin'
        })).appendTo($spin);

    // resize for fa
    var $resizefa = $('<div class="btn-group d-none only_fa"/>')
        .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])'));
    for (var size=1; size<=5; size++) {
        $(tplButton('<span class="note-fontsize-10">'+size+'x</span>', {
          title: size+"x",
          event: 'resizefa',
          value: size+''
        })).appendTo($resizefa);
    }
    var $colorfa = $airPopover.find('.note-color').clone();
    $colorfa.find(".dropdown-menu").css('min-width', '172px');
    $resizefa.after($colorfa);

    // show dialog box and delete
    var $imageprop = $('<div class="btn-group"/>');
    $imageprop.appendTo($imagePopover.find('.popover-body'));
    $(tplIconButton('fa fa-file-image-o', {
            title: _t('Edit'),
            event: 'showImageDialog'
        })).appendTo($imageprop);
    $(tplIconButton('fa fa-trash-o', {
            title: _t('Remove'),
            event: 'delete'
        })).appendTo($imageprop);

    $(tplIconButton('fa fa-crop', {
        title: _t('Crop Image'),
        event: 'cropImage',
    })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-thumbnail"]'));

    $imagePopover.find('.popover-body').append($airPopover.find(".note-history").clone());

    $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone());

    var $alt = $('<div class="btn-group"/>');
    $alt.appendTo($imagePopover.find('.popover-body'));
    $alt.append('<button class="btn btn-secondary" data-event="alt"><strong>' + _t('Description') + ': </strong><span class="o_image_alt"/></button>');

    //////////////// link popover

    $linkPopover.find('.popover-body').append($airPopover.find(".note-history").clone());

    $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link");
    $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone());

    //////////////// text/air popover

    //// highlight the text format
    $airPopover.find('.note-style .dropdown-toggle').on('mousedown', function () {
        var $format = $airPopover.find('[data-event="formatBlock"]');
        var node = range.create().sc;
        var formats = $format.map(function () { return $(this).data("value"); }).get();
        while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) {
            node = node.parentNode;
        }
        $format.removeClass('active');
        $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]')
            .addClass("active");
    });

    //////////////// tooltip

    setTimeout(function () {
        $airPopover.add($linkPopover).add($imagePopover).find("button")
            .tooltip('dispose')
            .tooltip({
                container: 'body',
                trigger: 'hover',
                placement: 'bottom'
            }).on('click', function () {$(this).tooltip('hide');});
    });

    return $popover;
};

var fn_boutton_update = eventHandler.modules.popover.button.update;
eventHandler.modules.popover.button.update = function ($container, oStyle) {
    // stop animation when edit content
    var previous = $(".note-control-selection").data('target');
    if (previous) {
        var $previous = $(previous);
        $previous.css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""});
        $previous.find('.o_we_selected_image').addBack('.o_we_selected_image').removeClass('o_we_selected_image');
    }
    // end

    fn_boutton_update.call(this, $container, oStyle);

    $container.find('.note-color').removeClass('d-none');

    if (oStyle.image) {
        $container.find('[data-event]').removeClass("active");

        $container.find('a[data-event="padding"][data-value="small"]').toggleClass("active", $(oStyle.image).hasClass("padding-small"));
        $container.find('a[data-event="padding"][data-value="medium"]').toggleClass("active", $(oStyle.image).hasClass("padding-medium"));
        $container.find('a[data-event="padding"][data-value="large"]').toggleClass("active", $(oStyle.image).hasClass("padding-large"));
        $container.find('a[data-event="padding"][data-value="xl"]').toggleClass("active", $(oStyle.image).hasClass("padding-xl"));
        $container.find('a[data-event="padding"][data-value=""]').toggleClass("active", !$container.find('li a.active[data-event="padding"]').length);

        $(oStyle.image).addClass('o_we_selected_image');

        if (dom.isImgFont(oStyle.image)) {
            $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', $(oStyle.image).css('color'));
            $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', $(oStyle.image).css('background-color'));
            $container.find('.note-fore-color-preview > button').css('border-bottom-color', $(oStyle.image).css('color'));
            $container.find('.note-back-color-preview > button').css('border-bottom-color', $(oStyle.image).css('background-color'));

            $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-value="img-thumbnail"])').addClass('d-none');
            $container.find('.only_fa').removeClass('d-none');
            $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x"));
            $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x"));
            $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
            $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
            $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);

            $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
            $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
            $container.find('.btn-group:has(button[data-event="imageShape"])').removeClass("d-none");

        } else {
            $container.find('.d-none:not(.only_fa, .note-recent-color)').removeClass('d-none');
            $container.find('.only_fa').addClass('d-none');
            var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
            if (width) {
                width = width[2];
            }
            $container.find('button[data-event="resize"][data-value="auto"]').toggleClass("active", width !== "100%" && width !== "50%" && width !== "25%");
            $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%");
            $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%");
            $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%");

            $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));

            if (!$(oStyle.image).is("img")) {
                $container.find('.btn-group:has(button[data-event="imageShape"])').addClass('d-none');
            }

            $container.find('.note-color').addClass('d-none');
        }

        $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("float-left"));
        $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("d-block mx-auto"));
        $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("float-right"));

        $(oStyle.image).trigger('attributes_change');
    } else {
        $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', oStyle.color);
        $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', oStyle['background-color']);
        $container.find('.note-fore-color-preview > button').css('border-bottom-color', oStyle.color);
        $container.find('.note-back-color-preview > button').css('border-bottom-color', oStyle['background-color']);
    }
};

var fn_popover_update = eventHandler.modules.popover.update;
eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
    var $imagePopover = $popover.find('.note-image-popover');
    var $linkPopover = $popover.find('.note-link-popover');
    var $airPopover = $popover.find('.note-air-popover');

    fn_popover_update.call(this, $popover, oStyle, isAirMode);

    if (oStyle.image) {
        if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
            oStyle.image = oStyle.image.parentNode;
        }
        var alt =  $(oStyle.image).attr("alt");

        $imagePopover.find('.o_image_alt').text( (alt || "").replace(/&quot;/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG");
        $imagePopover.show();

        // for video tag (non-void) we select the range over the tag,
        // for other media types we get the first descendant leaf element
        var target_node = oStyle.image;
        if (!oStyle.image.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
            target_node = dom.firstChild(target_node);
        }
        range.createFromNode(target_node).select();
        // save range on the editor so it is not lost if restored
        eventHandler.modules.editor.saveRange(dom.makeLayoutInfo(target_node).editable());
    } else {
        $(".note-control-selection").hide();
    }

    if (oStyle.image || (oStyle.range && (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)))) || (oStyle.image && !$(oStyle.image).closest('a').length)) {
        $linkPopover.hide();
        oStyle.anchor = false;
    }

    if (oStyle.image || oStyle.anchor || (oStyle.range && !$(oStyle.range.sc).closest('.note-editable').length)) {
        $airPopover.hide();
    } else {
        $airPopover.show();
    }
};

var fn_handle_update = eventHandler.modules.handle.update;
eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) {
    fn_handle_update.call(this, $handle, oStyle, isAirMode);
    if (oStyle.image) {
        $handle.find('.note-control-selection').hide();
    }
};

// Hack for image and link editor
function getImgTarget($editable) {
    var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined;
    return $(".note-control-selection", $handle).data('target');
}
eventHandler.modules.editor.padding = function ($editable, sValue) {
    var $target = $(getImgTarget($editable));
    var paddings = "small medium large xl".split(/\s+/);
    $editable.data('NoteHistory').recordUndo();
    if (sValue.length) {
        paddings.splice(paddings.indexOf(sValue),1);
        $target.toggleClass('padding-'+sValue);
    }
    $target.removeClass("padding-" + paddings.join(" padding-"));
};
eventHandler.modules.editor.resize = function ($editable, sValue) {
    var $target = $(getImgTarget($editable));
    $editable.data('NoteHistory').recordUndo();
    var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/);
    if (width) {
        width = width[2]/100;
    }
    $target.css('width', (width !== sValue && sValue !== "auto") ? (sValue * 100) + '%' : '');
};
eventHandler.modules.editor.resizefa = function ($editable, sValue) {
    var $target = $(getImgTarget($editable));
    $editable.data('NoteHistory').recordUndo();
    $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, ''));
    if (+sValue > 1) {
        $target.addClass('fa-'+sValue+'x');
    }
};
eventHandler.modules.editor.floatMe = function ($editable, sValue) {
    var $target = $(getImgTarget($editable));
    $editable.data('NoteHistory').recordUndo();
    switch (sValue) {
        case 'center': $target.toggleClass('d-block mx-auto').removeClass('float-right float-left'); break;
        case 'left': $target.toggleClass('float-left').removeClass('float-right d-block mx-auto'); break;
        case 'right': $target.toggleClass('float-right').removeClass('float-left d-block mx-auto'); break;
    }
};
eventHandler.modules.editor.imageShape = function ($editable, sValue) {
    var $target = $(getImgTarget($editable));
    $editable.data('NoteHistory').recordUndo();
    $target.toggleClass(sValue);
};

eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, linkInfo) {
    $editable.data('range').select();
    $editable.data('NoteHistory').recordUndo();

    var def = new $.Deferred();
    core.bus.trigger('link_dialog_demand', {
        $editable: $editable,
        linkInfo: linkInfo,
        onSave: function (linkInfo) {
            linkInfo.range.select();
            $editable.data('range', linkInfo.range);
            def.resolve(linkInfo);
            $editable.trigger('keyup');
            $('.note-popover .note-link-popover').show();
        },
        onCancel: def.reject.bind(def),
    });
    return def;
};
eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
    var r = $editable.data('range');
    if (r.sc.tagName && r.sc.childNodes.length) {
        r.sc = r.sc.childNodes[r.so];
    }
    var media = $(r.sc).parents().addBack().filter(function (i, el) {
        return dom.isImg(el);
    })[0];
    core.bus.trigger('media_dialog_demand', {
        $editable: $editable,
        media: media,
        options: {
            lastFilters: ['background'],
            onUpload: $editable.data('callbacks').onUpload,
        },
    });
    return new $.Deferred().reject();
};
$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
    var $editable = layoutInfo.editable();
    var $selection = layoutInfo.handle().find('.note-control-selection');
    core.bus.trigger('alt_dialog_demand', {
        $editable: $editable,
        media: $selection.data('target'),
    });
};
$.summernote.pluginEvents.customColor = function (event, editor, layoutInfo, customColor) {
    var defaultColor = event.target.dataset.color;
    core.bus.trigger('color_picker_dialog_demand', {
        color: defaultColor === 'rgba(0, 0, 0, 0)' ? 'rgb(255, 0, 0)' : defaultColor,
        onSave: function (color) {
            var $palettes = $(event.currentTarget).find('.note-custom-color-palette > .note-color-row')
                .append(('<button type="button" class="note-color-btn" data-value="' + color + '" style="background-color:' + color + ';" />'));
            $palettes.filter(':odd').find('button:not([data-event])').attr('data-event', 'backColor');
            $palettes.filter(':even').find('button:not([data-event])').attr('data-event', 'foreColor');
            if (customColor === 'foreColor') {
                $(event.currentTarget).find('.note-fore-color-preview > button').css('border-bottom-color', color);
                $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, color);
            } else {
                $(event.currentTarget).find('.note-back-color-preview > button').css('border-bottom-color', color);
                $.summernote.pluginEvents.backColor(event, editor, layoutInfo, color);
            }
        }
    });
};
$.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) {
    var $editable = layoutInfo.editable();
    var $selection = layoutInfo.handle().find('.note-control-selection');
    core.bus.trigger('crop_image_dialog_demand', {
        $editable: $editable,
        media: $selection.data('target'),
    });
};

// Utils
var fn_is_void = dom.isVoid || function () {};
dom.isVoid = function (node) {
    return fn_is_void(node) || dom.isImgFont(node) || (node && node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i));
};
var fn_is_img = dom.isImg || function () {};
dom.isImg = function (node) {
    return fn_is_img(node) || dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)(media_iframe_video|o_image)(\s|$)/i)) ));
};
var fn_is_forbidden_node = dom.isForbiddenNode || function () {};
dom.isForbiddenNode = function (node) {
    if (node.tagName === "BR") {
        return false;
    }
    return fn_is_forbidden_node(node) || $(node).is(".media_iframe_video");
};
var fn_is_img_font = dom.isImgFont || function () {};
dom.isImgFont = function (node) {
    if (fn_is_img_font(node)) return true;

    var nodeName = node && node.nodeName.toUpperCase();
    var className = (node && node.className || "");
    if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
        var classNames = className.split(/\s+/);
        for (var k=0; k<base.fontIcons.length; k++) {
            if (_.intersection(base.fontIcons[k].alias, classNames).length) {
                return true;
            }
        }
    }
    return false;
};
var fn_is_font = dom.isFont; // re-overwrite font to include theme icons
dom.isFont = function (node) {
    return fn_is_font(node) || dom.isImgFont(node);
};

var fn_visible = $.summernote.pluginEvents.visible;
$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
    var res = fn_visible.apply(this, arguments);
    var rng = range.create();
    if (!rng) return res;
    var $node = $(dom.node(rng.sc));
    if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
        $node.hasClass("o_editable") &&
        !$node[0].children.length &&
        "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small pre th td span label".toUpperCase().indexOf($node[0].nodeName) === -1) {
        var p = $('<p><br/></p>')[0];
        $node.append( p );
        range.createFromNode(p.firstChild).select();
    }
    return res;
};

function prettify_html(html) {
    html = html.trim();
    var result = '',
        level = 0,
        get_space = function (level) {
            var i = level, space = '';
            while (i--) space += '  ';
            return space;
        },
        reg = /^<\/?(a|span|font|u|em|i|strong|b)(\s|>)/i,
        inline_level = Infinity,
        tokens = _.compact(_.flatten(_.map(html.split(/</), function (value) {
            value = value.replace(/\s+/g, ' ').split(/>/);
            value[0] = /\S/.test(value[0]) ? '<' + value[0] + '>' : '';
            return value;
        })));

    // reduce => merge inline style + text

    for (var i = 0, l = tokens.length; i < l; i++) {
        var token = tokens[i];
        var inline_tag = reg.test(token);
        var inline = inline_tag || inline_level <= level;

        if (token[0] === '<' && token[1] === '/') {
            if (inline_tag && inline_level === level) {
                inline_level = Infinity;
            }
            level--;
        }

        if (!inline && !/\S/.test(token)) {
            continue;
        }
        if (!inline || (token[1] !== '/' && inline_level > level)) {
            result += get_space(level);
        }

        if (token[0] === '<' && token[1] !== '/') {
            level++;
            if (inline_tag && inline_level > level) {
                inline_level = level;
            }
        }

        if (token.match(/^<(img|hr|br)/)) {
            level--;
        }

        // don't trim inline content (which could change appearance)
        if (!inline) {
            token = token.trim();
        }

        result += token.replace(/\s+/, ' ');

        if (inline_level > level) {
            result += '\n';
        }
    }
    return result;
}

/*
 * This override when clicking on the 'Code View' button has two aims:
 *
 * - have our own code view implementation for FieldTextHtml
 * - add an 'enable' paramater to call the function directly and allow us to
 *   disable (false) or enable (true) the code view mode.
 */
$.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable) {
    if (layoutInfo === undefined) {
        return;
    }
    if (layoutInfo.toolbar) {
        // if editor inline (FieldTextHtmlSimple)
        var is_activated = $.summernote.eventHandler.modules.codeview.isActivated(layoutInfo);
        if (is_activated === enable) {
            return;
        }
        return eventHandler.modules.codeview.toggle(layoutInfo);
    } else {
        // if editor iframe (FieldTextHtml)
        var $editor = layoutInfo.editor();
        var $textarea = $editor.prev('textarea');
        if ($textarea.is('textarea') === enable) {
            return;
        }

        if (!$textarea.length) {
            // init and create texarea
            var html = prettify_html($editor.prop("innerHTML"));
            $editor.parent().css({
                'position': 'absolute',
                'top': 0,
                'bottom': 0,
                'left': 0,
                'right': 0
            });
            $textarea = $('<textarea/>').css({
                'margin': '0 -4px',
                'padding': '0 4px',
                'border': 0,
                'top': '51px',
                'left': '620px',
                'width': '100%',
                'font-family': 'sans-serif',
                'font-size': '13px',
                'height': '98%',
                'white-space': 'pre',
                'word-wrap': 'normal'
            }).val(html).data('init', html);
            $editor.before($textarea);
            $editor.hide();
        } else {
            // save changes
            $editor.prop('innerHTML', $textarea.val().replace(/\s*\n\s*/g, '')).trigger('content_changed');
            $textarea.remove();
            $editor.show();
        }
    }
};

// Fix ie and re-range to don't break snippet
var last_div;
var last_div_change;
var last_editable;
var initial_data = {};
function reRangeSelectKey(event) {
    initial_data.range = null;
    if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) {
        var r = range.create();
        if (r) {
            var rng = r.reRange(event.keyCode <= 38);
            if (r !== rng) {
                rng.select();
            }
        }
    }
}
function reRangeSelect(event, dx, dy) {
    var r = range.create();
    if (!r || r.isCollapsed()) return;

    // check if the user move the caret on up or down
    var data = r.reRange(dy < 0 || (dy === 0 && dx < 0));

    if (data.sc !== r.sc || data.so !== r.so || data.ec !== r.ec || data.eo !== r.eo) {
        setTimeout(function () {
            data.select();
            $(data.sc.parentNode).closest('.note-popover');
        },0);
    }

    $(data.sc).closest('.o_editable').data('range', r);
    return r;
}
function summernote_mouseup(event) {
    if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
        return;
    }
    // don't rerange if simple click
    if (initial_data.event) {
        var dx = event.clientX - (event.shiftKey && initial_data.rect ? initial_data.rect.left : initial_data.event.clientX);
        var dy = event.clientY - (event.shiftKey && initial_data.rect ? initial_data.rect.top : initial_data.event.clientY);
        if (10 < Math.pow(dx, 2)+Math.pow(dy, 2)) {
            reRangeSelect(event, dx, dy);
        }
    }

    if (!$(event.target).closest(".o_editable").length) {
        return;
    }
    if (!initial_data.range || !event.shiftKey) {
        setTimeout(function () {
            initial_data.range = range.create();
        },0);
    }
}
var remember_selection;
function summernote_mousedown(event) {
    rte.history.splitNext();

    var $editable = $(event.target).closest(".o_editable, .note-editor");
    var r;

    if (document.documentMode) {
        summernote_ie_fix(event, function (node) { return node.tagName === "DIV" || node.tagName === "IMG" || (node.dataset && node.dataset.oeModel); });
    } else if (last_div && event.target !== last_div) {
        if (last_div.tagName === "A") {
            summernote_ie_fix(event, function (node) { return node.dataset && node.dataset.oeModel; });
        } else if ($editable.length) {
            if (summernote_ie_fix(event, function (node) { return node.tagName === "A"; })) {
                r = range.create();
                r.select();
            }
        }
    }

    // restore range if range lost after clicking on non-editable area
    try {
        r = range.create();
    } catch (e) {
        // If this code is running inside an iframe-editor and that the range
        // is outside of this iframe, this will fail as the iframe does not have
        // the permission to check the outside content this way. In that case,
        // we simply ignore the exception as it is as if there was no range.
        return;
    }
    var editables = $(".o_editable[contenteditable], .note-editable[contenteditable]");
    var r_editable = editables.has((r||{}).sc).addBack(editables.filter((r||{}).sc));
    if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
        var saved_editable = editables.has((remember_selection||{}).sc);
        if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
            remember_selection = range.create(dom.firstChild($editable[0]), 0);
        } else if (!saved_editable.length) {
            remember_selection = undefined;
        }
        if (remember_selection) {
            try {
                remember_selection.select();
            } catch (e) {
                console.warn(e);
            }
        }
    } else if (r_editable.length) {
        remember_selection = r;
    }

    initial_data.event = event;

    // keep selection when click with shift
    if (event.shiftKey && $editable.length) {
        if (initial_data.range) {
            initial_data.range.select();
        }
        var rect = r && r.getClientRects();
        initial_data.rect = rect && rect.length ? rect[0] : { top: 0, left: 0 };
    }
}

function summernote_ie_fix(event, pred) {
    var editable;
    var div;
    var node = event.target;
    while (node.parentNode) {
        if (!div && pred(node)) {
            div = node;
        }
        if (last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
            break;
        }
        if (node.className && node.className.indexOf('o_editable') !== -1) {
            if (!div) {
                div = node;
            }
            editable = node;
            break;
        }
        node = node.parentNode;
    }

    if (!editable) {
        $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");
        $(last_editable).attr("contentEditable", "true").prop("contentEditable", "true");
        last_div_change = null;
        last_editable = null;
        return;
    }

    if (div === last_div) {
        return;
    }

    last_div = div;

    $(last_div_change).removeAttr("contentEditable").removeProp("contentEditable");

    if (last_editable !== editable) {
        if ($(editable).is("[contentEditable='true']")) {
           $(editable).removeAttr("contentEditable").removeProp("contentEditable");
            last_editable = editable;
        } else {
            last_editable = null;
        }
    }
    if (!$(div).attr("contentEditable") && !$(div).is("[data-oe-type='many2one'], [data-oe-type='contact']")) {
        $(div).attr("contentEditable", "true").prop("contentEditable", "true");
        last_div_change = div;
    } else {
        last_div_change = null;
    }
    return editable !== div ? div : null;
}

var fn_attach = eventHandler.attach;
eventHandler.attach = function (oLayoutInfo, options) {
    fn_attach.call(this, oLayoutInfo, options);

    oLayoutInfo.editor().on('dragstart', 'img', function (e) { e.preventDefault(); });
    $(document).on('mousedown', summernote_mousedown).on('mouseup', summernote_mouseup);
    oLayoutInfo.editor().off('click').on('click', function (e) {e.preventDefault();}); // if the content editable is a link

    /**
     * Open Media Dialog on double click on an image/video/icon.
     * Shows a tooltip on click to say to the user he can double click.
     */
    create_dblclick_feature("img, .media_iframe_video, i.fa, span.fa, a.o_image", function () {
        eventHandler.modules.imageDialog.show(oLayoutInfo);
    });

    /**
     * Open Link Dialog on double click on a link/button.
     * Shows a tooltip on click to say to the user he can double click.
     */
    create_dblclick_feature("a[href], a.btn, button.btn", function () {
        eventHandler.modules.linkDialog.show(oLayoutInfo);
    });

    oLayoutInfo.editable().on('mousedown', function (e) {
        if (dom.isImg(e.target) && dom.isContentEditable(e.target)) {
            range.createFromNode(e.target).select();
        }
    });
    $(document).on("keyup", reRangeSelectKey);

    var clone_data = false;

    if (options.model) {
        oLayoutInfo.editable().data({'oe-model': options.model, 'oe-id': options.id});
    }
    if (options.getMediaDomain) {
        oLayoutInfo.editable().data('oe-media-domain', options.getMediaDomain);
    }

    var $node = oLayoutInfo.editor();
    if ($node.data('oe-model') || $node.data('oe-translation-id')) {
        $node.on('content_changed', function () {
            var $nodes = $('[data-oe-model], [data-oe-translation-id]')
                .filter(function () { return this !== $node[0];});

            if ($node.data('oe-model')) {
                $nodes = $nodes.filter('[data-oe-model="'+$node.data('oe-model')+'"]')
                    .filter('[data-oe-id="'+$node.data('oe-id')+'"]')
                    .filter('[data-oe-field="'+$node.data('oe-field')+'"]');
            }
            if ($node.data('oe-translation-id')) $nodes = $nodes.filter('[data-oe-translation-id="'+$node.data('oe-translation-id')+'"]');
            if ($node.data('oe-type')) $nodes = $nodes.filter('[data-oe-type="'+$node.data('oe-type')+'"]');
            if ($node.data('oe-expression')) $nodes = $nodes.filter('[data-oe-expression="'+$node.data('oe-expression')+'"]');
            if ($node.data('oe-xpath')) $nodes = $nodes.filter('[data-oe-xpath="'+$node.data('oe-xpath')+'"]');
            if ($node.data('oe-contact-options')) $nodes = $nodes.filter('[data-oe-contact-options="'+$node.data('oe-contact-options')+'"]');

            var nodes = $node.get();

            if ($node.data('oe-type') === "many2one") {
                $nodes = $nodes.add($('[data-oe-model]')
                    .filter(function () { return this !== $node[0] && nodes.indexOf(this) === -1; })
                    .filter('[data-oe-many2one-model="'+$node.data('oe-many2one-model')+'"]')
                    .filter('[data-oe-many2one-id="'+$node.data('oe-many2one-id')+'"]')
                    .filter('[data-oe-type="many2one"]'));

                $nodes = $nodes.add($('[data-oe-model]')
                    .filter(function () { return this !== $node[0] && nodes.indexOf(this) === -1; })
                    .filter('[data-oe-model="'+$node.data('oe-many2one-model')+'"]')
                    .filter('[data-oe-id="'+$node.data('oe-many2one-id')+'"]')
                    .filter('[data-oe-field="name"]'));
            }

            if (!clone_data) {
                clone_data = true;
                $nodes.html(this.innerHTML);
                clone_data = false;
            }
        });
    }

    var custom_toolbar = oLayoutInfo.toolbar ? oLayoutInfo.toolbar() : undefined;
    var $toolbar = $(oLayoutInfo.popover()).add(custom_toolbar);
    $('button[data-event="undo"], button[data-event="redo"]', $toolbar).attr('disabled', true);

    $(oLayoutInfo.editor())
        .add(oLayoutInfo.handle())
        .add(oLayoutInfo.popover())
        .add(custom_toolbar)
        .on('click content_changed', function () {
            $('button[data-event="undo"]', $toolbar).attr('disabled', !oLayoutInfo.editable().data('NoteHistory').hasUndo());
            $('button[data-event="redo"]', $toolbar).attr('disabled', !oLayoutInfo.editable().data('NoteHistory').hasRedo());
        });

    function create_dblclick_feature(selector, callback) {
        var show_tooltip = true;

        oLayoutInfo.editor().on("dblclick", selector, function (e) {
            var $target = $(e.target);
            if (!dom.isContentEditable($target)) {
                // Prevent edition of non editable parts
                return;
            }

            show_tooltip = false;
            callback();
            e.stopImmediatePropagation();
        });

        oLayoutInfo.editor().on("click", selector, function (e) {
            var $target = $(e.target);
            if (!dom.isContentEditable($target)) {
                // Prevent edition of non editable parts
                return;
            }

            show_tooltip = true;
            setTimeout(function () {
                if (!show_tooltip) return;
                $target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show');
                setTimeout(function () {
                    $target.tooltip('dispose');
                }, 800);
            }, 400);
        });
    }
};
var fn_detach = eventHandler.detach;
eventHandler.detach = function (oLayoutInfo, options) {
    fn_detach.call(this, oLayoutInfo, options);
    oLayoutInfo.editable().off('mousedown');
    oLayoutInfo.editor().off("dragstart");
    oLayoutInfo.editor().off('click');
    $(document).off('mousedown', summernote_mousedown);
    $(document).off('mouseup', summernote_mouseup);
    oLayoutInfo.editor().off("dblclick");
    $(document).off("keyup", reRangeSelectKey);
};

// Translation for odoo
$.summernote.lang.odoo = {
    font: {
      bold: _t('Bold'),
      italic: _t('Italic'),
      underline: _t('Underline'),
      strikethrough: _t('Strikethrough'),
      subscript: _t('Subscript'),
      superscript: _t('Superscript'),
      clear: _t('Remove Font Style'),
      height: _t('Line Height'),
      name: _t('Font Family'),
      size: _t('Font Size')
    },
    image: {
      image: _t('File / Image'),
      insert: _t('Insert Image'),
      resizeFull: _t('Resize Full'),
      resizeHalf: _t('Resize Half'),
      resizeQuarter: _t('Resize Quarter'),
      floatLeft: _t('Float Left'),
      floatRight: _t('Float Right'),
      floatNone: _t('Float None'),
      dragImageHere: _t('Drag an image here'),
      selectFromFiles: _t('Select from files'),
      url: _t('Image URL'),
      remove: _t('Remove Image')
    },
    link: {
      link: _t('Link'),
      insert: _t('Insert Link'),
      unlink: _t('Unlink'),
      edit: _t('Edit'),
      textToDisplay: _t('Text to display'),
      url: _t('To what URL should this link go?'),
      openInNewWindow: _t('Open in new window')
    },
    video: {
      video: _t('Video'),
      videoLink: _t('Video Link'),
      insert: _t('Insert Video'),
      url: _t('Video URL?'),
      providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
    },
    table: {
      table: _t('Table')
    },
    hr: {
      insert: _t('Insert Horizontal Rule')
    },
    style: {
      style: _t('Style'),
      normal: _t('Normal'),
      blockquote: _t('Quote'),
      pre: _t('Code'),
      small: _t('Small'),
      h1: _t('Header 1'),
      h2: _t('Header 2'),
      h3: _t('Header 3'),
      h4: _t('Header 4'),
      h5: _t('Header 5'),
      h6: _t('Header 6')
    },
    lists: {
      unordered: _t('Unordered list'),
      ordered: _t('Ordered list')
    },
    options: {
      help: _t('Help'),
      fullscreen: _t('Full Screen'),
      codeview: _t('Code View')
    },
    paragraph: {
      paragraph: _t('Paragraph'),
      outdent: _t('Outdent'),
      indent: _t('Indent'),
      left: _t('Align left'),
      center: _t('Align center'),
      right: _t('Align right'),
      justify: _t('Justify full')
    },
    color: {
      custom: _t('Custom Color'),
      background: _t('Background Color'),
      foreground: _t('Font Color'),
      transparent: _t('Transparent'),
      setTransparent: _t('None'),
    },
    shortcut: {
      shortcuts: _t('Keyboard shortcuts'),
      close: _t('Close'),
      textFormatting: _t('Text formatting'),
      action: _t('Action'),
      paragraphFormatting: _t('Paragraph formatting'),
      documentStyle: _t('Document Style')
    },
    history: {
      undo: _t('Undo'),
      redo: _t('Redo')
    }
};

//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::

/**
 * @todo get rid of this. This has been implemented as a fix to be able to
 * instantiate media, link and alt dialogs outside the main editor: in the
 * simple HTML fields and forum textarea.
 */
var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
    /**
     * @constructor
     */
    init: function (parent) {
        mixins.EventDispatcherMixin.init.call(this);
        this.setParent(parent);

        core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
        core.bus.on('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
        core.bus.on('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
        core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
        core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
    },
    /**
     * @override
     */
    destroy: function () {
        mixins.EventDispatcherMixin.destroy.call(this);

        core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
        core.bus.off('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
        core.bus.off('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
        core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
        core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Called when a demand to open a alt dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onAltDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;
        var altDialog = new weWidgets.AltDialog(this,
            data.options || {},
            data.$editable,
            data.media
        );
        if (data.onSave) {
            altDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            altDialog.on('cancel', this, data.onCancel);
        }
        altDialog.open();
    },
/**
     * Called when a demand to open a color picker dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onColorPickerDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;
        var colorpicker = new ColorpickerDialog(this, {
            defaultColor: data.color,
        });
        if (data.onSave) {
            colorpicker.on('colorpicker:saved', this, function (ev) {
                data.onSave(ev.data.cssColor);
            });
        }
        colorpicker.open();
    },
    /**
     * Called when a demand to open a crop dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onCropImageDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;
        var cropImageDialog = new weWidgets.CropImageDialog(this,
            _.extend({
                res_model: data.$editable.data('oe-model'),
                res_id: data.$editable.data('oe-id'),
            }, data.options || {}),
            data.$editable,
            data.media
        );
        if (data.onSave) {
            cropImageDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            cropImageDialog.on('cancel', this, data.onCancel);
        }
        cropImageDialog.open();
    },
    /**
     * Called when a demand to open a link dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onLinkDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;
        var linkDialog = new weWidgets.LinkDialog(this,
            data.options || {},
            data.$editable,
            data.linkInfo
        );
        if (data.onSave) {
            linkDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            linkDialog.on('cancel', this, data.onCancel);
        }
        linkDialog.open();
    },
    /**
     * Called when a demand to open a media dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onMediaDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;

        var mediaDialog = new weWidgets.MediaDialog(this,
            _.extend({
                res_model: data.$editable.data('oe-model'),
                res_id: data.$editable.data('oe-id'),
                domain: data.$editable.data('oe-media-domain'),
            }, data.options),
            data.$editable,
            data.media
        );
        if (data.onSave) {
            mediaDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            mediaDialog.on('cancel', this, data.onCancel);
        }
        mediaDialog.open();
    },
});
/**
 * @todo cannot do this without include because it would make a loop in the
 * JS module dependencies otherwise.
 */
rte.Class.include({
    /**
     * @override
     */
    start: function () {
        this._summernoteManager = new SummernoteManager(this);
        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    cancel: function () {
        this._super.apply(this, arguments);
        this._summernoteManager.destroy();
    },
});
return SummernoteManager;
});
Example #15
0
 willStart: function() {
     var self = this;
     return $.when(ajax.loadLibs(this), this._super()).then(function() {
         return self.fetch_data();
     });
 },
Example #16
0
 fetch_subtypes: function (follower_id) {
     return ajax.jsonRpc('/mail/read_subscription_data', 'call', {
         res_model: this.view.model,
         follower_id: follower_id,
     });
 },
odoo.define('theme_common.s_media_block_frontend', function (require) {
    'use strict';

    var ajax = require('web.ajax');
    var core = require('web.core');
    var s_animation = require('web_editor.snippets.animation');

    var QWeb = core.qweb;

    ajax.loadXML('/theme_common/static/src/xml/s_media_block_modal.xml', core.qweb);

    // MEDIABLOCK EDITOR
    s_animation.registry.s_media_block = s_animation.Class.extend({
        selector : "section.s_media_block",

        start: function () {
            this.start_video();
        },

        stop: function () {
            this.stop_video();
        },

        update_video_type: function () {
            var regex_url = '((?:https?:)?//([^\\s\'"<>\\\\/]+)/([^\\s\'"<>\\\\]+))';
            var match = this.src.match(new RegExp('\\ssrc=[\'"]?' + regex_url));
            match = match || this.src.match(new RegExp('^\\s*' + regex_url));
            if (!match) {
                this.video_type = "image";
                this.src = "";
                return;
            }

            var url = match[1];
            var domain = match[2];
            var path = match[3];

            match = undefined;

            var services_prefix = {
                youtube: 'https://youtu.be/',
                vimeo: 'https://vimeo.com/',
                dailymotion: 'http://dai.ly/',
            };

            if (/\.youtube(-nocookie)?\./.test(domain)) {
                this.video_type = 'youtube';
                match = path.match(/^(?:embed\/|watch\?v=)?([^\/?&#]+)/i);
            } else if (domain == "youtu.be") {
                this.video_type = 'youtube';
                match = path.match(/^([^\/?&#]+)/);
            } else if (_.str.include(domain, "vimeo.")) {
                this.video_type = 'vimeo';
                match = path.match(/^(?:video\/)?([^?&#]+)/i);
            } else if (_.str.include(domain, ".dailymotion.")) {
                this.video_type = "dailymotion";
                match = path.match(/(?:embed\/)?(?:video\/)?([^\/?&#_]+)/i);
            } else if (domain == "dai.ly") {
                this.video_type = "dailymotion";
                match = path.match(/^([^\/?&#]+)/);
            }

            if (match) {
                this.src = services_prefix[this.video_type] + match[1];
            } else if (!/\ssrc=/.test(this.src)) {
                this.src = url;
                this.video_type = 'html5';
            } else {
                this.video_type = 'other';
            }
        },

        start_video: function () {
            var self = this;
            var src = self.$target.attr('src');
            var video_url;

            // mobile phones are not supported, just show the background image.
            if (jQuery.browser.mobile) { return; };

            if (!self.video_type || self.src !== self.$target.attr('src')) {
                self.src = self.$target.attr('src');
                if (!self.src) {
                    return;
                }
                self.update_video_type();
            }

            var params = _.chain(['muted', 'loop', 'autoplay', 'controls']).map(function (attribute) {
                var value = self.$target.attr(attribute);
                return [attribute, value ? 1 : value];
            }).object().value();

            var video_id = self.src.split('/')[3];

            switch(self.video_type) {
                case 'html5':
                    video_url = self.src;
                    break;
                case 'youtube':
                    video_url = "https://www.youtube.com/embed/" + video_id;
                    if ('muted' in params) params['mute'] = 1;
                    if ('loop' in params) params['playlist'] = video_id;
                    _.extend(params, {iv_load_policy: 3, rel: 0, showinfo: 0});
                    break;
                case 'vimeo':
                    video_url = "https://player.vimeo.com/video/" + video_id;
                    _.extend(params, {color: 'fff', title: 0, byline: 0, portrait: 0, badge: 0});
                    break;
                case 'dailymotion':
                    video_url = "https://www.dailymotion.com/embed/video/" + video_id;
                    if (!params["controls"]) params["chromeless"] = 1;
                    _.extend(params, {logo: 0, info: 0, related: 0, wmode: 'opaque'});
                    break;
            }

            var whenPlayerReady = (self['create_' + self.video_type + '_video'] || self['create_video']).call(self, self.$target, video_url, params);

            whenPlayerReady.then(function ($player) {
                $player.fadeTo(0, 0);
                $player.addClass('o_player').parent().addClass('o_player_wrapper');
                self.ratio = ($player.width() / $player.height());

                if (self.$target.attr('iframefit') == 'fitCont') {
                    $player.parentsUntil(self.$target).css({width: '100%', height: 'auto'});
                    if ($player.is('iframe')) {
                        $player.css('width', '100%');
                        $player.height($player.width() / self.ratio);
                    } else {
                        $player.css({width: '100%', height: 'auto'});
                    }
                } else if (self.$target.attr('iframefit') == 'fitIframe') {
                    $player.parentsUntil(self.$target).andSelf().css({width: '100%', height: '100%'});
                }

                $player.fadeTo("slow", 1);
                $player.removeClass('o_media_loading');
            });
        },

        stop_video: function () {
            this.$target.find('.videoBox, iframe').remove();
            $(window).unbind('resize.s_media_block');
        },

        create_video: function ($container, video_url, params) {
            var def = $.Deferred();
            var $iframe;

            if (video_url) {
                $iframe = $('<iframe/>', {
                    frameborder: "0",
                    allowfullscreen: "allowfullscreen",
                    src: video_url + '?' + $.param(params),
                });
            } else {
                $iframe = $('<div/>').html(this.src).find('iframe:first').css({
                    height: "100%", width: "100%", top: 0, position: "absolute",
                });
                if ($iframe.length) {
                    $container.css('max-height', '');
                } else {
                    return def.reject();
                }
            }

            $iframe.addClass('o_media_loading');
            $iframe.on('load', function () {
                def.resolve($iframe);
            });
            $container.append($iframe);

            return def;
        },

        create_html5_video: function ($container, video_url, params) {
            var def = $.Deferred();
            var $video_content = $('<video/>', {'class': 'videoBox hidden-xs', 'preload': 'metadata'}).attr(params).attr('src', video_url);

            $video_content.addClass('o_media_loading');
            $video_content.on('canplay', function () {
                def.resolve($video_content);
            });
            $container.append($video_content);

            $video_content.get(0).load();
            if (params['autoplay']) {
                $video_content.get(0).play();
            }

            return def;
        },

        // When the visitor is logged in YouTube, the console return "Failed to execute 'postMessage'";
        // That is a known error from the side of the Youtube API.
        // Fortunately it's not blocking but unfortunatly the fix should be done by the Google team.
        YTPlayer_video: function ($container, video_url, _params) {
            var video_id = this.src.split('/')[3];
            var params = _.mapObject(_params, function (v) { return !!v; });

            var opacity      = this.$target.attr('opacity');
            var background   = this.$target.attr('background') || '';

            var timeStamp = Date.now();

            var $video_container = $('<div/>', {
                'class': 'yt_video_container '+ background,
                id: "s_media_block_" + timeStamp,
            });
            var playerParams = {
                videoURL: video_id, containment: '#s_media_block_' + timeStamp, mute: params['muted'], loop: params['loop'],
                stopMovieOnBlur: false, autoPlay: true, showYTLogo: false, opacity: opacity, showControls: false,
            };
            var $el = $('<div/>', {'class': 'player', 'data-property': JSON.stringify(playerParams)});
            var $loader = $("<span class='yt-loader'><span/></span>");
            $video_container.append($el).append($loader);

            var interval = null;
            if ($("#oe_main_menu_navbar").length > 0) { $loader.css("top", $("#oe_main_menu_navbar").outerHeight()+1); }
            $loader.animate({width: "45%"}, 800, function () {
                var el = $loader;
                interval = setInterval(function () { timer(); }, 300);
                function timer() { var w =  el.width(); el.width(w + 5); }
            });

            if (!params['autoplay']) {
                $el.one('YTPStart', function () {
                    $el.YTPPause();
                });
            }

            var def = $.Deferred();
            $el.on('YTPReady', function () {
                clearInterval(interval);
                $loader.css("width", "100%").fadeOut(500);

                def.resolve($video_container.find('iframe'));

                if (!params['controls'] && params['autoplay']) {
                    return;
                }

                var $controls = $("<span/>", {'class': 'controls'}).appendTo($video_container);

                var $btnplay = $("<span/>", {'class': 'fa fa-fw'}).appendTo($controls);
                var playing = params['autoplay'];
                $btnplay.toggleClass("fa-pause", playing).toggleClass("fa-play", !playing);
                $btnplay.on("click", play_callback);
                if(!params['controls']) {
                    $btnplay.one('click', function () {
                        $controls.remove();
                    });
                }

                if (!params['muted'] && params['controls']) {
                    var $btnMute = $("<span/>", {'class': 'fa fa-fw fa-volume-up'}).appendTo($controls);
                    $btnMute.on("click", mute_callback);
                }

                function play_callback() {
                    if(playing) {
                        $el.YTPPause();
                    } else {
                        $el.YTPPlay();
                    }
                    playing = !playing;

                    $btnplay.toggleClass("fa-pause", playing).toggleClass("fa-play", !playing);
                }
                function mute_callback() {
                    $el.YTPToggleVolume();
                    $btnMute.toggleClass("fa-volume-up").toggleClass("fa-volume-off");
                }
            });

            $container.append($video_container);
            $el.YTPlayer();

            return def;
        },
    });
});
Example #18
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 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 $link = $('.js_language_selector a[data-default-lang]');
        if (!$link.length) {
            // Fallback for old website
            var l = false;
            _.each($('.js_language_selector a'), function(a) {
               if (!l || a.href.length < l.href.length) { l = a; }
            });
            $link = $(l);
        }
        $link[0].search += ($link[0].search ? '&' : '?') + 'enable_editor=1';
        $link.click();
    },
});


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 (!localStorage[nodialog]) {
            var dialog = new TranslatorDialog();
            dialog.appendTo($(document.body));
            dialog.on('activate', this, function () {
                if (dialog.$('input[name=do_not_show]').prop('checked')) {
                    localStorage.removeItem(nodialog);
                } else {
                    localStorage.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();
    },
});

});
Example #19
0
        send: function (e) {
            e.preventDefault();  // Prevent the default submit behavior
            this.$target.find('.o_website_form_send').off();  // Prevent users from crazy clicking

            var self = this;

            self.$target.find('#o_website_form_result').empty();
            if (!self.check_error_fields({})) {
                self.update_status('invalid');
                return false;
            }

            // Prepare form inputs
            this.form_fields = this.$target.serializeArray();
            _.each(this.$target.find('input[type=file]'), function (input) {
                $.each($(input).prop('files'), function (index, file) {
                    // Index field name as ajax won't accept arrays of files
                    // when aggregating multiple files into a single field value
                    self.form_fields.push({
                        name: input.name + '[' + index + ']',
                        value: file
                    });
                });
            });

            // Serialize form inputs into a single object
            // Aggregate multiple values into arrays
            var form_values = {};
            _.each(this.form_fields, function (input) {
                if (input.name in form_values) {
                    // If a value already exists for this field,
                    // we are facing a x2many field, so we store
                    // the values in an array.
                    if (Array.isArray(form_values[input.name])) {
                        form_values[input.name].push(input.value);
                    } else {
                        form_values[input.name] = [form_values[input.name], input.value];
                    }
                } else {
                    if (input.value !== '') {
                        form_values[input.name] = input.value;
                    }
                }
            });

            // Overwrite form_values array with values from the form tag
            // Necessary to handle field values generated server-side, since
            // using t-att- inside a snippet makes it non-editable !
            for (var key in this.$target.data()) {
                if (_.str.startsWith(key, 'form_field_')){
                    form_values[key.replace('form_field_', '')] = this.$target.data(key);
                }
            }

            // Post form and handle result
            ajax.post(this.$target.attr('action') + (this.$target.data('force_action')||this.$target.data('model_name')), form_values)
            .then(function (result_data) {
                result_data = $.parseJSON(result_data);
                if (!result_data.id) {
                    // Failure, the server didn't return the created record ID
                    self.update_status('error');
                    if (result_data.error_fields) {
                        // If the server return a list of bad fields, show these fields for users
                        self.check_error_fields(result_data.error_fields);
                    }
                } else {
                    // Success, redirect or update status
                    var success_page = self.$target.attr('data-success_page');
                    if (success_page) {
                        $(window.location).attr('href', success_page);
                    }
                    else {
                        self.update_status('success');
                    }

                    // Reset the form
                    self.$target[0].reset();
                }
            })
            .fail(function (result_data){
                self.update_status('error');
            });
        },
Example #20
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 mixins = require('web.mixins');
var Model = require('web.Model');
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-primary',
                description: "This keyword is used in the page title",
            } : htmlPage.isInDescription(keyword) ? {
                title: 'label label-info',
                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);
            });
            keyword.on('selected', self, function (word, language) {
                self.trigger('selected', word, language);
            });
            keyword.appendTo(self.$el);
        }
        if (self.isFull()) {
            self.trigger('list-full');
        }
    },
});

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 = Widget.extend({
    template: 'website.seo_configuration',
    events: {
        'keyup input[name=seo_page_keywords]': 'confirmKeyword',
        'keyup input[name=seo_page_title]': 'titleChanged',
        'keyup textarea[name=seo_page_description]': 'descriptionChanged',
        'click button[data-action=add]': 'addKeyword',
        'click button[data-action=update]': 'update',
        'hidden.bs.modal': 'destroy',
    },
    canEditTitle: false,
    canEditDescription: false,
    canEditKeywords: false,
    canEditLanguage: false,
    maxTitleSize: 65,
    maxDescriptionSize: 160,  // TODO master: remove me and add warning
    start: function () {
        var self = this;
        var $modal = self.$el;
        var htmlPage = this.htmlPage = new HtmlPage();
        $modal.find('.js_seo_page_url').text(htmlPage.url());
        $modal.find('input[name=seo_page_title]').val(htmlPage.title());
        $modal.find('textarea[name=seo_page_description]').val(htmlPage.description());
        // self.suggestImprovements();
        // self.imageList = new ImageList(self, { page: htmlPage });
        // if (htmlPage.images().length === 0) {
        //     $modal.find('.js_image_section').remove();
        // } else {
        //     self.imageList.appendTo($modal.find('.js_seo_image_list'));
        // }
        self.keywordList = new KeywordList(self, { page: htmlPage });
        self.keywordList.on('list-full', self, function () {
            $modal.find('input[name=seo_page_keywords]')
                .attr('readonly', "readonly")
                .attr('placeholder', "Remove a keyword first");
            $modal.find('button[data-action=add]')
                .prop('disabled', true).addClass('disabled');
        });
        self.keywordList.on('list-not-full', self, function () {
            $modal.find('input[name=seo_page_keywords]')
                .removeAttr('readonly').attr('placeholder', "");
            $modal.find('button[data-action=add]')
                .prop('disabled', false).removeClass('disabled');
        });
        self.keywordList.on('selected', self, function (word, language) {
            self.keywordList.add(word, language);
        });
        self.keywordList.appendTo($modal.find('.js_seo_keywords_list'));
        self.disableUnsavableFields();
        self.renderPreview();
        $modal.modal();
        self.getLanguages();
    },
    getLanguages: function(){
        var self = this;
        ajax.jsonRpc('/web/dataset/call_kw', 'call', {
            model: 'website',
            method: 'get_languages',
            args: [],
            kwargs: {
                ids: [base.get_context().website_id],
                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;
        var $modal = self.$el;
        self.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) {
                $modal.find('input[name=seo_page_title]').attr('disabled', true);
            }
            if (!self.canEditDescription) {
                $modal.find('textarea[name=seo_page_description]').attr('disabled', true);
            }
            if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
                $modal.find('button[data-action=update]').attr('disabled', true);
            }
        });
    },
    suggestImprovements: function () {
        var tips = [];
        var self = this;
        function displayTip(message, type) {
            new Tip(self, {
               message: message,
               type: type,
            }).appendTo(self.$('.js_seo_tips'));
        }
        var htmlPage = this.htmlPage;

        // Add message suggestions at the top of the dialog
        // if necessary....
        // if (htmlPage.headers('h1').length === 0) {
        //     tips.push({
        //         type: 'warning',
        //         message: "This page seems to be missing a title.",
        //     });
        // }

        if (tips.length > 0) {
            _.each(tips, function (tip) {
                displayTip(tip.message, tip.type);
            });
        }
    },
    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("");
    },
    update: function () {
        var self = this;
        var data = {};
        if (self.canEditTitle) {
            data.website_meta_title = self.htmlPage.title();
        }
        if (self.canEditDescription) {
            data.website_meta_description = self.htmlPage.description();
        }
        if (self.canEditKeywords) {
            data.website_meta_keywords = self.keywordList.keywords().join(", ");
        }
        self.saveMetaData(data).then(function () {
           self.$el.modal('hide');
        });
    },
    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 self = this;
        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'];
            var model = new Model(obj.model).call('read', [[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 new Model(obj.model).call('write', [[obj.id], data, base.get_context()]);
        }
    },
    titleChanged: function () {
        var self = this;
        setTimeout(function () {
            var title = self.$('input[name=seo_page_title]').val();
            self.htmlPage.changeTitle(title);
            self.renderPreview();
        }, 0);
    },
    descriptionChanged: function () {
        var self = this;
        setTimeout(function () {
            var description = self.$('textarea[name=seo_page_description]').val();
            self.htmlPage.changeDescription(description);
            self.renderPreview();
        }, 0);
    },
    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();
    },
});

website.TopBar.include({
    start: function () {
        this.$el.on('click', 'a[data-action=promote-current-page]', function() {
            new Configurator(this).appendTo($(document.body));
        });
        return this._super();
    }
});

return {
    Configurator: Configurator,
};

});
Example #21
0
 }).then(function () {
     ajax.jsonRpc('/website/post_gengo_jobs', 'call', {});
     self._save();
 }).fail(function () {
Example #22
0
/**
 * Client action to refresh the session context (making sure
 * HTTP requests will have the right one) then reload the
 * whole interface.
 */
function ReloadContext (parent, action) {
    // side-effect of get_session_info is to refresh the session context
    ajax.rpc("/web/session/get_session_info", {}).then(function() {
        Reload(parent, action);
    });
}
Example #23
0
 _callServer: function () {
     // Make a call to the database to avoid the auto close of the session
     return ajax.rpc("/hr_attendance/kiosk_keepalive", {});
 },
Example #24
0
 var wait_server = function() {
     ajax.rpc("/web/webclient/version_info", {}).done(load).fail(function() {
         setTimeout(wait_server, 250);
     });
 };
Example #25
0
 this.loadJS_def = ajax.loadJS('/web/static/lib/ace/ace.odoo-custom.js').then(function () {
     return $.when(ajax.loadJS('/web/static/lib/ace/mode-xml.js'),
         ajax.loadJS('/web/static/lib/ace/theme-monokai.js'));
 });
Example #26
0
odoo.define('website_slides.upload', function (require) {
"use strict";

var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
require('web.dom_ready');
var weContext = require("web_editor.context");
var slides = require('website_slides.slides');

var qweb = core.qweb;
var _t = core._t;

if (!$('.oe_slide_js_upload').length) {
    return $.Deferred().reject("DOM doesn't contain '.oe_slide_js_upload'");
}

ajax.loadXML('/website_slides/static/src/xml/website_slides.xml', qweb);

var SlideDialog = Widget.extend({
    template: 'website.slide.upload',
    events: {
        'hidden.bs.modal': 'destroy',
        'click button.save': 'save',
        'click button[data-dismiss="modal"]': 'cancel',
        'change input#upload': 'slide_upload',
        'change input#url': 'slide_url',
        'click .list-group-item': function (ev) {
            this.$('.list-group-item').removeClass('active');
            $(ev.target).closest('li').addClass('active');
        }
    },
    init: function (el, channel_id) {
        this._super(el, channel_id);
        this.channel_id = parseInt(channel_id, 10);
        this.file = {};
        this.index_content = "";
    },
    start: function () {
        this.$el.modal({
            backdrop: 'static'
        });
        this.set_category_id();
        this.set_tag_ids();
    },
    slide_url: function (ev) {
        var self = this,
            value = {
                'url': $(ev.target).val(),
                'channel_id': self.channel_id
            };
        this.$('.alert-warning').remove();
        this.is_valid_url = false;
        this.$('.save').button('loading');
        ajax.jsonRpc('/slides/dialog_preview/', 'call', value).then(function (data) {
            self.$('.save').button('reset');
            if (data.error) {
                self.display_alert(data.error);
            } else {
                self.$("#slide-image").attr("src", data.url_src);
                self.$('#name').val(data.title);
                self.$('#description').val(data.description);
                self.is_valid_url = true;
            }
        });
    },
    check_unique_slide: function (file_name) {
        var self = this;
        return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
            model: 'slide.slide',
            method: 'search_count',
            args: [[['channel_id', '=', self.channel_id], ['name', '=', file_name]]],
            kwargs: {}
        });
    },
    slide_upload: function (ev) {
        var self = this,
            file = ev.target.files[0],
            is_image = /^image\/.*/.test(file.type),
            loaded = false;
        this.file.name = file.name;
        this.file.type = file.type;
        if (!(is_image || this.file.type === 'application/pdf')) {
            this.display_alert(_t("Invalid file type. Please select pdf or image file"));
            this.reset_file();
            return;
        }
        if (file.size / 1024 / 1024 > 25) {
            this.display_alert(_t("File is too big. File size cannot exceed 25MB"));
            this.reset_file();
            return;
        }
        this.$('.alert-warning').remove();
        var BinaryReader = new FileReader();
        // file read as DataURL
        BinaryReader.readAsDataURL(file);
        BinaryReader.onloadend = function (upload) {
            var buffer = upload.target.result;
            if (is_image) {
                self.$("#slide-image").attr("src", buffer);
            }
            buffer = buffer.split(',')[1];
            self.file.data = buffer;
        };

        if (file.type === 'application/pdf') {
            var ArrayReader = new FileReader();
            this.$('.save').button('loading');
            // file read as ArrayBuffer for PDFJS get_Document API
            ArrayReader.readAsArrayBuffer(file);
            ArrayReader.onload = function (evt) {
                var buffer = evt.target.result;
                var passwordNeeded = function () {
                    self.display_alert(_t("You can not upload password protected file."));
                    self.reset_file();
                    self.$('.save').button('reset');
                };
                PDFJS.getDocument(new Uint8Array(buffer), null, passwordNeeded).then(function getPdf(pdf) {
                    pdf.getPage(1).then(function getFirstPage(page) {
                        var scale = 1;
                        var viewport = page.getViewport(scale);
                        var canvas = document.getElementById('data_canvas');
                        var context = canvas.getContext('2d');
                        canvas.height = viewport.height;
                        canvas.width = viewport.width;
                        // Render PDF page into canvas context
                        page.render({
                            canvasContext: context,
                            viewport: viewport
                        }).then(function () {
                            var image_data = self.$('#data_canvas')[0].toDataURL();
                            self.$("#slide-image").attr("src", image_data);
                            if (loaded) {
                                self.$('.save').button('reset');
                            }
                            loaded = true;

                        });
                    });
                    var maxPages = pdf.pdfInfo.numPages;
                    var page, j;
                    self.index_content = "";
                    for (j = 1; j <= maxPages; j += 1) {
                        page = pdf.getPage(j);
                        page.then(function (page_obj) {
                            var page_number = page_obj.pageIndex + 1;
                            page_obj.getTextContent().then(function (data) {
                                var page_content = '';
                                _.each(data.items, function (obj) {
                                    page_content = page_content + obj.str + " ";
                                });
                                // page_content may contain null characters
                                page_content = page_content.replace(/\0/g, "");
                                self.index_content = self.index_content + page_number + ". " + page_content + '\n';
                                if (maxPages === page_number) {
                                    if (loaded) {
                                        self.$('.save').button('reset');
                                    }
                                    loaded = true;
                                }
                            });
                        });
                    }
                });
            };
        }

        var input = file.name;
        var input_val = input.substr(0, input.lastIndexOf('.')) || input;
        this.check_unique_slide(input_val).then(function (exist) {
            if (exist) {
                var message = _t("Channel contains the given title, please change before Save or Publish.");
                self.display_alert(message);
            }
            self.$('#name').val(input_val);
        });
    },
    reset_file: function () {
        var control = this.$('#upload');
        control.replaceWith(control = control.clone(true));
        this.file.name = false;
    },
    display_alert: function (message) {
        this.$('.alert-warning').remove();
        $('<div class="alert alert-warning" role="alert">' + message + '</div>').insertBefore(this.$('form'));
    },

    /**
        Wrapper for select2 load data from server at once and store it.

        @param {String} Placeholder for element.
        @param {bool}  true for multiple selection box, false for single selection
        @param {Function} Function to fetch data from remote location should return $.deferred object
        resolved data should be array of object with id and name. eg. [{'id': id, 'name': 'text'}, ...]
        @returns {Object} select2 wrapper object
    */
    select2_wrapper: function (tag, multi, fetch_fnc) {
        return {
            width: '100%',
            placeholder: tag,
            allowClear: true,
            formatNoMatches: false,
            multiple: multi,
            selection_data: false,
            fetch_rpc_fnc : fetch_fnc,
            formatSelection: function (data) {
                if (data.tag) {
                    data.text = data.tag;
                }
                return data.text;
            },
            createSearchChoice: function (term, data) {
                var added_tags = $(this.opts.element).select2('data');
                if (_.filter(_.union(added_tags, data), function (tag) {
                    return tag.text.toLowerCase().localeCompare(term.toLowerCase()) === 0;
                }).length === 0) {
                    return {
                        id: _.uniqueId('tag_'),
                        create: true,
                        tag: term,
                        text: _.str.sprintf(_t("Create new tag '%s'"), term),
                    };
                }
            },
            fill_data: function (query, data) {
                var that = this,
                    tags = {results: []};
                _.each(data, function (obj) {
                    if (that.matcher(query.term, obj.name)) {
                        tags.results.push({id: obj.id, text: obj.name });
                    }
                });
                query.callback(tags);
            },
            query: function (query) {
                var that = this;
                // fetch data only once and store it
                if (!this.selection_data) {
                    this.fetch_rpc_fnc().then(function (data) {
                        that.fill_data(query, data);
                        that.selection_data = data;
                    });
                } else {
                    this.fill_data(query, this.selection_data);
                }
            }
        };
    },
    // Category management from select2
    set_category_id: function () {
        var self =  this;
        $('#category_id').select2(this.select2_wrapper(_t('Category'), false,
            function () {
                return ajax.jsonRpc("/web/dataset/call_kw", 'call', {
                    model: 'slide.category',
                    method: 'search_read',
                    args: [],
                    kwargs: {
                        fields: ['name'],
                        domain: [['channel_id', '=', self.channel_id]],
                        context: weContext.get()
                    }
                });
            }));
    },
    get_category_id: function () {
        var value = $('#category_id').select2('data');
        if (value && value.create) {
            return [0, {'name': value.text}];
        }
        return [value ? value.id : null];
    },
    // Tags management from select2
    set_tag_ids: function () {
        $('#tag_ids').select2(this.select2_wrapper(_t('Tags'), true, function () {
            return ajax.jsonRpc("/web/dataset/call_kw", 'call', {
                model: 'slide.tag',
                method: 'search_read',
                args: [],
                kwargs: {
                    fields: ['name'],
                    context: weContext.get()
                }
            });
        }));
    },
    get_tag_ids: function () {
        var res = [];
        _.each($('#tag_ids').select2('data'),
            function (val) {
                if (val.create) {
                    res.push([0, 0, {'name': val.text}]);
                } else {
                    res.push([4, val.id]);
                }
            });
        return res;
    },
    //Python PIL does not support SVG, so converting SVG to PNG
    svg_to_png: function () {
        var img = this.$el.find("img#slide-image")[0];
        var canvas = document.createElement("canvas");
        canvas.width = img.width;
        canvas.height = img.height;
        canvas.getContext("2d").drawImage(img, 0, 0);
        return canvas.toDataURL("image/png").split(',')[1];
    },
    // Values and save
    get_value: function () {
        var canvas = this.$('#data_canvas')[0],
            values = {
                'channel_id': this.channel_id || '',
                'name': this.$('#name').val(),
                'url': this.$('#url').val(),
                'description': this.$('#description').val(),
                'tag_ids': this.get_tag_ids(),
                'category_id': this.get_category_id()
            };
        if (this.file.type === 'application/pdf') {
            _.extend(values, {
                'image': canvas.toDataURL().split(',')[1],
                'index_content': this.index_content,
                'slide_type': canvas.height > canvas.width ? 'document' : 'presentation',
                'mime_type': this.file.type,
                'datas': this.file.data
            });
        }
        if (/^image\/.*/.test(this.file.type)) {
            _.extend(values, {
                'slide_type': 'infographic',
                'mime_type': this.file.type === 'image/svg+xml' ? 'image/png' : this.file.type,
                'datas': this.file.type === 'image/svg+xml' ? this.svg_to_png() : this.file.data
            });
        }
        return values;
    },
    validate: function () {
        this.$('.form-group').removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
        if (!this.$('#name').val()) {
            this.$('#name').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
            return false;
        }
        var url = this.$('#url').val() ? this.is_valid_url : false;
        if (!(this.file.name || url)) {
            this.$('#url').closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
            return false;
        }
        return true;
    },
    save: function (ev) {
        var self = this;
        if (this.validate()) {
            var values = this.get_value();
            if ($(ev.target).data('published')) {
                values.website_published = true;
            }
            this.$('.oe_slides_upload_loading').show();
            this.$('.modal-footer, .modal-body').hide();
            ajax.jsonRpc("/slides/add_slide", 'call', values).then(function (data) {
                if (data.error) {
                    self.display_alert(data.error);
                    self.$('.oe_slides_upload_loading').hide();
                    self.$('.modal-footer, .modal-body').show();

                } else {
                    window.location = data.url;
                }
            });
        }
    },
    cancel: function () {
        this.trigger("cancel");
    }
});

// bind the event to the button
$('.oe_slide_js_upload').on('click', function () {
    var channel_id = $(this).attr('channel_id');
    slides.page_widgets['upload_dialog'] = new SlideDialog(this, channel_id).appendTo(document.body);
});
});
Example #27
0
odoo.define('payment_stripe.stripe', function(require) {
    "use strict";

    var ajax = require('web.ajax');
    var core = require('web.core');
    var _t = core._t;
    var qweb = core.qweb;
    ajax.loadXML('/payment_stripe/static/src/xml/stripe_templates.xml', qweb);

    // The following currencies are integer only, see
    // https://stripe.com/docs/currencies#zero-decimal
    var int_currencies = [
        'BIF', 'XAF', 'XPF', 'CLP', 'KMF', 'DJF', 'GNF', 'JPY', 'MGA', 'PYG',
        'RWF', 'KRW', 'VUV', 'VND', 'XOF'
    ];

    if ($.blockUI) {
        // our message needs to appear above the modal dialog
        $.blockUI.defaults.baseZ = 2147483647; //same z-index as StripeCheckout
        $.blockUI.defaults.css.border = '0';
        $.blockUI.defaults.css["background-color"] = '';
        $.blockUI.defaults.overlayCSS["opacity"] = '0.9';
    }
    var stripeHandler;
    function getStripeHandler()
    {
        if (stripeHandler) {
            return stripeHandler;
        }
        var handler = stripeHandler = StripeCheckout.configure({
            key: $("input[name='stripe_key']").val(),
            image: $("input[name='stripe_image']").val(),
            locale: 'auto',
            token: function(token, args) {
                handler.isTokenGenerate = true;
                if ($.blockUI) {
                    var msg = _t("Just one more second, confirming your payment...");
                    $.blockUI({
                        'message': '<h2 class="text-white"><img src="/web/static/src/img/spin.png" class="fa-pulse"/>' +
                                '    <br />' + msg +
                                '</h2>'
                    });
                }
                ajax.jsonRpc("/payment/stripe/create_charge", 'call', {
                    tokenid: token.id,  // TBE TODO: for backward compatibility, remove on master
                    email: token.email, // TBE TODO: for backward compatibility, remove on master
                    token: token,
                    amount: $("input[name='amount']").val(),
                    acquirer_id: $("#acquirer_stripe").val(),
                    currency: $("input[name='currency']").val(),
                    invoice_num: $("input[name='invoice_num']").val(),
                    tx_ref: $("input[name='invoice_num']").val(),
                    return_url: $("input[name='return_url']").val()
                }).then(function(data){
                    handler.isTokenGenerate = false;
                    window.location.href = data;
                    if ($.blockUI) {
                        $.unblockUI();
                    }
                }).guardedCatch(function(data){
                    var msg = data && data.data && data.data.message;
                    var wizard = $(qweb.render('stripe.error', {'msg': msg || _t('Payment error')}));
                    wizard.appendTo($('body')).modal({'keyboard': true});
                    if ($.blockUI) {
                        $.unblockUI();
                    }
                });
            },
        });
        return handler;
    }

    require('web.dom_ready');
    if (!$('.o_payment_form').length) {
        return Promise.reject("DOM doesn't contain '.o_payment_form'");
    }

    var observer = new MutationObserver(function(mutations, observer) {
        for(var i=0; i<mutations.length; ++i) {
            for(var j=0; j<mutations[i].addedNodes.length; ++j) {
                if(mutations[i].addedNodes[j].tagName.toLowerCase() === "form" && mutations[i].addedNodes[j].getAttribute('provider') == 'stripe') {
                    display_stripe_form($(mutations[i].addedNodes[j]));
                }
            }
        }
    });


    function display_stripe_form(provider_form) {
        // Open Checkout with further options
        var payment_form = $('.o_payment_form');
        if(!payment_form.find('i').length)
            payment_form.append('<i class="fa fa-spinner fa-spin"/>');
            payment_form.attr('disabled','disabled');

        var payment_tx_url = payment_form.find('input[name="prepare_tx_url"]').val();
        var access_token = $("input[name='access_token']").val() || $("input[name='token']").val() || '';

        var get_input_value = function(name) {
            return provider_form.find('input[name="' + name + '"]').val();
        }

        var acquirer_id = parseInt(provider_form.find('#acquirer_stripe').val());
        var amount = parseFloat(get_input_value("amount") || '0.0');
        var currency = get_input_value("currency");
        var email = get_input_value("email");
        var invoice_num = get_input_value("invoice_num");
        var merchant = get_input_value("merchant");

        ajax.jsonRpc(payment_tx_url, 'call', {
            acquirer_id: acquirer_id,
            access_token: access_token,
        }).then(function(data) {
            var $pay_stripe = $('#pay_stripe').detach();
            try { provider_form[0].innerHTML = data; } catch (e) {}
            // Restore 'Pay Now' button HTML since data might have changed it.
            $(provider_form[0]).find('#pay_stripe').replaceWith($pay_stripe);
        }).then(function () {
            getStripeHandler().open({
                name: merchant,
                description: invoice_num,
                email: email,
                currency: currency,
                amount: _.contains(int_currencies, currency) ? amount : amount * 100,
            });
        });
    }

    $.getScript("https://checkout.stripe.com/checkout.js", function(data, textStatus, jqxhr) {
        observer.observe(document.body, {childList: true});
        display_stripe_form($('form[provider="stripe"]'));
    });
});
Example #28
0
 fetch_followers: function () {
     return ajax.jsonRpc('/mail/read_followers', 'call', {
         follower_ids: this.value,
         res_model: this.view.model,
     });
 },
Example #29
0
odoo.define('website.WebsiteRoot', function (require) {
'use strict';

var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var utils = require('web.utils');
var BodyManager = require('web_editor.BodyManager');
var weContext = require('web_editor.context');
var rootWidget = require('web_editor.root_widget');
var sAnimation = require('website.content.snippets.animation');
require("website.content.zoomodoo");

var _t = core._t;

var websiteRootRegistry = new rootWidget.RootWidgetRegistry();

// Load localizations outside the WebsiteRoot to not wait for DOM ready (but
// wait for them in WebsiteRoot)
var lang = utils.get_cookie('frontend_lang') || weContext.get().lang; // FIXME the cookie value should maybe be in the ctx?
var localeDef = ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));

var WebsiteRoot = BodyManager.extend({
    events: _.extend({}, BodyManager.prototype.events || {}, {
        'click .js_change_lang': '_onLangChangeClick',
        'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
        'submit .js_website_submit_form': '_onWebsiteFormSubmit',
        'click .js_disable_on_click': '_onDisableOnClick',
        'click .js_multi_website_switch': '_multiWebsiteSwitch',
        'click .js_multi_company_switch': '_multiCompanySwitch',
    }),
    custom_events: _.extend({}, BodyManager.prototype.custom_events || {}, {
        animation_start_demand: '_onAnimationStartDemand',
        animation_stop_demand: '_onAnimationStopDemand',
        main_object_request: '_onMainObjectRequest',
        ready_to_clean_for_save: '_onAnimationStopDemand',
    }),

    /**
     * @constructor
     */
    init: function () {
        this._super.apply(this, arguments);
        this.animations = [];
    },
    /**
     * @override
     */
    willStart: function () {
        // TODO would be even greater to wait for localeDef only when necessary
        return $.when(this._super.apply(this, arguments), localeDef);
    },
    /**
     * @override
     */
    start: function () {
        var defs = [this._super.apply(this, arguments)];

        // Animations
        defs.push(this._startAnimations());

        // Compatibility lang change ?
        if (!this.$('.js_change_lang').length) {
            var $links = this.$('ul.js_language_selector li a:not([data-oe-id])');
            var m = $(_.min($links, function (l) {
                return $(l).attr('href').length;
            })).attr('href');
            $links.each(function () {
                var $link = $(this);
                var t = $link.attr('href');
                var l = (t === m) ? "default" : t.split('/')[1];
                $link.data('lang', l).addClass('js_change_lang');
            });
        }

        // Display image thumbnail
        this.$(".o_image[data-mimetype^='image']").each(function () {
            var $img = $(this);
            if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
                $img.css('background-image', "url('" + $img.data('src') + "')");
            }
        });

        // Enable magnify on zommable img
        this.$('.zoomable img[data-zoom]').zoomOdoo();

        // Auto scroll
        if (window.location.hash.indexOf("scrollTop=") > -1) {
            this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1];
        }

        // Fix for IE:
        if ($.fn.placeholder) {
            $('input, textarea').placeholder();
        }

        return $.when.apply($, defs);
    },

    //--------------------------------------------------------------------------
    // Private
    //--------------------------------------------------------------------------

    /**
     * As the WebsiteRoot instance is designed to be unique, the associated
     * registry has been instantiated outside of the class and is simply
     * returned here.
     *
     * @private
     * @override
     */
    _getRegistry: function () {
        return websiteRootRegistry;
    },
    /**
     * Creates an Animation instance for each DOM element which matches the
     * `selector` key of one of the registered animations
     * (@see Animation.selector).
     *
     * @private
     * @param {boolean} [editableMode=false] - true if the page is in edition mode
     * @param {jQuery} [$from]
     *        only initialize the animations whose `selector` matches the
     *        element or one of its descendant (default to the wrapwrap element)
     * @returns {Deferred}
     */
    _startAnimations: function (editableMode, $from) {
        var self = this;

        editableMode = editableMode || false;
        if ($from === undefined) {
            $from = this.$('#wrapwrap');
        }

        this._stopAnimations($from);

        var defs = _.map(sAnimation.registry, function (Animation, animationName) {
            var selector = Animation.prototype.selector || '';
            var $target = $from.find(selector).addBack(selector);

            var defs = _.map($target, function (el) {
                var animation = new Animation(self, editableMode);
                self.animations.push(animation);
                return animation.attachTo($(el));
            });
            return $.when.apply($, defs);
        });
        return $.when.apply($, defs);
    },
    /**
     * Destroys all animation instances. Especially needed before saving while
     * in edition mode for example.
     *
     * @private
     * @param {jQuery} [$from]
     *        only stop the animations linked to the given element(s) or one of
     *        its descendants
     */
    _stopAnimations: function ($from) {
        var removedAnimations = _.map(this.animations, function (animation) {
            if (!$from
             || $from.filter(animation.el).length
             || $from.find(animation.el).length) {
                animation.destroy();
                return animation;
            }
            return null;
        });
        this.animations = _.difference(this.animations, removedAnimations);
    },

    //--------------------------------------------------------------------------
    // Handlers
    //--------------------------------------------------------------------------

    /**
     * Called when the root is notified that the animations have to be
     * (re)started.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onAnimationStartDemand: function (ev) {
        this._startAnimations(ev.data.editableMode, ev.data.$target)
            .done(ev.data.onSuccess)
            .fail(ev.data.onFailure);
    },
    /**
     * Called when the root is notified that the animations have to be
     * stopped.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onAnimationStopDemand: function (ev) {
        this._stopAnimations(ev.data.$target);
    },
    /**
     * @todo review
     * @private
     */
    _onLangChangeClick: function (ev) {
        ev.preventDefault();

        var $target = $(ev.target);
        // retrieve the hash before the redirect
        var redirect = {
            lang: $target.data('lang'),
            url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),
            hash: encodeURIComponent(window.location.hash)
        };
        window.location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect);
    },
    /**
     * Checks information about the page main object.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onMainObjectRequest: function (ev) {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        ev.data.callback({
            model: m[1],
            id: m[2] | 0,
        });
    },
    /**
     * @todo review
     * @private
     */
    _onPublishBtnClick: function (ev) {
        ev.preventDefault();

        var self = this;
        var $data = $(ev.currentTarget).parents(".js_publish_management:first");
        this._rpc({
            route: $data.data('controller') || '/website/publish',
            params: {
                id: +$data.data('id'),
                object: $data.data('object'),
            },
        })
        .done(function (result) {
            $data.toggleClass("css_unpublished css_published");
            $data.find('input').prop("checked", result);
            $data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off');
        })
        .fail(function (err, data) {
            return new Dialog(self, {
                title: data.data ? data.data.arguments[0] : "",
                $content: $('<div/>', {
                    html: (data.data ? data.data.arguments[1] : data.statusText)
                        + '<br/>'
                        + _.str.sprintf(
                            _t('It might be possible to edit the relevant items or fix the issue in <a href="%s">the classic Odoo interface</a>'),
                            '/web#return_label=Website&model=' + $data.data('object') + '&id=' + $data.data('id')
                        ),
                }),
            }).open();
        });
    },
    /**
     * @todo review
     * @private
     */
    _onWebsiteFormSubmit: function (ev) {
        var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit');
        _.each($buttons, function (btn) {
            var $btn = $(btn);
            $btn.attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text());
            $btn.button('loading');
        });
    },
    /**
     * Called when the root is notified that the button should be
     * disabled after the first click.
     *
     * @private
     * @param {Event} ev
     */
    _onDisableOnClick: function (ev) {
        $(ev.currentTarget).addClass('disabled');
    },

    /**
     * Called when clicking on the multi-website switcher.
     *
     * @param {OdooEvent} ev
     */
    _multiWebsiteSwitch: function (ev) {
        var website_id_to_switch_to = ev.currentTarget.getAttribute('website-id');

        // need to force in each case, even if domain is set
        // Website 1: localhost; Website 2: 0.0.0.0; website 3: -
        // when you switch 3 <--> 1, you need to force the website

        var website_domain = ev.currentTarget.getAttribute('domain');
        var url = $.param.querystring(window.location.href, {'fw': website_id_to_switch_to});
        if (website_domain && window.location.hostname !== website_domain) {
            // if domain unchanged, this line will do a nop while we need to refresh
            // the page to load the new forced website.
            url = new URL(url);
            url.hostname = website_domain;
        }
        window.location.href = url;
    },

    _multiCompanySwitch: function (ev) {
        var company_id_to_switch_to = ev.currentTarget.getAttribute('company-id');
        this._rpc({model: 'res.users',
            method: 'write',
            args: [odoo.session_info.user_id, {'company_id': parseInt(company_id_to_switch_to, 10)}],
        }).then(function () {
            window.location.reload(true);
        });
    },
});

return {
    WebsiteRoot: WebsiteRoot,
    websiteRootRegistry: websiteRootRegistry,
};
});
Example #30
0
 _.each(this.config.css_libs, function (url) {
     defs.push(ajax.loadCSS(url));
 });