Example #1
0
odoo.define('website.contentMenu', function (require) {
'use strict';

var Class = require('web.Class');
var core = require('web.core');
var Dialog = require('web.Dialog');
var time = require('web.time');
var weContext = require('web_editor.context');
var weWidgets = require('web_editor.widget');
var websiteNavbarData = require('website.navbar');
var websiteRootData = require('website.WebsiteRoot');
var Widget = require('web.Widget');

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

var PagePropertiesDialog = weWidgets.Dialog.extend({
    template: 'website.pagesMenu.page_info',
    xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.pageProperties.xml']
    ),
    events: _.extend({}, weWidgets.Dialog.prototype.events, {
        'keyup input#page_name': '_onNameChanged',
        'keyup input#page_url': '_onUrlChanged',
        'change input#create_redirect': '_onCreateRedirectChanged',
    }),

    /**
     * @constructor
     * @override
     */
    init: function (parent, page_id, options) {
        var self = this;
        var serverUrl = window.location.origin + '/';
        var length_url = serverUrl.length;
        var serverUrlTrunc = serverUrl;
        if (length_url > 30) {
            serverUrlTrunc = serverUrl.slice(0,14) + '..' + serverUrl.slice(-14);
        }
        this.serverUrl = serverUrl;
        this.serverUrlTrunc = serverUrlTrunc;
        this.current_page_url = window.location.pathname;
        this.page_id = page_id;

        var buttons = [
            {text: _t("Save"), classes: 'btn-primary', click: this.save},
            {text: _t("Discard"), close: true},
        ];
        if (options.fromPageManagement) {
            buttons.push({
                text: _t("Go To Page"),
                icon: 'fa-globe',
                classes: 'btn-link float-right',
                click: function (e) {
                    window.location.href = '/' + self.page.url;
                },
            });
        }
        buttons.push({
            text: _t("Delete Page"),
            icon: 'fa-trash',
            classes: 'btn-link float-right',
            click: function (e) {
                _deletePage.call(this, self.page_id, options.fromPageManagement);
            },
        });
        this._super(parent, _.extend({}, {
            title: _t("Page Properties"),
            size: 'medium',
            buttons: buttons,
        }, options || {}));
    },
    /**
     * @override
     */
    willStart: function () {
        var defs = [this._super.apply(this, arguments)];
        var self = this;
        var context = weContext.get();

        defs.push(this._rpc({
            model: 'website.page',
            method: 'get_page_info',
            args: [this.page_id],
            context: context,
        }).then(function (page) {
            page[0].url = _.str.startsWith(page[0].url, '/') ? page[0].url.substring(1) : page[0].url;
            self.page = page[0];
        }));

        defs.push(this._rpc({
            model: 'website.redirect',
            method: 'fields_get',
        }).then(function (fields) {
            self.fields = fields;
        }));

        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        var context = weContext.get();

        var defs = [this._super.apply(this, arguments)];

        this.$('.ask_for_redirect').addClass('d-none');
        this.$('.redirect_type').addClass('d-none');
        this.$('.warn_about_call').addClass('d-none');

        defs.push(this._getPageDependencies(this.page_id, context)
        .then(function (dependencies) {
            var dep_text = [];
            _.each(dependencies, function (value, index) {
                if (value.length > 0) {
                    dep_text.push(value.length + ' ' + index.toLowerCase());
                }
            });
            dep_text = dep_text.join(', ');
            self.$('#dependencies_redirect').html(qweb.render('website.show_page_dependencies', { dependencies: dependencies, dep_text: dep_text }));
            self.$('#dependencies_redirect [data-toggle="popover"]').popover({
                container: 'body',
            });
        }));

        defs.push(this._getSupportedMimetype(context)
        .then(function (mimetypes) {
            self.supportedMimetype = mimetypes;
        }));

        defs.push(this._getPageKeyDependencies(this.page_id, context)
        .then(function (dependencies) {
            var dep_text = [];
            _.each(dependencies, function (value, index) {
                if (value.length > 0) {
                    dep_text.push(value.length + ' ' + index.toLowerCase());
                }
            });
            dep_text = dep_text.join(', ');
            self.$('.warn_about_call').html(qweb.render('website.show_page_key_dependencies', {dependencies: dependencies, dep_text: dep_text}));
            self.$('.warn_about_call [data-toggle="popover"]').popover({
               container: 'body',
            });
        }));

        defs.push(this._rpc({model: 'res.users',
                             method: 'has_group',
                             args: ['website.group_multi_website'],
                             context: context})
                  .then(function (has_group) {
                      if (!has_group) {
                          self.$('#website_restriction').addClass('hidden');
                      }
                  }));

        var datepickersOptions = {
            minDate: moment({y: 1900}),
            maxDate: moment().add(200, 'y'),
            calendarWeeks: true,
            icons : {
                time: 'fa fa-clock-o',
                date: 'fa fa-calendar',
                next: 'fa fa-chevron-right',
                previous: 'fa fa-chevron-left',
                up: 'fa fa-chevron-up',
                down: 'fa fa-chevron-down',
            },
            locale : moment.locale(),
            format : time.getLangDatetimeFormat(),
            widgetPositioning : {
                horizontal: 'auto',
                vertical: 'top',
            },
             widgetParent: 'body',
        };
        if (this.page.date_publish) {
            datepickersOptions.defaultDate = time.str_to_datetime(this.page.date_publish);
        }
        this.$('#date_publish_container').datetimepicker(datepickersOptions);

        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    destroy: function () {
        $('.popover').popover('hide');
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    save: function (data) {
        var self = this;
        var context = weContext.get();
        var url = this.$('#page_url').val();

        var $date_publish = this.$("#date_publish");
        $date_publish.closest(".form-group").removeClass('o_has_error').find('.form-control, .custom-select').removeClass('is-invalid');
        var date_publish = $date_publish.val();
        if (date_publish !== "") {
            date_publish = this._parse_date(date_publish);
            if (!date_publish) {
                $date_publish.closest(".form-group").addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid');
                return;
            }
        }
        var params = {
            id: this.page.id,
            name: this.$('#page_name').val(),
            // Replace duplicate following '/' by only one '/'
            url: url.replace(/\/{2,}/g, '/'),
            is_menu: this.$('#is_menu').prop('checked'),
            is_homepage: this.$('#is_homepage').prop('checked'),
            website_published: this.$('#is_published').prop('checked'),
            create_redirect: this.$('#create_redirect').prop('checked'),
            redirect_type: this.$('#redirect_type').val(),
            website_indexed: this.$('#is_indexed').prop('checked'),
            date_publish: date_publish,
        };
        this._rpc({
            model: 'website.page',
            method: 'save_page_info',
            args: [[context.website_id], params],
            context: context,
        }).then(function (url) {
            // If from page manager: reload url, if from page itself: go to
            // (possibly) new url
            var mo;
            self.trigger_up('main_object_request', {
                callback: function (value) {
                    mo = value;
                },
            });
            if (mo.model === 'website.page') {
                window.location.href = url.toLowerCase();
            } else {
                window.location.reload(true);
            }
        });
    },

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

    /**
     * Retrieves the page URL dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_dependencies',
            args: [moID],
            context: context,
        });
    },
    /**
     * Retrieves the page's key dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageKeyDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_key_dependencies',
            args: [moID],
            context: context,
        });
    },
    /**
     * Retrieves supported mimtype
     *
     * @private
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getSupportedMimetype: function (context) {
        return this._rpc({
            model: 'website',
            method: 'guess_mimetype',
            context: context,
        });
    },
    /**
     * Returns information about the page main object.
     *
     * @private
     * @returns {Object} model and id
     */
    _getMainObject: function () {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        return {
            model: m[1],
            id: m[2] | 0,
        };
    },
    /**
     * Converts a string representing the browser datetime
     * (exemple: Albanian: '2018-Qer-22 15.12.35.')
     * to a string representing UTC in Odoo's datetime string format
     * (exemple: '2018-04-22 13:12:35').
     *
     * The time zone of the datetime string is assumed to be the one of the
     * browser and it will be converted to UTC (standard for Odoo).
     *
     * @private
     * @param {String} value A string representing a datetime.
     * @returns {String|false} A string representing an UTC datetime if the given value is valid, false otherwise.
     */
    _parse_date: function (value) {
        var datetime = moment(value, time.getLangDatetimeFormat(), true);
        if (datetime.isValid()) {
            return time.datetime_to_str(datetime.toDate());
        }
        else {
            return false;
        }
    },

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

    /**
     * @private
     */
    _onUrlChanged: function () {
        var url = this.$('input#page_url').val();
        this.$('.ask_for_redirect').toggleClass('d-none', url === this.page.url);
    },
    /**
     * @private
     */
    _onNameChanged: function () {
        var name = this.$('input#page_name').val();
        // If the file type is a supported mimetype, check if it is t-called.
        // If so, warn user. Note: different from page_search_dependencies which
        // check only for url and not key
        var ext = '.' + this.page.name.split('.').pop();
        if (ext in this.supportedMimetype && ext !== '.html') {
            this.$('.warn_about_call').toggleClass('d-none', name === this.page.name);
        }
    },
    /**
     * @private
     */
    _onCreateRedirectChanged: function () {
        var createRedirect = this.$('input#create_redirect').prop('checked');
        this.$('.redirect_type').toggleClass('d-none', !createRedirect);
    },
});

var MenuEntryDialog = weWidgets.LinkDialog.extend({
    xmlDependencies: weWidgets.LinkDialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),

    /**
     * @constructor
     */
    init: function (parent, options, editor, data) {
        data.text = data.name || '';
        data.isNewWindow = data.new_window;
        this.data = data;
        this._super(parent, _.extend({}, {
            title: _t("Create Menu"),
        }, options || {}), editor, data);
    },
    /**
     * @override
     */
    start: function () {
        // Remove style related elements
        this.$('.o_link_dialog_preview').remove();
        this.$('input[name="is_new_window"], .link-style').closest('.form-group').remove();
        this.$modal.find('.modal-lg').removeClass('modal-lg');
        this.$('form.col-lg-8').removeClass('col-lg-8').addClass('col-12');

        // Adapt URL label
        this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label"));

        this.$('#o_link_dialog_url_input').after(qweb.render('website.contentMenu.dialog.edit.link_menu_hint'));

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

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    save: function () {
        var $e = this.$('#o_link_dialog_label_input');
        if (!$e.val() || !$e[0].checkValidity()) {
            $e.closest('.form-group').addClass('o_has_error').find('.form-control, .custom-select').addClass('is-invalid')
            $e.focus();
            return;
        }
        return this._super.apply(this, arguments);
    },

});

var SelectEditMenuDialog = weWidgets.Dialog.extend({
    template: 'website.contentMenu.dialog.select',
    xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),

    /**
     * @constructor
     * @override
     */
    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 || {}));
    },
    /**
     * @override
     */
    save: function () {
        this.final_data = parseInt(this.$el.find('select').val() || null);
        this._super.apply(this, arguments);
    },
});

var EditMenuDialog = weWidgets.Dialog.extend({
    template: 'website.contentMenu.dialog.edit',
    xmlDependencies: weWidgets.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),
    events: _.extend({}, weWidgets.Dialog.prototype.events, {
        'click a.js_add_menu': '_onAddMenuButtonClick',
        'click button.js_delete_menu': '_onDeleteMenuButtonClick',
        'click button.js_edit_menu': '_onEditMenuButtonClick',
    }),

    /**
     * @constructor
     * @override
     */
    init: function (parent, options, rootID) {
        this._super(parent, _.extend({}, {
            title: _t("Edit Menu"),
            size: 'medium',
        }, options || {}));
        this.rootID = rootID;
    },
    /**
     * @override
     */
    willStart: function () {
        var defs = [this._super.apply(this, arguments)];
        var self = this;
        var context = weContext.get();
        defs.push(this._rpc({
            model: 'website.menu',
            method: 'get_tree',
            args: [context.website_id, this.rootID],
            context: context,
        }).then(function (menu) {
            self.menu = menu;
            self.root_menu_id = menu.id;
            self.flat = self._flatenize(menu);
            self.to_delete = [];
        }));
        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    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;
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    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 = weContext.get();
        // 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);
            }
        });
        this._rpc({
            model: 'website.menu',
            method: 'save',
            args: [context.website_id, { data: data, to_delete: self.to_delete }],
            context: context,
        }).then(function () {
            return _super();
        });
    },

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

    /**
     * Returns a mapping id -> menu item containing all the menu items in the
     * given menu hierarchy.
     *
     * @private
     * @param {Object} node
     * @param {Object} [_dict] internal use: the mapping being built
     * @returns {Object}
     */
    _flatenize: function (node, _dict) {
        _dict = _dict || {};
        var self = this;
        _dict[node.id] = node;
        node.children.forEach(function (child) {
            self._flatenize(child, _dict);
        });
        return _dict;
    },

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

    /**
     * Called when the "add menu" button is clicked -> Opens the appropriate
     * dialog to edit this new menu.
     *
     * @private
     */
    _onAddMenuButtonClick: 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();
    },
    /**
     * Called when the "delete menu" button is clicked -> Deletes this menu.
     *
     * @private
     */
    _onDeleteMenuButtonClick: function (ev) {
        var $menu = $(ev.currentTarget).closest('[data-menu-id]');
        var menuID = $menu.data('menu-id')|0;
        if (menuID) {
            this.to_delete.push(menuID);
        }
        $menu.remove();
    },
    /**
     * Called when the "edit menu" button is clicked -> Opens the appropriate
     * dialog to edit this menu.
     *
     * @private
     */
    _onEditMenuButtonClick: 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 {
            Dialog.alert(null, "Could not find menu entry");
        }
    },
});

var PageOption = Class.extend({
    /**
     * @constructor
     * @param {string} name
     *        the option's name = the field's name in website.page model
     * @param {*} value
     * @param {function} setValueCallback
     *        a function which simulates an option's value change without
     *        asking the server to change it
     */
    init: function (name, value, setValueCallback) {
        this.name = name;
        this.value = value;
        this.isDirty = false;
        this.setValueCallback = setValueCallback;
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * Sets the new option's value thanks to the related callback.
     *
     * @param {*} [value]
     *        by default: consider the current value is a boolean and toggle it
     */
    setValue: function (value) {
        if (value === undefined) {
            value = !this.value;
        }
        this.setValueCallback.call(this, value);
        this.value = value;
        this.isDirty = true;
    },
});

var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    xmlDependencies: ['/website/static/src/xml/website.xml'],
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        edit_menu: '_editMenu',
        get_page_option: '_getPageOption',
        on_save: '_onSave',
        page_properties: '_pageProperties',
        toggle_page_option: '_togglePageOption',
    }),
    pageOptionsSetValueCallbacks: {
        header_overlay: function (value) {
            $('#wrapwrap').toggleClass('o_header_overlay', value);
        },
        header_color: function (value) {
            $('#wrapwrap > header').removeClass(this.value)
                                   .addClass(value);
        },
    },

    /**
     * @override
     */
    start: function () {
        var self = this;
        this.pageOptions = {};
        _.each($('.o_page_option_data'), function (el) {
            var value = el.value;
            if (value === "True") {
                value = true;
            } else if (value === "False") {
                value = false;
            }
            self.pageOptions[el.name] = new PageOption(
                el.name,
                value,
                self.pageOptionsSetValueCallbacks[el.name]
            );
        });
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Asks the user which menu to edit if multiple menus exist on the page.
     * Then opens the menu edition dialog.
     * Then executes the given callback once the edition is saved, to finally
     * reload the page.
     *
     * @private
     * @param {function} [beforeReloadCallback]
     * @returns {Deferred}
     *          Unresolved if the menu is edited and saved as the page will be
     *          reloaded.
     *          Resolved otherwise.
     */
    _editMenu: function (beforeReloadCallback) {
        var self = this;
        var def = $.Deferred();

        // If there is multiple menu on the page, ask the user which one he
        // wants to edit
        var selectDef = $.Deferred();
        if ($('[data-content_menu_id]').length) {
            var select = new SelectEditMenuDialog(this);
            select.on('save', selectDef, selectDef.resolve);
            select.on('cancel', def, def.resolve);
            select.open();
        } else {
            selectDef.resolve(null);
        }
        selectDef.then(function (rootID) {
            // Open the dialog to show the menu structure and allow its edition
            var editDef = $.Deferred();
            var dialog = new EditMenuDialog(self, {}, rootID).open();
            dialog.on('save', editDef, editDef.resolve);
            dialog.on('cancel', def, def.resolve);
            return editDef;
        }).then(function () {
            // Before reloading the page after menu modification, does the
            // given action to do.
            return beforeReloadCallback && beforeReloadCallback();
        }).then(function () {
            // Reload the page so that the menu modification are shown
            window.location.reload(true);
        });

        return def;
    },
    /**
     * Retrieves the value of a page option.
     *
     * @private
     * @param {string} name
     * @returns {Deferred<*>}
     */
    _getPageOption: function (name) {
        var option = this.pageOptions[name];
        if (!option) {
            return $.Deferred().reject();
        }
        return $.when(option.value);
    },
    /**
     * On save, simulated page options have to be server-saved.
     *
     * @private
     * @returns {Deferred}
     */
    _onSave: function () {
        var self = this;
        var defs = _.map(this.pageOptions, function (option, optionName) {
            if (option.isDirty) {
                return self._togglePageOption({
                    name: optionName,
                    value: option.value,
                }, true, true);
            }
        });
        return $.when.apply($, defs);
    },
    /**
     * Opens the page properties dialog.
     *
     * @private
     * @returns {Deferred}
     */
    _pageProperties: function () {
        var mo;
        this.trigger_up('main_object_request', {
            callback: function (value) {
                mo = value;
            },
        });
        var dialog = new PagePropertiesDialog(this, mo.id, {}).open();
        return dialog.opened();
    },
    /**
     * Toggles a page option.
     *
     * @private
     * @param {Object} params
     * @param {string} params.name
     * @param {*} [params.value] (change value by default true -> false -> true)
     * @param {boolean} [forceSave=false]
     * @param {boolean} [noReload=false]
     * @returns {Deferred}
     */
    _togglePageOption: function (params, forceSave, noReload) {
        // First check it is a website page
        var mo;
        this.trigger_up('main_object_request', {
            callback: function (value) {
                mo = value;
            },
        });
        if (mo.model !== 'website.page') {
            return $.Deferred().reject();
        }

        // Check if this is a valid option
        var option = this.pageOptions[params.name];
        if (!option) {
            return $.Deferred().reject();
        }

        // Toggle the value
        option.setValue(params.value);

        // If simulate is true, it means we want the option to be toggled but
        // not saved on the server yet
        if (!forceSave) {
            return $.when();
        }

        // If not, write on the server page and reload the current location
        var vals = {};
        vals[params.name] = option.value;
        var def = this._rpc({
            model: 'website.page',
            method: 'write',
            args: [[mo.id], vals],
        });
        if (noReload) {
            return def;
        }
        return def.then(function () {
            window.location.reload();
            return $.Deferred();
        });
    },
});

var PageManagement = Widget.extend({
    xmlDependencies: ['/website/static/src/xml/website.xml'],
    events: {
        'click a.js_page_properties': '_onPagePropertiesButtonClick',
        'click a.js_clone_page': '_onClonePageButtonClick',
        'click a.js_delete_page': '_onDeletePageButtonClick',
    },

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

    /**
     * Retrieves the page dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_dependencies',
            args: [moID],
            context: context,
        });
    },

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

    _onPagePropertiesButtonClick: function (ev) {
        var moID = $(ev.currentTarget).data('id');
        var dialog = new PagePropertiesDialog(this,moID, {'fromPageManagement': true}).open();
        return dialog;
    },
    _onClonePageButtonClick: function (ev) {
        var pageId = $(ev.currentTarget).data('id');
        var context = weContext.get();
        this._rpc({
            model: 'website.page',
            method: 'clone_page',
            args: [pageId],
            kwargs: {
                context: context,
            },
        }).then(function (path) {
            window.location.href = path;
        });
    },
    _onDeletePageButtonClick: function (ev) {
        var pageId = $(ev.currentTarget).data('id');
        _deletePage.call(this, pageId, true);
    },
});

/**
 * Deletes the page after showing a dependencies warning for the given page id.
 *
 * @private
 * @param {integer} pageId - The ID of the page to be deleted
 * @param {Boolean} fromPageManagement
 *                  Is the function called by the page manager?
 *                  It will affect redirect after page deletion: reload or '/'
 */
// TODO: This function should be integrated in a widget in the future
function _deletePage(pageId, fromPageManagement) {
    var self = this;
    var context = weContext.get();
    var def = $.Deferred();

    // Search the page dependencies
    this._getPageDependencies(pageId, context)
    .then(function (dependencies) {
    // Inform the user about those dependencies and ask him confirmation
        var confirmDef = $.Deferred();
        Dialog.safeConfirm(self, "", {
            title: _t("Delete Page"),
            $content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
            confirm_callback: confirmDef.resolve.bind(confirmDef),
            cancel_callback: def.resolve.bind(self),
        });
        return confirmDef;
    }).then(function () {
    // Delete the page if the user confirmed
        return self._rpc({
            model: 'website.page',
            method: 'unlink',
            args: [pageId],
            context: context,
        });
    }).then(function () {
        if (fromPageManagement) {
            window.location.reload(true);
        }
        else {
            window.location.href = '/';
        }
    }, def.reject.bind(def));
}

websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu');
websiteRootData.websiteRootRegistry.add(PageManagement, '#list_website_pages');

return {
    PagePropertiesDialog: PagePropertiesDialog,
    ContentMenu: ContentMenu,
    EditMenuDialog: EditMenuDialog,
    MenuEntryDialog: MenuEntryDialog,
    SelectEditMenuDialog: SelectEditMenuDialog,
};
});
Example #2
0
odoo.define('website.theme', function (require) {
'use strict';

var config = require('web.config');
var core = require('web.core');
var ColorpickerDialog = require('web.colorpicker');
var Dialog = require('web.Dialog');
var widgets = require('web_editor.widget');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

var templateDef = null;

var ThemeCustomizeDialog = Dialog.extend({
    xmlDependencies: (Dialog.prototype.xmlDependencies || [])
        .concat(['/website/static/src/xml/website.editor.xml']),

    template: 'website.theme_customize',
    events: {
        'change [data-xmlid], [data-enable], [data-disable]': '_onChange',
        'click .checked [data-xmlid], .checked [data-enable], .checked [data-disable]': '_onChange',
        'click .o_theme_customize_color': '_onColorClick',
    },

    /**
     * @constructor
     */
    init: function (parent, options) {
        options = options || {};
        this._super(parent, _.extend({
            title: _t("Customize this theme"),
            buttons: [],
        }, options));

        this.defaultTab = options.tab || 0;
    },
    /**
     * @override
     */
    willStart: function () {
        if (templateDef === null) {
            templateDef = this._rpc({
                model: 'ir.ui.view',
                method: 'read_template',
                args: ['website.theme_customize'],
            }).then(function (data) {
                return core.qweb.add_template(data);
            });
        }
        return $.when(this._super.apply(this, arguments), templateDef);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        this._generateDialogHTML();
        this.$modal.addClass('o_theme_customize_modal');

        // Enable the first option tab or the given default tab
        var $tabs = this.$('[data-toggle="tab"]');
        this.opened().then(function () {
            $tabs.eq(self.defaultTab).tab('show');

            var $colorPreview = self.$('.o_theme_customize_color_previews:visible');
            var $primary = $colorPreview.find('.o_theme_customize_color[data-color="primary"]');
            var $alpha = $colorPreview.find('.o_theme_customize_color[data-color="alpha"]');
            var $secondary = $colorPreview.find('.o_theme_customize_color[data-color="secondary"]');
            var $beta = $colorPreview.find('.o_theme_customize_color[data-color="beta"]');
            var sameAlphaPrimary = $primary.find('.o_color_preview').css('background-color') === $alpha.find('.o_color_preview').css('background-color');
            var sameBetaSecondary = $secondary.find('.o_color_preview').css('background-color') === $beta.find('.o_color_preview').css('background-color');
            if (!sameAlphaPrimary) {
                $alpha.find('.o_color_name').text(_t("Extra Color"));
                $primary.removeClass('d-none').addClass('d-flex');
            }
            if (!sameBetaSecondary) {
                $beta.find('.o_color_name').text(_t("Extra Color"));
                $secondary.removeClass('d-none').addClass('d-flex');
            }
            if (!sameAlphaPrimary && sameBetaSecondary) {
                $beta.insertBefore($alpha);
            } else if (sameAlphaPrimary && !sameBetaSecondary) {
                $secondary.insertAfter($alpha);
            }
        });

        // Hide the tab navigation if only one tab
        if ($tabs.length <= 1) {
            $tabs.closest('.nav').addClass('d-none');
        }

        this.$inputs = this.$('[data-xmlid], [data-enable], [data-disable]');

        return $.when(
            this._super.apply(this, arguments),
            this._loadViews()
        );
    },

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

    /**
     * @private
     */
    _generateDialogHTML: function () {
        var $contents = this.$el.children('content');
        if ($contents.length === 0) {
            return;
        }

        $contents.remove();
        this.$el.append(core.qweb.render('website.theme_customize_modal_layout'));
        var $navLinksContainer = this.$('.nav');
        var $navContents = this.$('.tab-content');

        _.each($contents, function (content) {
            var $content = $(content);

            var contentID = _.uniqueId('content-');

            // Build the nav tab for the content
            $navLinksContainer.append($('<li/>', {
                class: 'nav-item mb-1',
            }).append($('<a/>', {
                href: '#' + contentID,
                class: 'nav-link',
                'data-toggle': 'tab',
                text: $content.attr('string'),
            })));

            // Build the tab pane for the content
            var $navContent = $(core.qweb.render('website.theme_customize_modal_content', {
                id: contentID,
                title: $content.attr('title'),
            }));
            $navContents.append($navContent);
            var $optionsContainer = $navContent.find('.o_options_container');

            // Process content items
            _processItems($content.children(), $optionsContainer);
        });

        this.$('[title]').tooltip();

        function _processItems($items, $container) {
            var optionsName = _.uniqueId('option-');

            _.each($items, function (item) {
                var $item = $(item);
                var $col;

                switch (item.tagName) {
                    case 'OPT':
                        var widgetName = $item.data('widget');

                        // Build the options template
                        var $option = $(core.qweb.render('website.theme_customize_modal_option', {
                            name: optionsName,
                            id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),

                            string: $item.attr('string'),
                            icon: $item.data('icon'),
                            font: $item.data('font'),

                            xmlid: $item.data('xmlid'),
                            enable: $item.data('enable'),
                            disable: $item.data('disable'),
                            reload: $item.data('reload'),

                            widget: widgetName,
                        }));

                        if (widgetName) {
                            var $widget = $(core.qweb.render('website.theme_customize_' + widgetName));
                            $option.append($widget);
                        }

                        if ($container.hasClass('form-row')) {
                            $col = $('<div/>', {
                                class: _.str.sprintf('col-%s', $item.data('col') || 6),
                            });
                            $col.append($option);
                            $container.append($col);
                        } else {
                            $container.append($option);
                        }
                        break;

                    case 'LIST':
                        var $listContainer = $('<div/>', {class: 'py-1 px-2 o_theme_customize_option_list'});
                        $col = $('<div/>', {
                            class: _.str.sprintf('col-%s mt-2', $item.data('col') || 6),
                            'data-depends': $item.data('depends'),
                        }).append($('<h6/>', {text: $item.attr('string')}), $listContainer);
                        $container.append($col);
                        _processItems($item.children(), $listContainer);
                        break;
                }
            });
        }
    },
    /**
     * @private
     */
    _loadViews: function () {
        var self = this;
        return this._rpc({
            route: '/website/theme_customize_get',
            params: {
                xml_ids: this._getXMLIDs(this.$inputs),
            },
        }).done(function (data) {
            self.$inputs.prop('checked', false);
            _.each(self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])'), function (input) {
                var $input = $(input);
                if (!_.difference(self._getXMLIDs($input), data[0]).length) {
                    $input.prop('checked', true);
                }
            });
            _.each(self.$inputs.filter('[data-xmlid=""]'), function (input) {
                var $input = $(input);
                if (!self.$inputs.filter('[name="' + $input.attr('name') + '"]:checked').length) {
                    $input.prop('checked', true);
                }
            });
            self._setActive();
        }).fail(function (d, error) {
            Dialog.alert(this, error.data.message);
        });
    },
    /**
     * @private
     */
    _getInputs: function (string) {
        if (!string) {
            return $();
        }
        return this.$inputs.filter('#' + string.replace(/\s*,\s*/g, ', #'));
    },
    /**
     * @private
     */
    _getXMLIDs: function ($inputs) {
        var xmlIDs = [];
        _.each($inputs, function (input) {
            var $input = $(input);
            var xmlID = $input.data('xmlid');
            if (xmlID) {
                xmlIDs = xmlIDs.concat(xmlID.split(/\s*,\s*/));
            }
        });
        return xmlIDs;
    },
    /**
     * @private
     */
    _processChange: function ($inputs) {
        var self = this;
        this.$modal.addClass('o_theme_customize_loading');

        var bodyCustomImageXMLID = 'option_custom_body_image';
        var $inputBodyCustomImage = $inputs.filter('[data-xmlid*="website.' + bodyCustomImageXMLID + '"]:checked');
        if (!$inputBodyCustomImage.length) {
            return $.when();
        }

        var def = $.Deferred();
        var $image = $('<img/>');
        var editor = new widgets.MediaDialog(this, {onlyImages: true, firstFilters: ['background']}, null, $image[0]);

        editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
            var src = $(media).attr('src');
            self._rpc({
                model: 'ir.model.data',
                method: 'get_object_reference',
                args: ['website', bodyCustomImageXMLID],
            }).then(function (data) {
                return self._rpc({
                    model: 'ir.ui.view',
                    method: 'save',
                    args: [
                        data[1],
                        '#wrapwrap { background-image: url("' + src + '"); }',
                        '//style',
                    ],
                });
            }).then(function () {
                def.resolve();
            });
        });
        editor.on('cancel', this, function () {
            def.resolve();
        });

        editor.open();

        return def;
    },
    /**
     * @private
     */
    _setActive: function () {
        var self = this;

        // Look at all options to see if they are enabled or disabled
        var $enable = this.$inputs.filter('[data-xmlid]:checked');

        // Mark the labels as checked accordingly
        this.$('label').removeClass('checked');
        $enable.closest('label').addClass('checked');

        // Mark the option sets as checked if all their option are checked/unchecked
        var $sets = this.$inputs.filter('[data-enable], [data-disable]').not('[data-xmlid]');
        _.each($sets, function (set) {
            var $set = $(set);
            var checked = true;
            if (self._getInputs($set.data('enable')).not(':checked').length) {
                checked = false;
            }
            if (self._getInputs($set.data('disable')).filter(':checked').length) {
                checked = false;
            }
            $set.prop('checked', checked).closest('label').toggleClass('checked', checked);
        });

        // Make the hidden sections visible if their dependencies are met
        _.each(this.$('[data-depends]'), function (hidden) {
            var $hidden = $(hidden);
            var depends = $hidden.data('depends');
            var nbDependencies = depends ? depends.split(',').length : 0;
            var enabled = self._getInputs(depends).filter(':checked').length === nbDependencies;
            $hidden.toggleClass('d-none', !enabled);
        });
    },
    /**
     * @private
     */
    _updateStyle: function (enable, disable, reload) {
        if (reload || config.debug === 'assets') {
            window.location.href = $.param.querystring('/website/theme_customize_reload', {
                href: window.location.href,
                enable: (enable || []).join(','),
                disable: (disable || []).join(','),
                tab: this.$('.nav-link.active').parent().index(),
            });
            return $.Deferred();
        }

        var self = this;
        return this._rpc({
            route: '/website/theme_customize',
            params: {
                enable: enable,
                disable: disable,
                get_bundle: true,
            },
        }).then(function (bundles) {
            var defs = _.map(bundles, function (bundleContent, bundleName) {
                var linkSelector = 'link[href*="' + bundleName + '"]';
                var $links = $(linkSelector);
                var $newLinks = $(bundleContent).filter(linkSelector);

                var linksLoaded = $.Deferred();
                var nbLoaded = 0;
                $newLinks.on('load', function (e) {
                    if (++nbLoaded >= $newLinks.length) {
                        linksLoaded.resolve();
                    }
                });
                $newLinks.on('error', function (e) {
                    linksLoaded.reject();
                    window.location.hash = 'theme=true';
                    window.location.reload();
                });
                $links.last().after($newLinks);
                return linksLoaded.then(function () {
                    $links.remove();
                });
            });

            return $.when.apply($, defs).then(function () {
                self.$modal.removeClass('o_theme_customize_loading');
            });
        });
    },

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

    /**
     * @private
     * @param {Event} ev
     */
    _onChange: function (ev) {
        var self = this;

        // Checkout the option that changed
        var $option = $(ev.currentTarget);
        var $options = $option;
        var checked = $option.is(':checked');

        // If it was enabled, enable/disable the related input (see data-enable,
        // data-disable) and retain the ones that actually changed
        if (checked) {
            var $inputs;
            // Input to enable
            $inputs = this._getInputs($option.data('enable'));
            $options = $options.add($inputs.filter(':not(:checked)'));
            $inputs.prop('checked', true);
            // Input to disable
            $inputs = this._getInputs($option.data('disable'));
            $options = $options.add($inputs.filter(':checked'));
            $inputs.prop('checked', false);
        }
        var optionNames = _.uniq(_.map($options, function (option) {
            return option.name;
        }));
        $options = this.$inputs.filter(function (i, input) {
            return _.contains(optionNames, input.name);
        });

        // Look at all options to see if they are enabled or disabled
        var $enable = $options.filter('[data-xmlid]:checked');
        var $disable = $options.filter('[data-xmlid]:not(:checked)');

        this._setActive();

        // Update the style according to the whole set of options
        self._processChange($options).then(function () {
            self._updateStyle(
                self._getXMLIDs($enable),
                self._getXMLIDs($disable),
                $option.data('reload') && window.location.href.match(new RegExp($option.data('reload')))
            );
        });
    },
    /**
     * @private
     * @param {Event} ev
     */
    _onColorClick: function (ev) {
        var self = this;
        var $color = $(ev.currentTarget);
        var colorName = $color.data('color');
        var colorType = $color.data('colorType');

        var colorpicker = new ColorpickerDialog(this, {
            defaultColor: $color.find('.o_color_preview').css('background-color'),
        });
        colorpicker.on('colorpicker:saved', this, function (ev) {
            ev.stopPropagation();

            // TODO improve to be more efficient
            self._rpc({
                route: '/web_editor/get_assets_editor_resources',
                params: {
                    key: 'website.layout',
                    get_views: false,
                    get_scss: true,
                    bundles: false,
                    bundles_restriction: [],
                },
            }).then(function (data) {
                var files = data.scss[0][1];
                var file = _.find(files, function (file) {
                    var baseURL = '/website/static/src/scss/options/colors/';
                    return file.url === _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));
                });

                var colors = {};
                colors[colorName] = ev.data.cssColor;
                if (colorName === 'alpha') {
                    colors['beta'] = 'null';
                    colors['gamma'] = 'null';
                    colors['delta'] = 'null';
                    colors['epsilon'] = 'null';
                }

                var updatedFileContent = file.arch;
                _.each(colors, function (colorValue, colorName) {
                    var pattern = _.str.sprintf("'%s': %%s,\n", colorName);
                    var regex = new RegExp(_.str.sprintf(pattern, ".+"));
                    var replacement = _.str.sprintf(pattern, colorValue);
                    if (regex.test(updatedFileContent)) {
                        updatedFileContent = updatedFileContent
                            .replace(regex, replacement);
                    } else {
                        updatedFileContent = updatedFileContent
                            .replace(/( *)(.*hook.*)/, _.str.sprintf('$1%s$1$2', replacement));
                    }
                });

                return self._rpc({
                    route: '/web_editor/save_scss',
                    params: {
                        url: file.url,
                        bundle_xmlid: 'web.assets_common',
                        content: updatedFileContent,
                    },
                });
            }).then(function () {
                return self._updateStyle();
            });
        });
        colorpicker.open();
    },
});

var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        customize_theme: '_openThemeCustomizeDialog',
    }),

    /**
     * Automatically opens the theme customization dialog if the corresponding
     * hash is in the page URL.
     *
     * @override
     */
    start: function () {
        if ((window.location.hash || '').indexOf('theme=true') > 0) {
            var tab = window.location.hash.match(/tab=(\d+)/);
            this._openThemeCustomizeDialog(tab ? tab[1] : false);
            window.location.hash = '';
        }
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Instantiates and opens the theme customization dialog.
     *
     * @private
     * @param {string} tab
     * @returns {Deferred}
     */
    _openThemeCustomizeDialog: function (tab) {
        return new ThemeCustomizeDialog(this, {tab: tab}).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize');

return ThemeCustomizeDialog;
});
Example #3
0
odoo.define('website.seo', function (require) {
'use strict';

var core = require('web.core');
var Class = require('web.Class');
var Dialog = require('web.Dialog');
var mixins = require('web.mixins');
var rpc = require('web.rpc');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

// This replaces \b, because accents(e.g. à, é) are not seen as word boundaries.
// Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b
var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)';

function analyzeKeyword(htmlPage, keyword) {
    return  htmlPage.isInTitle(keyword) ? {
                title: 'label label-success',
                description: "This keyword is used in the page title",
            } : htmlPage.isInDescription(keyword) ? {
                title: 'label label-primary',
                description: "This keyword is used in the page description",
            } : htmlPage.isInBody(keyword) ? {
                title: 'label label-info',
                description: "This keyword is used in the page content."
            } : {
                title: 'label label-default',
                description: "This keyword is not used anywhere on the page."
            };
}

var Suggestion = Widget.extend({
    template: 'website.seo_suggestion',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    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',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],

    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 || weContext.get().lang.toLowerCase();
        this._rpc({
            route: '/website/seo_suggest',
            params: {
                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');
        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',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    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',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    maxKeywords: 10,

    init: function (parent, options) {
        this.htmlPage = options.page;
        this._super(parent);
    },
    start: function () {
        var self = this;
        var existingKeywords = self.htmlPage.keywords();
        if (existingKeywords.length > 0) {
            _.each(existingKeywords, function (word) {
                self.add.call(self, word);
            });
        }
    },
    keywords: function () {
        var result = [];
        this.$('.js_seo_keyword').each(function () {
            result.push($(this).data('keyword'));
        });
        return result;
    },
    isFull: function () {
        return this.keywords().length >= this.maxKeywords;
    },
    exists: function (word) {
        return _.contains(this.keywords(), word);
    },
    add: function (candidate, language) {
        var self = this;
        // TODO Refine
        var word = candidate ? candidate.replace(/[,;.:<>]+/g, ' ').replace(/ +/g, ' ').trim().toLowerCase() : '';
        if (word && !self.isFull() && !self.exists(word)) {
            var keyword = new Keyword(self, {
                word: word,
                language: language,
                page: this.htmlPage,
            });
            keyword.on('removed', self, function () {
               self.trigger('list-not-full');
               self.trigger('removed', word);
               self.trigger('content-updated', true);
            });
            keyword.on('selected', self, function (word, language) {
                self.trigger('selected', word, language);
            });
            keyword.appendTo(self.$el);
        }
        if (self.isFull()) {
            self.trigger('list-full');
        }
        self.trigger('content-updated');
    },
});

var Preview = Widget.extend({
    template: 'website.seo_preview',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],

    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',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    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 SeoConfigurator = Dialog.extend({
    template: 'website.seo_configuration',
    xmlDependencies: Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.seo.xml']
    ),
    events: {
        'keyup input[name=seo_page_keywords]': 'confirmKeyword',
        'blur input[name=seo_page_title]': 'titleChanged',
        'blur textarea[name=seo_page_description]': 'descriptionChanged',
        'click button[data-action=add]': 'addKeyword',
    },
    canEditTitle: false,
    canEditDescription: false,
    canEditKeywords: false,
    canEditLanguage: false,
    maxTitleSize: 65,
    maxDescriptionSize: 160,  // TODO master: remove me and add warning

    init: function (parent, options) {
        options = options || {};
        _.defaults(options, {
            title: _t('Promote This Page'),
            subtitle: _t('Get this page efficiently referenced in Google to attract more visitors.'),
            buttons: [
                {text: _t('Save'), classes: 'btn-primary', click: this.update},
                {text: _t('Discard'), close: true},
            ],
        });

        this._super(parent, options);
    },
    start: function () {
        var self = this;

        this.$modal.addClass('oe_seo_configuration js_seo_configuration');

        this.htmlPage = new HtmlPage();
        this.$('.js_seo_page_url').text(this.htmlPage.url());
        this.$('input[name=seo_page_title]').val(this.htmlPage.title());
        this.$('textarea[name=seo_page_description]').val(this.htmlPage.description());

        this.keywordList = new KeywordList(self, { page: this.htmlPage });
        this.keywordList.on('list-full', self, function () {
            self.$('input[name=seo_page_keywords]').attr({
                readonly: 'readonly',
                placeholder: "Remove a keyword first"
            });
            self.$('button[data-action=add]').prop('disabled', true).addClass('disabled');
        });
        this.keywordList.on('list-not-full', self, function () {
            self.$('input[name=seo_page_keywords]').removeAttr('readonly').attr('placeholder', "");
            self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled');
        });
        this.keywordList.on('selected', self, function (word, language) {
            self.keywordList.add(word, language);
        });
        this.keywordList.on('content-updated', self, function (removed) {
            self.updateTable(removed);
        });
        this.keywordList.insertAfter(this.$('.table thead'));
        this.disableUnsavableFields().then(function(){
            self.renderPreview();
        });

        this.getLanguages();
        this.updateTable();
    },
    getLanguages: function () {
        var self = this;
        this._rpc({
            model: 'website',
            method: 'get_languages',
            args: [[weContext.get().website_id]],
            context: weContext.get(),
        }).then( function (data) {
            self.$('#language-box').html(core.qweb.render('Configurator.language_promote', {
                'language': data,
                'def_lang': weContext.get().lang
            }));
        });
    },
    disableUnsavableFields: function () {
        var self = this;
        return this.loadMetaData().then(function (data) {
            //If website.page, hide the google preview & tell user his page is currently unindexed 
            self.isIndexed = (data && ('website_indexed' in data)) ? data.website_indexed : true;
            self.canEditTitle = data && ('website_meta_title' in data);
            self.canEditDescription = data && ('website_meta_description' in data);
            self.canEditKeywords = data && ('website_meta_keywords' in data);
            if (!self.canEditTitle) {
                self.$('input[name=seo_page_title]').attr('disabled', true);
            }
            if (!self.canEditDescription) {
                self.$('textarea[name=seo_page_description]').attr('disabled', true);
            }
            if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
                self.$footer.find('button[data-action=update]').attr('disabled', true);
            }
        });
    },
    suggestImprovements: function () {
        var self = this;
        var tips = [];
        _.each(tips, function (tip) {
            displayTip(tip.message, tip.type);
        });

        function displayTip(message, type) {
            new Tip(self, {
               message: message,
               type: type,
            }).appendTo(self.$('.js_seo_tips'));
        }
    },
    confirmKeyword: function (e) {
        if (e.keyCode === 13) {
            this.addKeyword();
        }
    },
    addKeyword: function (word) {
        var $input = this.$('input[name=seo_page_keywords]');
        var $language = this.$('select[name=seo_page_language]');
        var keyword = _.isString(word) ? word : $input.val();
        var language = $language.val().toLowerCase();
        this.keywordList.add(keyword, language);
        $input.val('').focus();
    },
    update: function () {
        var self = this;
        var data = {};
        if (this.canEditTitle) {
            data.website_meta_title = this.htmlPage.title();
        }
        if (this.canEditDescription) {
            data.website_meta_description = this.htmlPage.description();
        }
        if (this.canEditKeywords) {
            data.website_meta_keywords = this.keywordList.keywords().join(', ');
        }
        this.saveMetaData(data).then(function () {
           self.htmlPage.changeKeywords(self.keywordList.keywords());
           self.close();
        });
    },
    getMainObject: function () {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        if (!m) {
            return null;
        } else {
            return {
                model: m[1],
                id: m[2]|0
            };
        }
    },
    loadMetaData: function () {
        var obj = this.getMainObject();
        var def = $.Deferred();
        if (!obj) {
            // return $.Deferred().reject(new Error("No main_object was found."));
            def.resolve(null);
        } else {
            var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'];
            if (obj.model == 'website.page'){
                fields.push('website_indexed');
            }
            rpc.query({
                model: obj.model,
                method: 'read',
                args: [[obj.id], fields, weContext.get()],
            }).then(function (data) {
                if (data.length) {
                    var meta = data[0];
                    meta.model = obj.model;
                    def.resolve(meta);
                } else {
                    def.resolve(null);
                }
            }).fail(function () {
                def.reject();
            });
        }
        return def;
    },
    saveMetaData: function (data) {
        var obj = this.getMainObject();
        if (!obj) {
            return $.Deferred().reject();
        } else {
            return rpc.query({
                model: obj.model,
                method: 'write',
                args: [[obj.id], data, weContext.get()],
            });
        }
    },
    titleChanged: function () {
        var self = this;
        _.defer(function () {
            var title = self.$('input[name=seo_page_title]').val();
            self.htmlPage.changeTitle(title);
            self.renderPreview();
        });
    },
    descriptionChanged: function () {
        var self = this;
        _.defer(function () {
            var description = self.$('textarea[name=seo_page_description]').val();
            self.htmlPage.changeDescription(description);
            self.renderPreview();
        });
    },
    renderPreview: function () {
        var indexed = this.isIndexed;
        var preview = "";
        if(indexed){
            preview = new Preview(this, {
                title: this.htmlPage.title(),
                description: this.htmlPage.description(),
                url: this.htmlPage.url(),
            });
        }
        else{
            preview = new Preview(this, {
                description: _("You have hidden this page from search results. It won't be indexed by search engine"),
            });
        }
        var $preview = this.$('.js_seo_preview');
        $preview.empty();
        preview.appendTo($preview);
    },
    updateTable : function (removed) {
        var self = this,
             val = removed ? (this.$el.find('tbody > tr').length - 1) : (this.$el.find('tbody > tr').length);
        this.$('table').toggleClass('js_seo_has_content', val > 0 );
        this.$el.scrollTop(self.$el[0].scrollHeight);
    },
});

var SeoMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        'promote-current-page': '_promoteCurrentPage',
    }),

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Opens the SEO configurator dialog.
     *
     * @private
     */
    _promoteCurrentPage: function () {
        new SeoConfigurator(this).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(SeoMenu, '#promote-menu');

return {
    SeoConfigurator: SeoConfigurator,
    SeoMenu: SeoMenu,
};
});
Example #4
0
odoo.define('website.contentMenu', function (require) {
'use strict';

var core = require('web.core');
var Dialog = require('web.Dialog');
var time = require('web.time');
var weContext = require('web_editor.context');
var widget = require('web_editor.widget');
var websiteNavbarData = require('website.navbar');
var websiteRootData = require('website.WebsiteRoot');
var Widget = require('web.Widget');

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

var PagePropertiesDialog = widget.Dialog.extend({
    template: 'website.pagesMenu.page_info',
    xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.pageProperties.xml']
    ),
    events: _.extend({}, widget.Dialog.prototype.events, {
        'keyup input#page_name': '_onNameChanged',
        'keyup input#page_url': '_onUrlChanged',
        'change input#create_redirect': '_onCreateRedirectChanged',
    }),

    /**
     * @constructor
     * @override
     */
    init: function (parent, page_id, options) {
        var self = this;
        var serverUrl = window.location.origin + '/';
        var length_url = serverUrl.length;
        var serverUrlTrunc = serverUrl;
        if (length_url > 30) {
            serverUrlTrunc = serverUrl.slice(0,14) + '..' + serverUrl.slice(-14);
        }
        this.serverUrl = serverUrl;
        this.serverUrlTrunc = serverUrlTrunc;
        this.current_page_url = window.location.pathname;
        this.page_id = page_id;

        var buttons = [
            {text: _t("Save"), classes: 'btn-primary o_save_button', click: this.save},
            {text: _t("Discard"), close: true},
        ];
        if (options.fromPageManagement) {
            buttons.push({
                text: _t("Go To Page"),
                icon: 'fa-globe',
                classes: 'btn-link pull-right',
                click: function (e) {
                    window.location.href = '/' + self.page.url;
                },
            });
        }
        this._super(parent, _.extend({}, {
            title: _t("Page Properties"),
            size: 'medium',
            buttons: buttons,
        }, options || {}));
    },
    /**
     * @override
     */
    willStart: function () {
        var defs = [this._super.apply(this, arguments)];
        var self = this;
        var context = weContext.get();

        defs.push(this._rpc({
            model: 'website.page',
            method: 'get_page_info',
            args: [this.page_id, context.website_id],
            context: context,
        }).then(function (page) {
            page[0].url = _.str.startsWith(page[0].url, '/') ? page[0].url.substring(1) : page[0].url;
            self.page = page[0];
        }));

        defs.push(this._rpc({
            model: 'website.redirect',
            method: 'fields_get',
        }).then(function (fields) {
            self.fields = fields;
        }));

        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        var context = weContext.get();

        var defs = [this._super.apply(this, arguments)];

        this.$('.ask_for_redirect').addClass('hidden');
        this.$('.redirect_type').addClass('hidden');
        this.$('.warn_about_call').addClass('hidden');

        defs.push(this._getPageDependencies(this.page_id, context)
        .then(function (dependencies) {
            var dep_text = [];
            _.each(dependencies, function (value, index) {
                if (value.length > 0) {
                    dep_text.push(value.length + ' ' + index.toLowerCase());
                }
            });
            dep_text = dep_text.join(', ');
            self.$('#dependencies_redirect').html(qweb.render('website.show_page_dependencies', { dependencies: dependencies, dep_text: dep_text }));
            self.$('#dependencies_redirect [data-toggle="popover"]').popover({
                container: 'body',
            });
        }));

        defs.push(this._getSupportedMimetype(context)
        .then(function (mimetypes) {
            self.supportedMimetype = mimetypes;
        }));

        defs.push(this._getPageKeyDependencies(this.page_id, context)
        .then(function (dependencies) {
            var dep_text = [];
            _.each(dependencies, function (value, index) {
                if (value.length > 0) {
                    dep_text.push(value.length + ' ' + index.toLowerCase());
                }
            });
            dep_text = dep_text.join(', ');
            self.$('.warn_about_call').html(qweb.render('website.show_page_key_dependencies', {dependencies: dependencies, dep_text: dep_text}));
            self.$('.warn_about_call [data-toggle="popover"]').popover({
               container: 'body',
            });
        }));

        var l10n = _t.database.parameters;
        var datepickersOptions = {
            minDate: moment({y: 1900}),
            maxDate: moment().add(200, 'y'),
            calendarWeeks: true,
            icons : {
                time: 'fa fa-clock-o',
                date: 'fa fa-calendar',
                next: 'fa fa-chevron-right',
                previous: 'fa fa-chevron-left',
                up: 'fa fa-chevron-up',
                down: 'fa fa-chevron-down',
            },
            locale : moment.locale(),
            format : time.getLangDatetimeFormat(),
            widgetPositioning : {
                horizontal: 'auto',
                vertical: 'top',
            },
        };
        if (this.page.date_publish) {
            datepickersOptions.defaultDate = this.page.date_publish;
        }
        this.$('#date_publish_container').datetimepicker(datepickersOptions);

        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    destroy: function () {
        $('.popover').popover('hide');
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    save: function (data) {
        var self = this;
        var context = weContext.get();
        var url = this.$('#page_url').val();

        var date_publish = this.$('#date_publish').val();
        if (date_publish !== '') {
            date_publish = time.datetime_to_str(new Date(date_publish));
        }
        var params = {
            id: this.page.id,
            name: this.$('#page_name').val(),
            // Replace duplicate following '/' by only one '/'
            url: url.replace(/\/{2,}/g, '/'),
            is_menu: this.$('#is_menu').prop('checked'),
            is_homepage: this.$('#is_homepage').prop('checked'),
            website_published: this.$('#is_published').prop('checked'),
            create_redirect: this.$('#create_redirect').prop('checked'),
            redirect_type: this.$('#redirect_type').val(),
            website_indexed: this.$('#is_indexed').prop('checked'),
            date_publish: date_publish,
        };
        this._rpc({
            model: 'website.page',
            method: 'save_page_info',
            args: [[context.website_id], params],
            context: context,
        }).then(function () {
            // If from page manager: reload url, if from page itself: go to
            // (possibly) new url
            if (self._getMainObject().model === 'website.page') {
                window.location.href = url.toLowerCase();
            } else {
                window.location.reload(true);
            }
        });
    },

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

    /**
     * Retrieves the page URL dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_dependencies',
            args: [moID],
            context: context,
        });
    },
    /**
     * Retrieves the page's key dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageKeyDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_key_dependencies',
            args: [moID],
            context: context,
        });
    },
    /**
     * Retrieves supported mimtype
     *
     * @private
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getSupportedMimetype: function (context) {
        return this._rpc({
            model: 'website',
            method: 'guess_mimetype',
            context: context,
        });
    },
    /**
     * Returns information about the page main object.
     *
     * @private
     * @returns {Object} model and id
     */
    _getMainObject: function () {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        return {
            model: m[1],
            id: m[2] | 0,
        };
    },

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

    /**
     * @private
     */
    _onUrlChanged: function () {
        var url = this.$('input#page_url').val();
        this.$('.ask_for_redirect').toggleClass('hidden', url === this.page.url);
    },
    /**
     * @private
     */
    _onNameChanged: function () {
        var name = this.$('input#page_name').val();
        // If the file type is a supported mimetype, check if it is t-called.
        // If so, warn user. Note: different from page_search_dependencies which
        // check only for url and not key
        var ext = '.' + this.page.name.split('.').pop();
        if (ext in this.supportedMimetype && ext !== '.html') {
            this.$('.warn_about_call').toggleClass('hidden', name === this.page.name);
        }
    },
    /**
     * @private
     */
    _onCreateRedirectChanged: function () {
        var createRedirect = this.$('input#create_redirect').prop('checked');
        this.$('.redirect_type').toggleClass('hidden', !createRedirect);
    },
});

var MenuEntryDialog = widget.LinkDialog.extend({
    xmlDependencies: widget.LinkDialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),

    /**
     * @constructor
     * @override
     */
    init: function (parent, options, editor, data) {
        data.text = data.name || '';
        data.isNewWindow = data.new_window;
        this.data = data;
        this.menu_link_options = options.menu_link_options;
        this._super(parent, _.extend({}, {
            title: _t("Create Menu"),
        }, options || {}), editor, data);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        this.$('.o_link_dialog_preview').remove();
        this.$('.window-new, .link-style').closest('.form-group').remove();
        this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label"));
        if (this.menu_link_options) { // add menu link option only when adding new menu
            self.$('#o_link_dialog_url_input').closest('.form-group').hide();
            this.$('#o_link_dialog_label_input').closest('.form-group').after(qweb.render('website.contentMenu.dialog.edit.link_menu_options'));
            // remove the label that is automatically added before
            this.$('#o_link_dialog_url_input').parent().siblings().html('');
            this.$('input[name=link_menu_options]').on('change', function () {
                self.$('#o_link_dialog_url_input').closest('.form-group').toggle();
            });
        }
        this.$modal.find('.modal-lg').removeClass('modal-lg')
                   .find('.col-md-8').removeClass('col-md-8').addClass('col-xs-12');
        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    save: function () {
        var $e = this.$('#o_link_dialog_label_input');
        if (!$e.val() || !$e[0].checkValidity()) {
            $e.closest('.form-group').addClass('has-error');
            $e.focus();
            return;
        }
        if (this.$('input[name=link_menu_options]:checked').val() === 'new_page') {
            window.location = '/website/add/' + encodeURIComponent($e.val()) + '?add_menu=1';
            return;
        }
        return this._super.apply(this, arguments);
    },
});

var SelectEditMenuDialog = widget.Dialog.extend({
    template: 'website.contentMenu.dialog.select',
    xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),

    /**
     * @constructor
     * @override
     */
    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 || {}));
    },
    /**
     * @override
     */
    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',
    xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.contentMenu.xml']
    ),
    events: _.extend({}, widget.Dialog.prototype.events, {
        'click a.js_add_menu': '_onAddMenuButtonClick',
        'click button.js_delete_menu': '_onDeleteMenuButtonClick',
        'click button.js_edit_menu': '_onEditMenuButtonClick',
    }),

    /**
     * @constructor
     * @override
     */
    init: function (parent, options, rootID) {
        this._super(parent, _.extend({}, {
            title: _t("Edit Menu"),
            size: 'medium',
        }, options || {}));
        this.rootID = rootID;
    },
    /**
     * @override
     */
    willStart: function () {
        var defs = [this._super.apply(this, arguments)];
        var self = this;
        var context = weContext.get();
        defs.push(this._rpc({
            model: 'website.menu',
            method: 'get_tree',
            args: [context.website_id, this.rootID],
            context: context,
        }).then(function (menu) {
            self.menu = menu;
            self.root_menu_id = menu.id;
            self.flat = self._flatenize(menu);
            self.to_delete = [];
        }));
        return $.when.apply($, defs);
    },
    /**
     * @override
     */
    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;
    },

    //--------------------------------------------------------------------------
    // Public
    //--------------------------------------------------------------------------

    /**
     * @override
     */
    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 = weContext.get();
        // 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);
            }
        });
        this._rpc({
            model: 'website.menu',
            method: 'save',
            args: [[context.website_id], { data: data, to_delete: self.to_delete }],
            context: context,
        }).then(function () {
            return _super();
        });
    },

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

    /**
     * Returns a mapping id -> menu item containing all the menu items in the
     * given menu hierarchy.
     *
     * @private
     * @param {Object} node
     * @param {Object} [_dict] internal use: the mapping being built
     * @returns {Object}
     */
    _flatenize: function (node, _dict) {
        _dict = _dict || {};
        var self = this;
        _dict[node.id] = node;
        node.children.forEach(function (child) {
            self._flatenize(child, _dict);
        });
        return _dict;
    },

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

    /**
     * Called when the "add menu" button is clicked -> Opens the appropriate
     * dialog to edit this new menu.
     *
     * @private
     */
    _onAddMenuButtonClick: function () {
        var self = this;
        var dialog = new MenuEntryDialog(this, {menu_link_options: true}, undefined, {});
        dialog.on('save', this, function (link) {
            var new_menu = {
                id: _.uniqueId('new-'),
                name: link.text,
                url: link.url,
                new_window: link.isNewWindow,
                parent_id: false,
                sequence: 0,
                children: [],
            };
            self.flat[new_menu.id] = new_menu;
            self.$('.oe_menu_editor').append(
                qweb.render('website.contentMenu.dialog.submenu', { submenu: new_menu }));
        });
        dialog.open();
    },
    /**
     * Called when the "delete menu" button is clicked -> Deletes this menu.
     *
     * @private
     */
    _onDeleteMenuButtonClick: function (ev) {
        var $menu = $(ev.currentTarget).closest('[data-menu-id]');
        var menuID = $menu.data('menu-id')|0;
        if (menuID) {
            this.to_delete.push(menuID);
        }
        $menu.remove();
    },
    /**
     * Called when the "edit menu" button is clicked -> Opens the appropriate
     * dialog to edit this menu.
     *
     * @private
     */
    _onEditMenuButtonClick: 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 {
            Dialog.alert(null, "Could not find menu entry");
        }
    },
});

var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    xmlDependencies: ['/website/static/src/xml/website.xml'],
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        edit_menu: '_editMenu',
        page_properties: '_pageProperties',
    }),

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

    /**
     * Returns information about the page main object.
     *
     * @private
     * @returns {Object} model and id
     */
    _getMainObject: function () {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        return {
            model: m[1],
            id: m[2] | 0,
        };
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Asks the user which menu to edit if multiple menus exist on the page.
     * Then opens the menu edition dialog.
     * Then executes the given callback once the edition is saved, to finally
     * reload the page.
     *
     * @private
     * @param {function} [beforeReloadCallback]
     * @returns {Deferred}
     *          Unresolved if the menu is edited and saved as the page will be
     *          reloaded.
     *          Resolved otherwise.
     */
    _editMenu: function (beforeReloadCallback) {
        var self = this;
        var def = $.Deferred();

        // If there is multiple menu on the page, ask the user which one he
        // wants to edit
        var selectDef = $.Deferred();
        if ($('[data-content_menu_id]').length) {
            var select = new SelectEditMenuDialog(this);
            select.on('save', selectDef, selectDef.resolve);
            select.on('cancel', def, def.resolve);
            select.open();
        } else {
            selectDef.resolve(null);
        }
        selectDef.then(function (rootID) {
            // Open the dialog to show the menu structure and allow its edition
            var editDef = $.Deferred();
            var dialog = new EditMenuDialog(self, {}, rootID).open();
            dialog.on('save', editDef, editDef.resolve);
            dialog.on('cancel', def, def.resolve);
            return editDef;
        }).then(function () {
            // Before reloading the page after menu modification, does the
            // given action to do.
            return beforeReloadCallback && beforeReloadCallback();
        }).then(function () {
            // Reload the page so that the menu modification are shown
            window.location.reload(true);
        });

        return def;
    },
    /**
     * Opens the page properties dialog.
     *
     * @private
     * @returns {Deferred}
     */
    _pageProperties: function () {
        var moID = this._getMainObject().id;
        var dialog = new PagePropertiesDialog(this, moID, {}).open();
        return dialog.opened();
    },
});

var PageManagement = Widget.extend({
    xmlDependencies: ['/website/static/src/xml/website.xml'],
    events: {
        'click a.js_page_properties': '_onPagePropertiesButtonClick',
        'click a.js_clone_page': '_onClonePageButtonClick',
        'click a.js_delete_page': '_onDeletePageButtonClick',
    },

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

    /**
     * Retrieves the page dependencies for the given object id.
     *
     * @private
     * @param {integer} moID
     * @param {Object} context
     * @returns {Deferred<Array>}
     */
    _getPageDependencies: function (moID, context) {
        return this._rpc({
            model: 'website',
            method: 'page_search_dependencies',
            args: [moID],
            context: context,
        });
    },

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

    _onPagePropertiesButtonClick: function (ev) {
        var moID = $(ev.currentTarget).data('id');
        var dialog = new PagePropertiesDialog(this,moID, {'fromPageManagement': true}).open();
        return dialog;
    },
    _onClonePageButtonClick: function (ev) {
        var pageId = $(ev.currentTarget).data('id');
        var context = weContext.get();
        this._rpc({
            model: 'website.page',
            method: 'clone_page',
            args: [pageId],
            kwargs: {
                context: context,
            },
        }).then(function (path) {
            window.location.href = path;
        });
    },
    _onDeletePageButtonClick: function (ev) {
        var pageId = $(ev.currentTarget).data('id');
        var self = this;
        var context = weContext.get();

        var def = $.Deferred();
        // Search the page dependencies
        this._getPageDependencies(pageId, context)
        .then(function (dependencies) {
        // Inform the user about those dependencies and ask him confirmation
            var confirmDef = $.Deferred();
            Dialog.safeConfirm(self, "", {
                title: _t("Delete Page"),
                $content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
                confirm_callback: confirmDef.resolve.bind(confirmDef),
                cancel_callback: def.resolve.bind(self),
            });
            return confirmDef;
        }).then(function () {
        // Delete the page if the user confirmed
            return self._rpc({
                model: 'website.page',
                method: 'delete_page',
                args: [pageId],
                context: context,
            });
        }).then(function () {
            window.location.reload(true);
        }, def.reject.bind(def));
    },
});

websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu');
websiteRootData.websiteRootRegistry.add(PageManagement, '#edit_website_pages');

return {
    PagePropertiesDialog: PagePropertiesDialog,
    ContentMenu: ContentMenu,
    EditMenuDialog: EditMenuDialog,
    MenuEntryDialog: MenuEntryDialog,
    SelectEditMenuDialog: SelectEditMenuDialog,
};
});
Example #5
0
odoo.define('website.translateMenu', function (require) {
'use strict';

var utils = require('web.utils');
var TranslatorMenu = require('website.editor.menu.translate');
var websiteNavbarData = require('website.navbar');

var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    assetLibs: ['web_editor.compiled_assets_wysiwyg', 'website.compiled_assets_wysiwyg'],

    actions: _.extend({}, websiteNavbarData.WebsiteNavbar.prototype.actions || {}, {
        edit_master: '_goToMasterPage',
        translate: '_startTranslateMode',
    }),

    /**
     * @override
     */
    start: function () {
        var context;
        this.trigger_up('context_get', {
            extra: true,
            callback: function (ctx) {
                context = ctx;
            },
        });
        this._mustEditTranslations = context.edit_translations;
        if (this._mustEditTranslations) {
            var url = window.location.href.replace(/([?&])&*edit_translations[^&#]*&?/, '\$1');
            window.history.replaceState({}, null, url);

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

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Redirects the user to the same page but in the original language and in
     * edit mode.
     *
     * @private
     * @returns {Promise}
     */
    _goToMasterPage: function () {
        var lang = '/' + utils.get_cookie('frontend_lang');

        var current = document.createElement('a');
        current.href = window.location.toString();
        current.search += (current.search ? '&' : '?') + 'enable_editor=1';
        if (current.pathname.indexOf(lang) === 0) {
            current.pathname = current.pathname.replace(lang, '');
        }

        var link = document.createElement('a');
        link.href = '/website/lang/default';
        link.search += (link.search ? '&' : '?') + 'r=' + encodeURIComponent(current.pathname + current.search + current.hash);

        window.location = link.href;
        return new Promise(function () {});
    },
    /**
     * Redirects the user to the same page in translation mode (or start the
     * translator is translation mode is already enabled).
     *
     * @private
     * @returns {Promise}
     */
    _startTranslateMode: function () {
        if (!this._mustEditTranslations) {
            window.location.search += '&edit_translations';
            return new Promise(function () {});
        }

        var translator = new TranslatorMenu(this);
        return translator.prependTo(document.body);
    },
});

websiteNavbarData.websiteNavbarRegistry.add(TranslatePageMenu, '.o_menu_systray:has([data-action="translate"])');
});
Example #6
0
File: theme.js Project: 10537/odoo
odoo.define('website.theme', function (require) {
'use strict';

var ajax = require('web.ajax');
var core = require('web.core');
var session = require('web.session');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');

var QWeb = core.qweb;

var templateDef = null;

var ThemeCustomizeDialog = Widget.extend({
    template: 'website.theme_customize',
    events: {
        'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection',
        'mousedown label:has(input[data-xmlid],input[data-enable],input[data-disable])': function (event) {
            var self = this;
            this.time_select = _.defer(function () {
                var input = $(event.target).find('input').length ? $(event.target).find('input') : $(event.target).parent().find('input');
                self.on_select(input, event);
            });
        },
        'click .close': 'close',
        'click': 'click',
    },
    willStart: function () {
        if (templateDef === null) {
            templateDef = this._rpc({
                model: 'ir.ui.view',
                method: 'read_template',
                args: ['website.theme_customize', weContext.get()],
            }).then(function (data) {
                return QWeb.add_template(data);
            });
        }
        return $.when(this._super.apply(this, arguments), templateDef);
    },
    start: function () {
        var self = this;
        this.timer = null;
        this.reload = false;
        this.flag = false;
        this.active_select_tags();
        this.$inputs = this.$('input[data-xmlid],input[data-enable],input[data-disable]');
        setTimeout(function () {self.$el.addClass('in');}, 0);
        this.keydown_escape = function (event) {
            if (event.keyCode === 27) {
                self.close();
            }
        };
        $(document).on('keydown', this.keydown_escape);
        return this.load_xml_data().then(function () {
            self.flag = true;
        });
    },
    active_select_tags: function () {
        var uniqueID = 0;
        var self = this;
        var $selects = this.$('select:has(option[data-xmlid],option[data-enable],option[data-disable])');
        $selects.each(function () {
            uniqueID++;
            var $select = $(this);
            var $options = $select.find('option[data-xmlid], option[data-enable], option[data-disable]');
            $options.each(function () {
                var $option = $(this);
                var $input = $('<input 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 this._rpc({
            route: '/website/theme_customize_get',
            params: {
                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;
    },
    update_style: function (enable, disable, reload) {
        if (this.$el.hasClass('loading')) {
            return;
        }
        this.$el.addClass('loading');

        if (!reload && session.debug !== 'assets') {
            var self = this;
            return this._rpc({
                route: '/website/theme_customize',
                params: {
                    enable: enable,
                    disable: disable,
                    get_bundle: true,
                },
            }).then(function (bundleHTML) {
                var $links = $('link[href*=".assets_frontend"]');
                var $newLinks = $(bundleHTML).filter('link');

                var linksLoaded = $.Deferred();
                var nbLoaded = 0;
                $newLinks.on('load', function (e) {
                    if (++nbLoaded >= $newLinks.length) {
                        linksLoaded.resolve();
                    }
                });
                $newLinks.on('error', function (e) {
                    linksLoaded.reject();
                    window.location.hash = 'theme=true';
                    window.location.reload();
                });

                $links.last().after($newLinks);
                return linksLoaded.then(function () {
                    $links.remove();
                    self.$el.removeClass('loading');
                });
            });
        } else {
            var href = '/website/theme_customize_reload'+
                '?href='+encodeURIComponent(window.location.href)+
                '&enable='+encodeURIComponent(enable.join(','))+
                '&disable='+encodeURIComponent(disable.join(','));
            window.location.href = href;
            return $.Deferred();
        }
    },
    enable_disable: function ($inputs, enable) {
        $inputs.each(function () {
            var check = $(this).prop('checked');
            var $label = $(this).closest('label');
            $(this).prop('checked', enable);
            if (enable) $label.addClass('checked');
            else $label.removeClass('checked');
            if (check !== enable) {
                $(this).change();
            }
        });
    },
    change_selection: function (event, init_mode) {
        var self = this;
        clearTimeout(this.time_select);

        if (this.$el.hasClass('loading')) return; // prevent to change selection when css is loading

        var $option = $(event.target).is('input') ? $(event.target) : $('input', event.target),
            $options = $option,
            checked = $option.prop('checked');

        if (checked) {
            var $inputs;
            if ($option.data('enable')) {
                $inputs = this.get_inputs($option.data('enable'));
                $options = $options.add($inputs.filter(':not(:checked)'));
                this.enable_disable($inputs, true);
            }
            if ($option.data('disable')) {
                $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);
    }
});

var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        customize_theme: '_openThemeCustomizeDialog',
    }),

    /**
     * Automatically opens the theme customization dialog if the corresponding
     * hash is in the page URL.
     *
     * @override
     */
    start: function () {
        var def;
        if ((window.location.hash || '').indexOf('theme=true') > 0) {
            def = this._openThemeCustomizeDialog();
            window.location.hash = '';
        }
        return $.when(this._super.apply(this, arguments), def);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Instantiates and opens the theme customization dialog.
     *
     * @private
     * @returns {Deferred}
     */
    _openThemeCustomizeDialog: function () {
        return new ThemeCustomizeDialog(this).appendTo(document.body);
    },
});

websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize');

return ThemeCustomizeDialog;
});
Example #7
0
odoo.define('website.newMenu', function (require) {
'use strict';

var core = require('web.core');
var Dialog = require('web.Dialog');
var websiteNavbarData = require('website.navbar');
var wUtils = require('website.utils');

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

var enableFlag = 'enable_new_content';

var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        close_all_widgets: '_handleCloseDemand',
        new_page: '_createNewPage',
    }),
    events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
        'click': '_onBackgroundClick',
        'click [data-module-id]': '_onModuleIdClick',
        'keydown': '_onBackgroundKeydown',
    }),
    // allow text to be customized with inheritance
    newContentText: {
        failed: _t('Failed to install "%s"'),
        installInProgress: _t("The installation of an App is already in progress."),
        installNeeded: _t('Do you want to install the "%s" App?'),
        installPleaseWait: _t('Installing "%s"'),
    },

    /**
     * Prepare the navigation and find the modules to install.
     * Move not installed module buttons after installed modules buttons,
     * but keep the original index to be able to move back the pending install
     * button at its final position, so the user can click at the same place.
     *
     * @override
     */
    start: function () {
        this.pendingInstall = false;
        this.$newContentMenuChoices = this.$('#o_new_content_menu_choices');

        var $modules = this.$newContentMenuChoices.find('.o_new_content_element');
        _.each($modules, function (el, index) {
            var $el = $(el);
            $el.data('original-index', index);
            if ($el.data('module-id')) {
                $el.appendTo($el.parent());
                $el.find('a i, a p').addClass('text-muted');
            }
        });

        this.$firstLink = this.$newContentMenuChoices.find('a:eq(0)');
        this.$lastLink = this.$newContentMenuChoices.find('a:last');

        if ($.deparam.querystring()[enableFlag] !== undefined) {
            this._showMenu();
        }
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Asks the user information about a new page to create, then creates it and
     * redirects the user to this new page.
     *
     * @private
     * @returns {Promise} Unresolved if there is a redirection
     */
    _createNewPage: function () {
        return wUtils.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 row'})
                            .append($('<span/>', {'class': 'offset-md-3 col-md-9 text-left'})
                                    .append(qweb.render('website.components.switch', {id: 'switch_addTo_menu', label: _t("Add to menu")})));
                $add.find('input').prop('checked', true);
                $group.after($add);
            }
        }).then(function (result) {
            var val = result.val;
            var $dialog = result.dialog;
            if (!val) {
                return;
            }
            var url = '/website/add/' + encodeURIComponent(val);
            if ($dialog.find('input[type="checkbox"]').is(':checked')) url +='?add_menu=1';
            document.location = url;
            return new Promise(function () {});
        });
    },
    /**
     * @private
     */
    _handleCloseDemand: function () {
        this._hideMenu();
    },

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

    /**
     * Set the focus on the first link
     *
     * @private
     */
    _focusFirstLink: function () {
        this.$firstLink.focus();
    },
    /**
     * Set the focus on the last link
     *
     * @private
     */
    _focusLastLink: function () {
        this.$lastLink.focus();
    },
    /**
     * Hide the menu
     *
     * @private
     */
    _hideMenu: function () {
        this.$newContentMenuChoices.addClass('o_hidden');
        $('body').removeClass('o_new_content_open');
    },
    /**
     * Install a module
     *
     * @private
     * @param {number} moduleId: the module to install
     * @return {Promise}
     */
    _install: function (moduleId) {
        this.pendingInstall = true;
        $('body').css('pointer-events', 'none');
        return this._rpc({
            model: 'ir.module.module',
            method: 'button_immediate_install',
            args: [[moduleId]],
        }).guardedCatch(function () {
            $('body').css('pointer-events', '');
        });
    },
    /**
     * Show the menu
     *
     * @private
     * @returns {Promise}
     */
    _showMenu: function () {
        var self = this;
        return new Promise(function (resolve, reject) {
            self.trigger_up('action_demand', {
                actionName: 'close_all_widgets',
                onSuccess: resolve,
            });
        }).then(function () {
            self.firstTab = true;
            self.$newContentMenuChoices.removeClass('o_hidden');
            $('body').addClass('o_new_content_open');
            self.$('> a').focus();
        });
    },

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

    /**
     * Called when the menu's toggle button is clicked:
     *  -> Opens the menu and reset the tab navigation (if closed)
     *  -> Close the menu (if open)
     * Called when a click outside the menu's options occurs -> Close the menu
     *
     * @private
     * @param {Event} ev
     */
    _onBackgroundClick: function (ev) {
        if (this.$newContentMenuChoices.hasClass('o_hidden')) {
            this._showMenu();
        } else {
            this._hideMenu();
        }
    },
    /**
     * Called when a keydown occurs:
     *  ESC -> Closes the modal
     *  TAB -> Navigation (captured in the modal)
     *
     * @private
     * @param {Event} ev
     */
    _onBackgroundKeydown: function (ev) {
        switch (ev.which) {
            case $.ui.keyCode.ESCAPE:
                this._hideMenu();
                break;
            case $.ui.keyCode.TAB:
                if (ev.shiftKey) {
                    if (this.firstTab || document.activeElement === this.$firstLink[0]) {
                        this._focusLastLink();
                        ev.preventDefault();
                    }
                } else {
                    if (this.firstTab || document.activeElement === this.$lastLink[0]) {
                        this._focusFirstLink();
                        ev.preventDefault();
                    }
                }
                this.firstTab = false;
                break;
        }
    },
    /**
     * Open the install dialog related to an element:
     *  - open the dialog depending on access right and another pending install
     *  - if ok to install, prepare the install action:
     *      - call the proper action on click
     *      - change the button text and style
     *      - handle the result (reload on the same page or error)
     *
     * @private
     * @param {Event} ev
     */
    _onModuleIdClick: function (ev) {
        var self = this;
        var $el = $(ev.currentTarget);
        var $i = $el.find('a i');
        var $p = $el.find('a p');

        var title = $p.text();
        var content = '';
        var buttons;

        var moduleId = $el.data('module-id');
        var name = $el.data('module-shortdesc');

        ev.stopPropagation();
        ev.preventDefault();

        if (this.pendingInstall) {
            content = this.newContentText.installInProgress;
        } else {
            content = _.str.sprintf(this.newContentText.installNeeded, name);
            buttons = [{
                text: _t("Install"),
                classes: 'btn-primary',
                close: true,
                click: function () {
                    // move the element where it will be after installation
                    var $finalPosition = self.$newContentMenuChoices
                        .find('.o_new_content_element:not([data-module-id])')
                        .filter(function () {
                            return $(this).data('original-index') < $el.data('original-index');
                        }).last();
                    if ($finalPosition) {
                        $el.fadeTo(400, 0, function () {
                            $el.insertAfter($finalPosition);
                            // change style to use spinner
                            $i.removeClass()
                                .addClass('fa fa-spin fa-spinner fa-pulse');
                            $p.removeClass('text-muted')
                                .text(_.str.sprintf(self.newContentText.installPleaseWait, name));
                            $el.fadeTo(1000, 1);
                        });
                    }

                    self._install(moduleId).then(function () {
                        window.location.href = window.location.origin + window.location.pathname + '?' + enableFlag;
                    }, function () {
                        $i.removeClass()
                            .addClass('fa fa-exclamation-triangle');
                        $p.text(_.str.sprintf(self.newContentText.failed, name));
                    });
                }
            },{
                text: _t("Cancel"),
                close: true,
            }];
        }

        new Dialog(this, {
            title: title,
            size: 'medium',
            $content: $('<p/>', {text: content}),
            buttons: buttons
        }).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu');

return NewContentMenu;
});
Example #8
0
File: edit.js Project: Gorrice/odoo
odoo.define('website.editMenu', function (require) {
'use strict';

var core = require('web.core');
var weContext = require('web_editor.context');
var editor = require('web_editor.editor');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

/**
 * Adds the behavior when clicking on the 'edit' button (+ editor interaction)
 */
var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions, {
        edit: '_startEditMode',
        on_save: '_onSave',
    }),
    custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
        snippet_dropped: '_onSnippetDropped',
    }),

    /**
     * @constructor
     */
    init: function () {
        this._super.apply(this, arguments);
        this._editorAutoStart = (weContext.getExtra().editable && window.location.search.indexOf('enable_editor') >= 0);
    },
    /**
     * Auto-starts the editor if necessary or add the welcome message otherwise.
     *
     * @override
     */
    start: function () {
        var def = this._super.apply(this, arguments);

        // If we auto start the editor, do not show a welcome message
        if (this._editorAutoStart) {
            this._startEditMode();
            return def;
        }

        // Check that the page is empty
        var $wrap = $('#wrapwrap.homepage #wrap'); // TODO find this element another way
        if (!$wrap.length || $wrap.html().trim() !== '') {
            return def;
        }

        // If readonly empty page, show the welcome message
        this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message'));
        this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height()));
        $wrap.empty().append(this.$welcomeMessage);

        setTimeout(function () {
            if ($('.o_tooltip.o_animated').length) {
                $('.o_tooltip_container').addClass('show');
            }
        }, 1000); // ugly hack to wait that tooltip is loaded

        return def;
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Creates an editor instance and appends it to the DOM. Also remove the
     * welcome message if necessary.
     *
     * @private
     * @returns {Deferred}
     */
    _startEditMode: function () {
        var self = this;
        return (new (editor.Class)(this)).prependTo(document.body).then(function () {
            if (self.$welcomeMessage) {
                self.$welcomeMessage.remove();
            }
            var $wrapwrap = $('#wrapwrap'); // TODO find this element another way
            $wrapwrap.find('.oe_structure.oe_empty, [data-oe-type="html"]').attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE'));
            var def = $.Deferred();
            self.trigger_up('animation_start_demand', {
                editableMode: true,
                onSuccess: def.resolve.bind(def),
                onFailure: def.reject.bind(def),
            });
            return def;
        });
    },
    /**
     * On save, the editor will ask to parent widgets if something needs to be
     * done first. The website navbar will receive that demand and asks to its
     * action-capable components to do something. For example, the content menu
     * handles page-related options saving. However, some users with limited
     * access rights do not have the content menu... but the website navbar
     * expects that the save action is performed. So, this empty action is
     * defined here so that all users have an 'on_save' related action.
     *
     * @private
     * @todo improve the system to somehow declare required/optional actions
     */
    _onSave: function () {},

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

    /**
     * Called when a snippet is dropped in the page. Notifies the WebsiteRoot
     * that is should start the animations for this snippet.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onSnippetDropped: function (ev) {
        this.trigger_up('animation_start_demand', {
            editableMode: true,
            $target: ev.data.$target,
        });
    },
});

websiteNavbarData.websiteNavbarRegistry.add(EditPageMenu, '#edit-page-menu');
});
Example #9
0
File: seo.js Project: astirpe/odoo
odoo.define('website.seo', function (require) {
'use strict';

var core = require('web.core');
var Class = require('web.Class');
var Dialog = require('web.Dialog');
var mixins = require('web.mixins');
var rpc = require('web.rpc');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var weWidgets = require('web_editor.widget');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

// 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]+|^|$)';

var Suggestion = Widget.extend({
    template: 'website.seo_suggestion',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    events: {
        'click .js_seo_suggestion': 'select',
    },

    init: function (parent, options) {
        this.keyword = options.keyword;
        this._super(parent);
    },
    select: function () {
        this.trigger('selected', this.keyword);
    },
});

var SuggestionList = Widget.extend({
    template: 'website.seo_suggestion_list',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],

    init: function (parent, options) {
        this.root = options.root;
        this.language = options.language;
        this.htmlPage = options.htmlPage;
        this._super(parent);
    },
    start: function () {
        this.refresh();
    },
    refresh: function () {
        var self = this;
        self.$el.append(_t("Loading..."));
        var language = self.language || weContext.get().lang.toLowerCase();
        this._rpc({
            route: '/website/seo_suggest',
            params: {
                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(WORD_SEPARATORS_REGEX + self.root + WORD_SEPARATORS_REGEX, 'gi');
        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, {
                    keyword: keyword,
                });
                suggestion.on('selected', self, function (word, language) {
                    self.trigger('selected', word, language);
                });
                suggestion.appendTo(self.$el);
            }
        });
     },
});

var Keyword = Widget.extend({
    template: 'website.seo_keyword',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    events: {
        'click a[data-action=remove-keyword]': 'destroy',
    },

    init: function (parent, options) {
        this.keyword = options.word;
        this.language = options.language;
        this.htmlPage = options.htmlPage;
        this.used_h1 = this.htmlPage.isInHeading1(this.keyword);
        this.used_h2 = this.htmlPage.isInHeading2(this.keyword);
        this.used_content = this.htmlPage.isInBody(this.keyword);
        this._super(parent);
    },
    start: function () {
        this.$('.js_seo_keyword_suggestion').empty();
        this.suggestionList = new SuggestionList(this, {
            root: this.keyword,
            language: this.language,
            htmlPage: this.htmlPage,
        });
        this.suggestionList.on('selected', this, function (word, language) {
            this.trigger('selected', word, language);
        });
        this.suggestionList.appendTo(this.$('.js_seo_keyword_suggestion'));

        this.htmlPage.on('title-changed', this, this._updateTitle);
        this.htmlPage.on('description-changed', this, this._updateDescription);
        this._updateTitle();
        this._updateDescription();
    },
    destroy: function () {
        this.trigger('removed');
        this._super();
    },
    _updateTitle: function () {
        var $title = this.$('.js_seo_keyword_title');
        if (this.htmlPage.isInTitle(this.keyword)) {
            $title.css('visibility','visible');
        } else {
            $title.css('visibility','hidden');
        }
    },
    _updateDescription: function () {
        var $description = this.$('.js_seo_keyword_description');
        if (this.htmlPage.isInDescription(this.keyword)) {
            $description.css('visibility','visible');
        } else {
            $description.css('visibility','hidden');
        }
    },
});

var KeywordList = Widget.extend({
    template: 'website.seo_list',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    maxKeywords: 10,

    init: function (parent, options) {
        this.htmlPage = options.htmlPage;
        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,
                htmlPage: this.htmlPage,
            });
            keyword.on('removed', self, function () {
               self.trigger('list-not-full');
               self.trigger('content-updated', true);
            });
            keyword.on('selected', self, function (word, language) {
                self.trigger('selected', word, language);
            });
            keyword.appendTo(self.$el);
        }
        if (self.isFull()) {
            self.trigger('list-full');
        }
        self.trigger('content-updated');
    },
});

var Preview = Widget.extend({
    template: 'website.seo_preview',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],

    init: function (parent, options) {
        this.title = options.title;
        this.url = options.url;
        this.description = options.description || _t("The description will be generated by search engines based on page content unless you specify one.");
        if (this.description.length > 160) {
            this.description = this.description.substring(0,159) + '…';
        }
        this._super(parent);
    },
});

var HtmlPage = Class.extend(mixins.PropertiesMixin, {
    init: function () {
        mixins.PropertiesMixin.init.call(this);
        this.initTitle = this.title();
        this.initDescription = this.description();
    },
    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.trim() || this.initTitle);
        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(','));
    },
    headers: function (tag) {
        return $('#wrap '+tag).map(function () {
            return $(this).text();
        });
    },
    getOgMeta: function () {
        var ogImageUrl = $('meta[property="og:image"]').attr('content');
        var title = $('meta[property="og:title"]').attr('content');
        var description = $('meta[property="og:description"]').attr('content');
        return {
            ogImageUrl: ogImageUrl && ogImageUrl.replace(window.location.origin, ''),
            metaTitle: title,
            metaDescription: description,
        };
    },
    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('.oe_seo_configuration').text();
    },
    heading1: function () {
        return $('body').children().not('.oe_seo_configuration').find('h1').text();
    },
    heading2: function () {
        return $('body').children().not('.oe_seo_configuration').find('h2').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());
    },
    isInHeading1: function (text) {
        return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.heading1());
    },
    isInHeading2: function (text) {
        return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.heading2());
    },
});

var MetaTitleDescription = Widget.extend({
    // Form and preview for SEO meta title and meta description
    //
    // We only want to show an alert for "description too small" on those cases
    // - at init and the description is not empty
    // - we reached past the minimum and went back to it
    // - focus out of the field
    // Basically we don't want the too small alert when the field is empty and
    // we start typing on it.
    template: 'website.seo_meta_title_description',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    events: {
        'input input[name=website_meta_title]': '_titleChanged',
        'input textarea[name=website_meta_description]': '_descriptionOnInput',
        'change textarea[name=website_meta_description]': '_descriptionOnChange',
    },
    maxRecommendedDescriptionSize: 300,
    minRecommendedDescriptionSize: 50,
    showDescriptionTooSmall: false,

    /**
     * @override
     */
    init: function (parent, options) {
        this.htmlPage = options.htmlPage;
        this.canEditTitle = !!options.canEditTitle;
        this.canEditDescription = !!options.canEditDescription;
        this.isIndexed = !!options.isIndexed;
        this._super(parent, options);
    },
    /**
     * @override
     */
    start: function () {
        this.$title = this.$('input[name=website_meta_title]');
        this.$description = this.$('textarea[name=website_meta_description]');
        this.$warning = this.$('div#website_meta_description_warning');
        this.$preview = this.$('.js_seo_preview');

        this._renderPreview();

        if (!this.canEditTitle) {
            this.$title.attr('disabled', true);
        }
        if (!this.canEditDescription) {
            this.$description.attr('disabled', true);
        }

        this.$title.val(this.htmlPage.title());
        this.$description.val(this.htmlPage.description());

        this._descriptionOnChange();
    },
    /**
     * Get the current title
     */
    getTitle: function () {
        return this.$title.val().trim() || this.htmlPage.initTitle;
    },
    /**
     * Get the current description
     */
    getDescription: function () {
        return this.$description.val();
    },
    /**
     * @private
     */
    _titleChanged: function () {
        var self = this;
        self._renderPreview();
        self.trigger('title-changed');
    },
    /**
     * @private
     */
    _descriptionOnChange: function () {
        this.showDescriptionTooSmall = true;
        this._descriptionOnInput();
    },
    /**
     * @private
     */
    _descriptionOnInput: function () {
        var length = this.getDescription().length;

        if (length >= this.minRecommendedDescriptionSize) {
            this.showDescriptionTooSmall = true;
        } else if (length === 0) {
            this.showDescriptionTooSmall = false;
        }

        if (length > this.maxRecommendedDescriptionSize) {
            this.$warning.text(_t('Your description looks too long.')).show();
        } else if (this.showDescriptionTooSmall && length < this.minRecommendedDescriptionSize) {
            this.$warning.text(_t('Your description looks too short.')).show();
        } else {
            this.$warning.hide();
        }

        this._renderPreview();
        this.trigger('description-changed');
    },
    /**
     * @private
     */
    _renderPreview: function () {
        var indexed = this.isIndexed;
        var preview = "";
        if (indexed){
            preview = new Preview(this, {
                title: this.getTitle(),
                description: this.getDescription(),
                url: this.htmlPage.url(),
            });
        } else {
            preview = new Preview(this, {
                description: _t("You have hidden this page from search results. It won't be indexed by search engines."),
            });
        }
        this.$preview.empty();
        preview.appendTo(this.$preview);
    },
});

var MetaKeywords = Widget.extend({
    // Form and table for SEO meta keywords
    template: 'website.seo_meta_keywords',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    events: {
        'keyup input[name=website_meta_keywords]': '_confirmKeyword',
        'click button[data-action=add]': '_addKeyword',
    },

    init: function (parent, options) {
        this.htmlPage = options.htmlPage;
        this._super(parent, options);
    },
    start: function () {
        var self = this;
        this.$input = this.$('input[name=website_meta_keywords]');
        this.keywordList = new KeywordList(this, { htmlPage: this.htmlPage });
        this.keywordList.on('list-full', this, function () {
            self.$input.attr({
                readonly: 'readonly',
                placeholder: "Remove a keyword first"
            });
            self.$('button[data-action=add]').prop('disabled', true).addClass('disabled');
        });
        this.keywordList.on('list-not-full', this, function () {
            self.$input.removeAttr('readonly').attr('placeholder', "");
            self.$('button[data-action=add]').prop('disabled', false).removeClass('disabled');
        });
        this.keywordList.on('selected', this, function (word, language) {
            self.keywordList.add(word, language);
        });
        this.keywordList.on('content-updated', this, function (removed) {
            self._updateTable(removed);
        });
        this.keywordList.insertAfter(this.$('.table thead'));

        this._getLanguages();
        this._updateTable();
    },
    _addKeyword: function () {
        var $language = this.$('select[name=seo_page_language]');
        var keyword = this.$input.val();
        var language = $language.val().toLowerCase();
        this.keywordList.add(keyword, language);
        this.$input.val('').focus();
    },
    _confirmKeyword: function (e) {
        if (e.keyCode === 13) {
            this._addKeyword();
        }
    },
    _getLanguages: function () {
        var self = this;
        this._rpc({
            model: 'website',
            method: 'get_languages',
            args: [[weContext.get().website_id]],
            context: weContext.get(),
        }).then( function (data) {
            self.$('#language-box').html(core.qweb.render('Configurator.language_promote', {
                'language': data,
                'def_lang': weContext.get().lang
            }));
        });
    },
    /*
     * Show the table if there is at least one keyword. Hide it otherwise.
     *
     * @private
     * @param {boolean} removed: a keyword is about to be removed,
     *   we need to exclude it from the count
     */
    _updateTable : function (removed) {
        var min = removed ? 1 : 0;
        if (this.keywordList.keywords().length > min) {
            this.$('table').show();
        } else {
            this.$('table').hide();
        }
    },
});

var MetaImageSelector = Widget.extend({
    template: 'website.seo_meta_image_selector',
    xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
    events: {
        'click .o_meta_img_upload': '_onClickUploadImg',
        'click .o_meta_img': '_onClickSelectImg',
    },
    /**
     * @override
     * @param {widget} parent
     * @param {Object} data
     */
    init: function (parent, data) {
        this.metaTitle = data.title || '';
        this._setDescription(data.description);
        this.activeMetaImg = data.metaImg;
        this.serverUrl = data.htmlpage.url();
        data.pageImages.unshift(_.str.sprintf('/web/image/res.company/%s/logo', odoo.session_info.website_company_id));
        this.images = _.uniq(data.pageImages);
        this.customImgUrl = _.contains(data.pageImages, data.metaImg) ? false : data.metaImg;
        this._super(parent);
    },
    setTitle: function (title) {
        this.metaTitle = title;
        this._updateTemplateBody();
    },
    setDescription: function (description) {
        this._setDescription(description);
        this._updateTemplateBody();
    },

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

    /**
     * Set the description, applying ellipsis if too long.
     *
     * @private
    */
    _setDescription: function (description) {
        this.metaDescription = description || _t("The description will be generated by social media based on page content unless you specify one.");
        if (this.metaDescription.length > 160) {
            this.metaDescription = this.metaDescription.substring(0,159) + '…';
        }
    },

    /**
     * Update template.
     *
     * @private
    */
    _updateTemplateBody: function () {
        this.$el.empty();
        this.images = _.uniq(this.images);
        this.$el.append(core.qweb.render('website.og_image_body', {widget: this}));
    },

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

    /**
     * Called when a select image from list -> change the preview accordingly.
     *
     * @private
     * @param {MouseEvent} ev
     */
    _onClickSelectImg: function (ev) {
        var $img = $(ev.currentTarget);
        this.activeMetaImg = $img.find('img').attr('src');
        this._updateTemplateBody();
    },
    /**
     * Open a mediaDialog to select/upload image.
     *
     * @private
     * @param {MouseEvent} ev
     */
    _onClickUploadImg: function (ev) {
        var self = this;
        var $image = $('<img/>');
        var mediaDialog = new weWidgets.MediaDialog(this, {
            onlyImages: true,
            res_model: 'ir.ui.view',
        }, null, $image);
        mediaDialog.open();
        mediaDialog.on('save', this, function (image) {
            var src = image.attr('src');
            self.activeMetaImg = src;
            self.customImgUrl = src;
            self._updateTemplateBody();
        });
    },
});

var SeoConfigurator = Dialog.extend({
    template: 'website.seo_configuration',
    xmlDependencies: Dialog.prototype.xmlDependencies.concat(
        ['/website/static/src/xml/website.seo.xml']
    ),
    canEditTitle: false,
    canEditDescription: false,
    canEditKeywords: false,
    canEditLanguage: false,

    init: function (parent, options) {
        options = options || {};
        _.defaults(options, {
            title: _t('Optimize SEO'),
            buttons: [
                {text: _t('Save'), classes: 'btn-primary', click: this.update},
                {text: _t('Discard'), close: true},
            ],
        });

        this._super(parent, options);
    },
    start: function () {
        var self = this;

        this.$modal.addClass('oe_seo_configuration');

        this.htmlPage = new HtmlPage();

        this.disableUnsavableFields().then(function () {
            // Image selector
            self.metaImageSelector = new MetaImageSelector(self, {
                htmlpage: self.htmlPage,
                title: self.htmlPage.getOgMeta().metaTitle,
                description: self.htmlPage.getOgMeta().metaDescription,
                metaImg : self.metaImg || self.htmlPage.getOgMeta().ogImageUrl,
                pageImages : _.pluck(self.htmlPage.images().get(), 'src'),
            });
            self.metaImageSelector.appendTo(self.$('.js_seo_image'));

            // title and description
            self.metaTitleDescription = new MetaTitleDescription(self, {
                htmlPage: self.htmlPage,
                canEditTitle: self.canEditTitle,
                canEditDescription: self.canEditDescription,
                isIndexed: self.isIndexed,
            });
            self.metaTitleDescription.on('title-changed', self, self.titleChanged);
            self.metaTitleDescription.on('description-changed', self, self.descriptionChanged);
            self.metaTitleDescription.appendTo(self.$('.js_seo_meta_title_description'));

            // keywords
            self.metaKeywords = new MetaKeywords(self, {htmlPage: self.htmlPage});
            self.metaKeywords.appendTo(self.$('.js_seo_meta_keywords'));
        });
    },
    /*
     * Reset meta tags to their initial value if not saved.
     *
     * @private
     */
    destroy: function () {
        if (!this.savedData) {
            this.htmlPage.changeTitle(this.htmlPage.initTitle);
            this.htmlPage.changeDescription(this.htmlPage.initDescription);
        }
        this._super.apply(this, arguments);
    },
    disableUnsavableFields: function () {
        var self = this;
        return this.loadMetaData().then(function (data) {
            // We only need a reload for COW when the copy is happening, therefore:
            // - no reload if we are not editing a view (condition: website_id === undefined)
            // - reload if generic page (condition: website_id === false)
            self.reloadOnSave = data.website_id === undefined ? false : !data.website_id;
            //If website.page, hide the google preview & tell user his page is currently unindexed 
            self.isIndexed = (data && ('website_indexed' in data)) ? data.website_indexed : true;
            self.canEditTitle = data && ('website_meta_title' in data);
            self.canEditDescription = data && ('website_meta_description' in data);
            self.canEditKeywords = data && ('website_meta_keywords' in data);
            self.metaImg = data.website_meta_og_img;
            if (!self.canEditTitle && !self.canEditDescription && !self.canEditKeywords) {
                // disable the button to prevent an error if the current page doesn't use the mixin
                // we make the check here instead of on the view because we don't need to check
                // at every page load, just when the rare case someone clicks on this link
                // TODO don't show the modal but just an alert in this case
                self.$footer.find('button[data-action=update]').attr('disabled', true);
            }
        });
    },
    update: function () {
        var self = this;
        var data = {};
        if (this.canEditTitle) {
            data.website_meta_title = this.htmlPage.title();
        }
        if (this.canEditDescription) {
            data.website_meta_description = this.htmlPage.description();
        }
        if (this.canEditKeywords) {
            data.website_meta_keywords = this.metaKeywords.keywordList.keywords().join(', ');
        }
        data.website_meta_og_img = this.metaImageSelector.activeMetaImg;
        this.saveMetaData(data).then(function () {
            // We want to reload if we are editing a generic page
            // because it will become a specific page after this change (COW)
            // and we want the user to be on the page he just created.
            if (self.reloadOnSave) {
                window.location.href = self.htmlPage.url();
            } else {
                self.htmlPage.changeKeywords(self.metaKeywords.keywordList.keywords());
                self.savedData = true;
                self.close();
            }
        });
    },
    getMainObject: function () {
        var repr = $('html').data('main-object');
        var m = repr.match(/(.+)\((\d+),(.*)\)/);
        if (!m) {
            return null;
        } else {
            return {
                model: m[1],
                id: m[2]|0
            };
        }
    },
    loadMetaData: function () {
        var obj = this.getMainObject();
        var def = $.Deferred();
        if (!obj) {
            // return $.Deferred().reject(new Error("No main_object was found."));
            def.resolve(null);
        } else {
            var fields = ['website_meta_title', 'website_meta_description', 'website_meta_keywords'
                            ,'website_meta_og_img'];
            if (obj.model === 'website.page'){
                fields.push('website_indexed');
                fields.push('website_id');
            }
            rpc.query({
                model: obj.model,
                method: 'read',
                args: [[obj.id], fields, weContext.get()],
            }).then(function (data) {
                if (data.length) {
                    var meta = data[0];
                    meta.model = obj.model;
                    def.resolve(meta);
                } else {
                    def.resolve(null);
                }
            }).fail(function () {
                def.reject();
            });
        }
        return def;
    },
    saveMetaData: function (data) {
        var obj = this.getMainObject();
        if (!obj) {
            return $.Deferred().reject();
        } else {
            return rpc.query({
                model: obj.model,
                method: 'write',
                args: [[obj.id], data, weContext.get()],
            });
        }
    },
    titleChanged: function () {
        var self = this;
        _.defer(function () {
            var title = self.metaTitleDescription.getTitle();
            self.htmlPage.changeTitle(title);
            self.metaImageSelector.setTitle(title);
        });
    },
    descriptionChanged: function () {
        var self = this;
        _.defer(function () {
            var description = self.metaTitleDescription.getDescription();
            self.htmlPage.changeDescription(description);
            self.metaImageSelector.setDescription(description);
        });
    },
});

var SeoMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        'promote-current-page': '_promoteCurrentPage',
    }),

    init: function (parent, options) {
        this._super(parent, options);

        if (window.location.href.includes('enable_seo')) {
            this._promoteCurrentPage();
        }
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Opens the SEO configurator dialog.
     *
     * @private
     */
    _promoteCurrentPage: function () {
        new SeoConfigurator(this).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(SeoMenu, '#promote-menu');

return {
    SeoConfigurator: SeoConfigurator,
    SeoMenu: SeoMenu,
};
});
Example #10
0
odoo.define('website.customizeMenu', function (require) {
'use strict';

var core = require('web.core');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');
var WebsiteAceEditor = require('website.ace');

var qweb = core.qweb;

var CustomizeMenu = Widget.extend({
    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
    events: {
        'show.bs.dropdown': '_onDropdownShow',
        'click .dropdown-item[data-view-id]': '_onCustomizeOptionClick',
    },

    /**
     * @override
     */
    start: function () {
        this.viewName = $(document.documentElement).data('view-xmlid');
        if (!this.viewName) {
            _.defer(this.destroy.bind(this));
        }

        if (this.$el.is('.show')) {
            this._loadCustomizeOptions();
        }
    },

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

    /**
     * Enables/Disables a view customization whose id is given.
     *
     * @private
     * @param {integer} viewID
     * @returns {Deferred}
     *          Unresolved if the customization succeeded as the page will be
     *          reloaded.
     *          Rejected otherwise.
     */
    _doCustomize: function (viewID) {
        return this._rpc({
            model: 'ir.ui.view',
            method: 'toggle',
            args: [[viewID]],
            context: weContext.get(),
        }).then(function () {
            window.location.reload();
            return $.Deferred();
        });
    },
    /**
     * Loads the information about the views which can be enabled/disabled on
     * the current page and shows them as switchable elements in the menu.
     *
     * @private
     * @return {Deferred}
     */
    _loadCustomizeOptions: function () {
        if (this.__customizeOptionsLoaded) {
            return $.when();
        }
        this.__customizeOptionsLoaded = true;

        var $menu = this.$el.children('.dropdown-menu');
        return this._rpc({
            route: '/website/get_switchable_related_views',
            params: {
                key: this.viewName,
            },
        }).then(function (result) {
            var currentGroup = '';
            _.each(result, function (item) {
                if (currentGroup !== item.inherit_id[1]) {
                    currentGroup = item.inherit_id[1];
                    $menu.append('<li class="dropdown-header">' + currentGroup + '</li>');
                }
                var $a = $('<a/>', {href: '#', class: 'dropdown-item', 'data-view-id': item.id, role: 'menuitem'})
                            .append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name}));
                $a.find('input').prop('checked', !!item.active);
                $menu.append($a);
            });
        });
    },

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

    /**
     * Called when a view's related switchable element is clicked -> enable /
     * disable the related view.
     *
     * @private
     * @param {Event} ev
     */
    _onCustomizeOptionClick: function (ev) {
        ev.preventDefault();
        var viewID = parseInt($(ev.currentTarget).data('view-id'), 10);
        this._doCustomize(viewID);
    },
    /**
     * @private
     */
    _onDropdownShow: function () {
        this._loadCustomizeOptions();
    },
});

var AceEditorMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        edit: '_enterEditMode',
        ace: '_launchAce',
    }),

    /**
     * Launches the ace editor automatically when the corresponding hash is in
     * the page URL.
     *
     * @override
     */
    start: function () {
        var def;
        if (window.location.hash.substr(0, WebsiteAceEditor.prototype.hash.length) === WebsiteAceEditor.prototype.hash) {
            def = this._launchAce();
        }
        return $.when(this._super.apply(this, arguments), def);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * When handling the "edit" website action, the ace editor has to be closed.
     *
     * @private
     */
    _enterEditMode: function () {
        if (this.globalEditor) {
            this.globalEditor.do_hide();
        }
    },
    /**
     * Launches the ace editor to be able to edit the templates and scss files
     * which are used by the current page.
     *
     * @private
     * @returns {Deferred}
     */
    _launchAce: function () {
        if (this.globalEditor) {
            this.globalEditor.do_show();
            return $.when();
        } else {
            var currentHash = window.location.hash;
            var indexOfView = currentHash.indexOf("?res=");
            var initialResID = undefined;
            if (indexOfView >= 0) {
                initialResID = currentHash.substr(indexOfView + ("?res=".length));
                var parsedResID = parseInt(initialResID, 10);
                if (parsedResID) {
                    initialResID = parsedResID;
                }
            }

            this.globalEditor = new WebsiteAceEditor(this, $(document.documentElement).data('view-xmlid'), {
                initialResID: initialResID,
                defaultBundlesRestriction: [
                    "web.assets_frontend",
                    "website.assets_frontend",
                ],
            });
            return this.globalEditor.appendTo(document.body);
        }
    },
});

websiteNavbarData.websiteNavbarRegistry.add(CustomizeMenu, '#customize-menu');
websiteNavbarData.websiteNavbarRegistry.add(AceEditorMenu, '#html_editor');

return CustomizeMenu;
});
Example #11
0
odoo.define('website.editMenu', function (require) {
'use strict';

var core = require('web.core');
var EditorMenu = require('website.editor.menu');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

/**
 * Adds the behavior when clicking on the 'edit' button (+ editor interaction)
 */
var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    assetLibs: ['web_editor.compiled_assets_wysiwyg', 'website.compiled_assets_wysiwyg'],

    xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions, {
        edit: '_startEditMode',
        on_save: '_onSave',
    }),
    custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
        content_will_be_destroyed: '_onContentWillBeDestroyed',
        content_was_recreated: '_onContentWasRecreated',
        snippet_cloned: '_onSnippetCloned',
        snippet_dropped: '_onSnippetDropped',
        edition_will_stopped: '_onEditionWillStop',
        edition_was_stopped: '_onEditionWasStopped',
    }),

    /**
     * @constructor
     */
    init: function () {
        this._super.apply(this, arguments);
        var context;
        this.trigger_up('context_get', {
            extra: true,
            callback: function (ctx) {
                context = ctx;
            },
        });
        this._editorAutoStart = (context.editable && window.location.search.indexOf('enable_editor') >= 0);
        var url = window.location.href.replace(/([?&])&*enable_editor[^&#]*&?/, '\$1');
        window.history.replaceState({}, null, url);
    },
    /**
     * Auto-starts the editor if necessary or add the welcome message otherwise.
     *
     * @override
     */
    start: function () {
        var def = this._super.apply(this, arguments);

        // If we auto start the editor, do not show a welcome message
        if (this._editorAutoStart) {
            return $.when(def, this._startEditMode());
        }

        // Check that the page is empty
        var $wrap = this._targetForEdition().filter('#wrapwrap.homepage').find('#wrap');

        if ($wrap.length && $wrap.html().trim() === '') {
            // If readonly empty page, show the welcome message
            this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message'));
            this.$welcomeMessage.addClass('o_homepage_editor_welcome_message');
            this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height()));
            $wrap.empty().append(this.$welcomeMessage);
        }

        setTimeout(function () {
            if ($('.o_tooltip.o_animated').length) {
                $('.o_tooltip_container').addClass('show');
            }
        }, 1000); // ugly hack to wait that tooltip is loaded

        return def;
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Creates an editor instance and appends it to the DOM. Also remove the
     * welcome message if necessary.
     *
     * @private
     * @returns {Deferred}
     */
    _startEditMode: function () {
        var self = this;
        if (this.editModeEnable) {
            return;
        }
        this.trigger_up('animation_stop_demand', {
            $target: this._targetForEdition(),
        });
        var $welcomeMessageParent = null;
        if (this.$welcomeMessage) {
            $welcomeMessageParent = this.$welcomeMessage.parent();
            this.$welcomeMessage.detach(); // detach from the readonly rendering before the clone by summernote
        }
        this.editModeEnable = true;
        return new EditorMenu(this).prependTo(document.body).then(function () {
            if (self.$welcomeMessage) {
                $welcomeMessageParent.append(self.$welcomeMessage); // reappend if the user cancel the edition
            }

            var $target = self._targetForEdition();
            self.$editorMessageElements = $target
                .find('.oe_structure.oe_empty, [data-oe-type="html"]')
                .not('[data-editor-message]')
                .attr('data-editor-message', _t('DRAG BUILDING BLOCKS HERE'));
            var def = $.Deferred();
            self.trigger_up('animation_start_demand', {
                editableMode: true,
                onSuccess: def.resolve.bind(def),
                onFailure: def.reject.bind(def),
            });
            return def;
        });
    },
    /**
     * On save, the editor will ask to parent widgets if something needs to be
     * done first. The website navbar will receive that demand and asks to its
     * action-capable components to do something. For example, the content menu
     * handles page-related options saving. However, some users with limited
     * access rights do not have the content menu... but the website navbar
     * expects that the save action is performed. So, this empty action is
     * defined here so that all users have an 'on_save' related action.
     *
     * @private
     * @todo improve the system to somehow declare required/optional actions
     */
    _onSave: function () {},

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

    /**
     * Returns the target for edition.
     *
     * @private
     * @returns {JQuery}
     */
    _targetForEdition: function () {
        return $('#wrapwrap'); // TODO should know about this element another way
    },

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

    /**
     * Called when content will be destroyed in the page. Notifies the
     * WebsiteRoot that is should stop the animations.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onContentWillBeDestroyed: function (ev) {
        this.trigger_up('animation_stop_demand', {
            $target: ev.data.$target,
        });
    },
    /**
     * Called when content was recreated in the page. Notifies the
     * WebsiteRoot that is should start the animations.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onContentWasRecreated: function (ev) {
        this.trigger_up('animation_start_demand', {
            editableMode: true,
            $target: ev.data.$target,
        });
    },
    /**
     * Called when edition will stop. Notifies the
     * WebsiteRoot that is should stop the animations.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onEditionWillStop: function (ev) {
        this.$editorMessageElements.removeAttr('data-editor-message');
        this.trigger_up('animation_stop_demand', {
            $target: this._targetForEdition(),
        });
    },
    /**
     * Called when edition was stopped. Notifies the
     * WebsiteRoot that is should start the animations.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onEditionWasStopped: function (ev) {
        this.trigger_up('animation_start_demand', {
            $target: this._targetForEdition(),
        });
        this.editModeEnable = false;
    },
    /**
     * Called when a snippet is cloned in the page. Notifies the WebsiteRoot
     * that is should start the animations for this snippet.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onSnippetCloned: function (ev) {
        this.trigger_up('animation_start_demand', {
            editableMode: true,
            $target: ev.data.$target,
        });
    },
    /**
     * Called when a snippet is dropped in the page. Notifies the WebsiteRoot
     * that is should start the animations for this snippet.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onSnippetDropped: function (ev) {
        this.trigger_up('animation_start_demand', {
            editableMode: true,
            $target: ev.data.$target,
        });
    },
});

websiteNavbarData.websiteNavbarRegistry.add(EditPageMenu, '#edit-page-menu');
});
Example #12
0
odoo.define('website.theme', function (require) {
'use strict';

var config = require('web.config');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var weWidgets = require('wysiwyg.widgets');
var ColorpickerDialog = require('wysiwyg.widgets.ColorpickerDialog');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

var templateDef = null;

var QuickEdit = Widget.extend({
    xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
    template: 'website.theme_customize_active_input',
    events: {
        'keydown input': '_onInputKeydown',
        'click .btn-primary': '_onSaveClick',
        'click .btn-secondary': '_onResetClick',
    },

    /**
     * @constructor
     */
    init: function (parent, value, unit) {
        this._super.apply(this, arguments);
        this.value = value;
        this.unit = unit;
    },
    /**
     * @override
     */
    start: function () {
        this.$input = this.$('input');
        this.$input.select();
        return this._super.apply(this, arguments);
    },

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

    /**
     * @private
     * @param {string} [value]
     */
    _save: function (value) {
        if (value === undefined) {
            value = parseFloat(this.$input.val());
            value = isNaN(value) ? 'null' : (value + this.unit);
        }
        this.trigger_up('QuickEdit:save', {
            value: value,
        });
        this.destroy();
    },

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

    /**
     * @private
     * @param {Event} ev
     */
    _onInputKeydown: function (ev) {
        var inputValue = this.$input.val();
        var value = 0;
        if (inputValue !== '') {
            value = parseFloat(this.$input.val());
            if (isNaN(value)) {
                return;
            }
        }
        switch (ev.which) {
            case $.ui.keyCode.UP:
                this.$input.val(value + 1);
                break;
            case $.ui.keyCode.DOWN:
                this.$input.val(value - 1);
                break;
            case $.ui.keyCode.ENTER:
                // Do not listen to change events, we want the user to be able
                // to confirm in all cases.
                this._save();
                break;
        }
    },
    /**
     * @private
     */
    _onSaveClick: function () {
        this._save();
    },
    /**
     * @private
     */
    _onResetClick: function () {
        this._save('null');
    },
});

var ThemeCustomizeDialog = Dialog.extend({
    xmlDependencies: (Dialog.prototype.xmlDependencies || [])
        .concat(['/website/static/src/xml/website.editor.xml']),

    template: 'website.theme_customize',
    events: {
        'change .o_theme_customize_option_input': '_onChange',
        'click .checked .o_theme_customize_option_input[type="radio"]': '_onChange',
    },

    CUSTOM_BODY_IMAGE_XML_ID: 'option_custom_body_image',

    /**
     * @constructor
     */
    init: function (parent, options) {
        options = options || {};
        this._super(parent, _.extend({
            title: _t("Customize Theme"),
            buttons: [],
        }, options));

        this.defaultTab = options.tab || 0;
    },
    /**
     * @override
     */
    willStart: function () {
        if (templateDef === null) {
            templateDef = this._rpc({
                model: 'ir.ui.view',
                method: 'read_template',
                args: ['website.theme_customize'],
            }).then(function (data) {
                if (!/^<templates>/.test(data)) {
                    data = _.str.sprintf('<templates>%s</templates>', data);
                }
                return core.qweb.add_template(data);
            });
        }
        return $.when(this._super.apply(this, arguments), templateDef);
    },
    /**
     * @override
     */
    start: function () {
        var self = this;

        this.PX_BY_REM = parseFloat($(document.documentElement).css('font-size'));

        this.$modal.addClass('o_theme_customize_modal');

        this.style = window.getComputedStyle(document.documentElement);

        var $tabs;
        var loadDef = this._loadViews().then(function (data) {
            self._generateDialogHTML(data);
            $tabs = self.$('[data-toggle="tab"]');

            // Hide the tab navigation if only one tab
            if ($tabs.length <= 1) {
                $tabs.closest('.nav').addClass('d-none');
            }
        });

        // Enable the first option tab or the given default tab
        this.opened().then(function () {
            $tabs.eq(self.defaultTab).tab('show');

            // Hack to hide primary/secondary if they are equal to alpha/beta
            // (this is the case with default values but not in some themes).
            var $primary = self.$('.o_theme_customize_color[data-color="primary"]');
            var $alpha = self.$('.o_theme_customize_color[data-color="alpha"]');
            var $secondary = self.$('.o_theme_customize_color[data-color="secondary"]');
            var $beta = self.$('.o_theme_customize_color[data-color="beta"]');

            var sameAlphaPrimary = $primary.css('background-color') === $alpha.css('background-color');
            var sameBetaSecondary = $secondary.css('background-color') === $beta.css('background-color');

            if (!sameAlphaPrimary) {
                $alpha.prev().text(_t("Extra Color"));
            }
            if (!sameBetaSecondary) {
                $beta.prev().text(_t("Extra Color"));
            }

            $primary = $primary.closest('.o_theme_customize_option');
            $alpha = $alpha.closest('.o_theme_customize_option');
            $secondary = $secondary.closest('.o_theme_customize_option');
            $beta = $beta.closest('.o_theme_customize_option');

            $primary.toggleClass('d-none', sameAlphaPrimary);
            $secondary.toggleClass('d-none', sameBetaSecondary);

            if (!sameAlphaPrimary && sameBetaSecondary) {
                $beta.insertBefore($alpha);
            } else if (sameAlphaPrimary && !sameBetaSecondary) {
                $secondary.insertAfter($alpha);
            }
        });

        return $.when(this._super.apply(this, arguments), loadDef);
    },

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

    /**
     * @private
     */
    _chooseBodyCustomImage: function () {
        var self = this;
        var def = $.Deferred();
        var $image = $('<img/>');
        var editor = new weWidgets.MediaDialog(this, {
            onlyImages: true,
            firstFilters: ['background'],
        }, $image[0]);

        editor.on('save', this, function (media) { // TODO use scss customization instead (like for user colors)
            var src = $(media).attr('src');
            self._rpc({
                model: 'ir.model.data',
                method: 'get_object_reference',
                args: ['website', this.CUSTOM_BODY_IMAGE_XML_ID],
            }).then(function (data) {
                return self._rpc({
                    model: 'ir.ui.view',
                    method: 'save',
                    args: [
                        data[1],
                        '#wrapwrap { background-image: url("' + src + '"); }',
                        '//style',
                    ],
                });
            }).always(def.resolve.bind(def));
        });
        editor.on('cancel', this, function () {
            def.resolve();
        });

        editor.open();

        return def;
    },
    /**
     * @private
     * @param {Object} data - @see this._loadViews
     */
    _generateDialogHTML: function (data) {
        var self = this;
        var $contents = this.$el.children('content');
        if ($contents.length === 0) {
            return;
        }

        $contents.remove();
        this.$el.append(core.qweb.render('website.theme_customize_modal_layout'));
        var $navLinksContainer = this.$('.nav');
        var $navContents = this.$('.tab-content');

        _.each($contents, function (content) {
            var $content = $(content);

            var contentID = _.uniqueId('content-');

            // Build the nav tab for the content
            $navLinksContainer.append($('<li/>', {
                class: 'nav-item mb-1',
            }).append($('<a/>', {
                href: '#' + contentID,
                class: 'nav-link',
                'data-toggle': 'tab',
                text: $content.attr('string'),
            })));

            // Build the tab pane for the content
            var $navContent = $(core.qweb.render('website.theme_customize_modal_content', {
                id: contentID,
                title: $content.attr('title'),
            }));
            $navContents.append($navContent);
            var $optionsContainer = $navContent.find('.o_options_container');

            // Process content items
            _processItems($content.children(), $optionsContainer);
        });

        this.$('[title]').tooltip();

        this.$inputs = self.$('.o_theme_customize_option_input');
        // Enable data-xmlid="" inputs if none of their neighbors were enabled
        _.each(this.$inputs.filter('[data-xmlid=""]'), function (input) {
            var $input = $(input);
            var $neighbors = self.$inputs.filter('[name="' + $input.attr('name') + '"]').not($input);
            if ($neighbors.length && !$neighbors.filter(':checked').length) {
                $input.prop('checked', true);
            }
        });
        this._setActive();
        this._updateValues();

        function _processItems($items, $container) {
            var optionsName = _.uniqueId('option-');
            var alone = ($items.length === 1);

            _.each($items, function (item) {
                var $item = $(item);
                var $col;

                switch (item.tagName) {
                    case 'OPT':
                        var widgetName = $item.data('widget');

                        var xmlid = $item.data('xmlid');

                        var renderingOptions = _.extend({
                            string: $item.attr('string') || data.names[xmlid.split(',')[0].trim()],
                            icon: $item.data('icon'),
                            font: $item.data('font'),
                        }, $item.data());

                        // Build the options template
                        var $option = $(core.qweb.render('website.theme_customize_modal_option', _.extend({
                            alone: alone,
                            name: xmlid === undefined ? _.uniqueId('option-') : optionsName,
                            id: $item.attr('id') || _.uniqueId('o_theme_customize_input_id_'),
                            checked: xmlid === undefined || xmlid && (!_.difference(self._getXMLIDs($item), data.enabled).length),
                            widget: widgetName,
                        }, renderingOptions)));
                        $option.find('input')
                            .addClass('o_theme_customize_option_input')
                            .attr({
                                'data-xmlid': xmlid,
                                'data-enable': $item.data('enable'),
                                'data-disable': $item.data('disable'),
                                'data-reload': $item.data('reload'),
                            });

                        if (widgetName) {
                            var $widget = $(core.qweb.render('website.theme_customize_widget_' + widgetName, renderingOptions));
                            $option.find('label').append($widget);
                        }

                        var $final;
                        if ($container.hasClass('form-row')) {
                            $final = $('<div/>', {
                                class: _.str.sprintf('col-%s', $item.data('col') || 6),
                            });
                            $final.append($option);
                        } else {
                            $final = $option;
                        }
                        $final.attr('data-depends', $item.data('depends'));
                        $container.append($final);
                        break;

                    case 'LIST':
                        var $listContainer = $('<div/>', {class: 'py-1 px-2 o_theme_customize_option_list'});
                        $col = $('<div/>', {
                            class: _.str.sprintf('col-%s mt-2', $item.data('col') || 6),
                            'data-depends': $item.data('depends'),
                        }).append($('<h6/>', {text: $item.attr('string')}), $listContainer);
                        $container.append($col);
                        _processItems($item.children(), $listContainer);
                        break;

                    default:
                        _processItems($item.children(), $container);
                        break;
                }
            });
        }
    },
    /**
     * @private
     */
    _loadViews: function () {
        return this._rpc({
            route: '/website/theme_customize_get',
            params: {
                'xml_ids': this._getXMLIDs(this.$inputs || this.$('[data-xmlid]')),
            },
        });
    },
    /**
     * @private
     */
    _getInputs: function (string) {
        if (!string) {
            return $();
        }
        return this.$inputs.filter('#' + string.replace(/\s*,\s*/g, ', #'));
    },
    /**
     * @private
     */
    _getXMLIDs: function ($inputs) {
        var xmlIDs = [];
        _.each($inputs, function (input) {
            var $input = $(input);
            var xmlID = $input.data('xmlid');
            if (xmlID) {
                xmlIDs = xmlIDs.concat(xmlID.split(/\s*,\s*/));
            }
        });
        return xmlIDs;
    },
    /**
     * @private
     */
    _makeSCSSCusto: function (url, values) {
        return this._rpc({
            route: '/website/make_scss_custo',
            params: {
                'url': url,
                'values': values,
            },
        });
    },
    /**
     * @private
     */
    _pickColor: function (colorElement) {
        var self = this;
        var $color = $(colorElement);
        var colorName = $color.data('color');
        var colorType = $color.data('colorType');

        var def = $.Deferred();

        var colorpicker = new ColorpickerDialog(this, {
            defaultColor: $color.css('background-color'),
        });
        var chosenColor = undefined;
        colorpicker.on('colorpicker:saved', this, function (ev) {
            ev.stopPropagation();
            chosenColor = ev.data.cssColor;
        });
        colorpicker.on('closed', this, function (ev) {
            if (chosenColor === undefined) {
                def.resolve();
                return;
            }

            var baseURL = '/website/static/src/scss/options/colors/';
            var url = _.str.sprintf('%suser_%scolor_palette.scss', baseURL, (colorType ? (colorType + '_') : ''));

            var colors = {};
            colors[colorName] = chosenColor;
            if (colorName === 'alpha') {
                colors['beta'] = 'null';
                colors['gamma'] = 'null';
                colors['delta'] = 'null';
                colors['epsilon'] = 'null';
            }

            self._makeSCSSCusto(url, colors).always(def.resolve.bind(def));
        });
        colorpicker.open();

        return def;
    },
    /**
     * @private
     */
    _processChange: function ($inputs) {
        var self = this;
        var defs = [];

        var $options = $inputs.closest('.o_theme_customize_option');

        // Handle body image changes
        var $bodyImageInputs = $inputs.filter('[data-xmlid*="website.' + this.CUSTOM_BODY_IMAGE_XML_ID + '"]:checked');
        defs = defs.concat(_.map($bodyImageInputs, function () {
            return self._chooseBodyCustomImage();
        }));

        // Handle color changes
        var $colors = $options.find('.o_theme_customize_color');
        defs = defs.concat(_.map($colors, function (colorElement) {
            return self._pickColor($(colorElement));
        }));

        // Handle input changes
        var $inputsData = $options.find('.o_theme_customize_input');
        defs = defs.concat(_.map($inputsData, function (inputData, i) {
            return self._quickEdit($(inputData));
        }));

        return $.when.apply($, defs);
    },
    /**
     * @private
     */
    _quickEdit: function ($inputData) {
        var text = $inputData.text().trim();
        var value = parseFloat(text) || '';
        var unit = text.match(/([^\s\d]+)$/)[1];

        var def = $.Deferred();
        var qEdit = new QuickEdit(this, value, unit);
        qEdit.on('QuickEdit:save', this, function (ev) {
            ev.stopPropagation();

            var value = ev.data.value;
            // Convert back to rem if needed
            if ($inputData.data('unit') === 'rem' && unit === 'px' && value !== 'null') {
                value = parseFloat(value) / this.PX_BY_REM + 'rem';
            }

            var values = {};
            values[$inputData.data('value')] = value;
            this._makeSCSSCusto('/website/static/src/scss/options/user_values.scss', values)
                .always(def.resolve.bind(def));
        });
        qEdit.appendTo($inputData.closest('.o_theme_customize_option'));
        return def;
    },
    /**
     * @private
     */
    _setActive: function () {
        var self = this;

        // Look at all options to see if they are enabled or disabled
        var $enable = this.$inputs.filter(':checked');

        // Mark the labels as checked accordingly
        this.$('label').removeClass('checked');
        $enable.closest('label:not(.o_switch)').addClass('checked');

        // Mark the option sets as checked if all their option are checked/unchecked
        var $sets = this.$inputs.filter('[data-enable], [data-disable]').not('[data-xmlid]');
        _.each($sets, function (set) {
            var $set = $(set);
            var checked = true;
            if (self._getInputs($set.data('enable')).not(':checked').length) {
                checked = false;
            }
            if (self._getInputs($set.data('disable')).filter(':checked').length) {
                checked = false;
            }
            $set.prop('checked', checked).closest('label:not(.o_switch)').toggleClass('checked', checked);
        });

        // Make the hidden sections visible if their dependencies are met
        _.each(this.$('[data-depends]'), function (hidden) {
            var $hidden = $(hidden);
            var depends = $hidden.data('depends');
            var dependencies = depends ? depends.split(/\s*,\s*/g) : [];
            var enabled = _.all(dependencies, function (dep) {
                var toBeChecked = (dep[0] !== '!');
                if (!toBeChecked) {
                    dep = dep.substr(1);
                }
                return self._getInputs(dep).is(':checked') === toBeChecked;
            });
            $hidden.toggleClass('d-none', !enabled);
        });
    },
    /**
     * @private
     */
    _updateStyle: function (enable, disable, reload) {
        var self = this;

        var $loading = $('<i/>', {class: 'fa fa-refresh fa-spin'});
        this.$modal.find('.modal-title').append($loading);

        if (reload || config.debug === 'assets') {
            window.location.href = $.param.querystring('/website/theme_customize_reload', {
                href: window.location.href,
                enable: (enable || []).join(','),
                disable: (disable || []).join(','),
                tab: this.$('.nav-link.active').parent().index(),
            });
            return $.Deferred();
        }

        return this._rpc({
            route: '/website/theme_customize',
            params: {
                'enable': enable,
                'disable': disable,
                'get_bundle': true,
            },
        }).then(function (bundles) {
            var $allLinks = $();
            var defs = _.map(bundles, function (bundleContent, bundleName) {
                var linkSelector = 'link[href*="' + bundleName + '"]';
                var $links = $(linkSelector);
                $allLinks = $allLinks.add($links);
                var $newLinks = $(bundleContent).filter(linkSelector);

                var linksLoaded = $.Deferred();
                var nbLoaded = 0;
                $newLinks.on('load', function (e) {
                    if (++nbLoaded >= $newLinks.length) {
                        linksLoaded.resolve();
                    }
                });
                $newLinks.on('error', function (e) {
                    linksLoaded.reject();
                    window.location.hash = 'theme=true';
                    window.location.reload();
                });
                $links.last().after($newLinks);
                return linksLoaded;
            });
            return $.when.apply($, defs).always(function () {
                $loading.remove();
                $allLinks.remove();
            });
        }).then(function () {
            // Some animations may depend on the variables that were
            // customized, so we have to restart them.
            self.trigger_up('animation_start_demand');
        });
    },
    /**
     * @private
     */
    _updateValues: function () {
        var self = this;
        // Put user values
        _.each(this.$('.o_theme_customize_color'), function (el) {
            var $el = $(el);
            var value = self.style.getPropertyValue('--' + $el.data('color')).trim();
            $el.css('background-color', value);
        });
        _.each(this.$('.o_theme_customize_input'), function (el) {
            var $el = $(el);
            var value = self.style.getPropertyValue('--' + $el.data('value')).trim();

            // Convert rem values to px values
            if (_.str.endsWith(value, 'rem')) {
                value = parseFloat(value) * self.PX_BY_REM + 'px';
            }

            var $span = $el.find('span');
            $span.removeClass().text('');
            switch (value) {
                case '':
                case 'false':
                case 'true':
                    // When null or a boolean value, shows an icon which tells
                    // the user that there is no numeric/text value
                    $span.addClass('fa fa-ban text-danger');
                    break;
                default:
                    $span.text(value);
            }
        });
    },

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

    /**
     * @private
     * @param {Event} ev
     */
    _onChange: function (ev) {
        var self = this;

        // Checkout the option that changed
        var $option = $(ev.currentTarget);
        if ($option.is(':disabled')) {
            return;
        }
        this.$inputs.prop('disabled', true);

        var $options = $option;
        var checked = $option.is(':checked');

        // If it was enabled, enable/disable the related input (see data-enable,
        // data-disable) and retain the ones that actually changed
        if (checked) {
            var $inputs;
            // Input to enable
            $inputs = this._getInputs($option.data('enable'));
            $options = $options.add($inputs.filter(':not(:checked)'));
            $inputs.prop('checked', true);
            // Input to disable
            $inputs = this._getInputs($option.data('disable'));
            $options = $options.add($inputs.filter(':checked'));
            $inputs.prop('checked', false);
        }
        var optionNames = _.uniq(_.map($options, function (option) {
            return option.name;
        }));
        $options = this.$inputs.filter(function (i, input) {
            return _.contains(optionNames, input.name);
        });

        // Look at all options to see if they are enabled or disabled
        var $enable = $options.filter('[data-xmlid]:checked');
        var $disable = $options.filter('[data-xmlid]:not(:checked)');

        this._setActive();

        // Update the style according to the whole set of options
        self._processChange($options).then(function () {
            return self._updateStyle(
                self._getXMLIDs($enable),
                self._getXMLIDs($disable),
                $option.data('reload') && window.location.href.match(new RegExp($option.data('reload')))
            );
        }).then(function () {
            self._updateValues();
            self.$inputs.prop('disabled', false);
        });
    },
});

var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        'customize_theme': '_openThemeCustomizeDialog',
    }),

    /**
     * Automatically opens the theme customization dialog if the corresponding
     * hash is in the page URL.
     *
     * @override
     */
    start: function () {
        if ((window.location.hash || '').indexOf('theme=true') > 0) {
            var tab = window.location.hash.match(/tab=(\d+)/);
            this._openThemeCustomizeDialog(tab ? tab[1] : false);
            window.location.hash = '';
        }
        return this._super.apply(this, arguments);
    },

    //--------------------------------------------------------------------------
    // Actions
    //--------------------------------------------------------------------------

    /**
     * Instantiates and opens the theme customization dialog.
     *
     * @private
     * @param {string} tab
     * @returns {Deferred}
     */
    _openThemeCustomizeDialog: function (tab) {
        return new ThemeCustomizeDialog(this, {tab: tab}).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize');

return ThemeCustomizeDialog;
});
Example #13
0
odoo.define('website.mobile', function (require) {
'use strict';

var core = require('web.core');
var Dialog = require('web.Dialog');
var websiteNavbarData = require('website.navbar');

var _t = core._t;

var MobilePreviewDialog = Dialog.extend({
    /**
     * Tweaks the modal so that it appears as a phone and modifies the iframe
     * rendering to show more accurate mobile view.
     *
     * @override
     */
    start: function () {
        var self = this;
        this.$modal.addClass('oe_mobile_preview');
        this.$modal.on('click', '.modal-header', function () {
            self.$el.toggleClass('o_invert_orientation');
        });
        this.$iframe = $('<iframe/>', {
            id: 'mobile-viewport',
            src: $.param.querystring(window.location.href, 'mobilepreview'),
        });
        this.$iframe.on('load', function (e) {
            self.$iframe.contents().find('body').removeClass('o_connected_user');
            self.$iframe.contents().find('#oe_main_menu_navbar').remove();
        });
        this.$iframe.appendTo(this.$el);

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

var MobileMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
    actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
        'show-mobile-preview': '_onMobilePreviewClick',
    }),

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

    /**
     * Called when the mobile action is triggered -> instantiate the mobile
     * preview dialog.
     *
     * @private
     */
    _onMobilePreviewClick: function () {
        new MobilePreviewDialog(this, {
            title: _t('Mobile preview') + ' <span class="fa fa-refresh"/>',
        }).open();
    },
});

websiteNavbarData.websiteNavbarRegistry.add(MobileMenu, '#mobile-menu');

return {
    MobileMenu: MobileMenu,
    MobilePreviewDialog: MobilePreviewDialog,
};
});