Exemple #1
0
odoo.define('web_editor.widget', function (require) {
'use strict';

var core = require('web.core');
var ajax = require('web.ajax');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var rte = require('web_editor.rte');

var QWeb = core.qweb;
var range = $.summernote.core.range;
var dom = $.summernote.core.dom;

//////////////////////////////////////////////////////////////////////////////////////////////////////////

var Dialog = Widget.extend({
    events: {
        'hidden.bs.modal': 'destroy',
        'keydown.dismiss.bs.modal': 'stop_escape',
        'click button.save': 'save',
        'click button[data-dismiss="modal"]': 'cancel',
    },
    init: function () {
        this._super();
    },
    start: function () {
        var sup = this._super();
        this.$el.modal({backdrop: 'static'});
        this.$('input:first').focus();
        return sup;
    },
    save: function () {
        this.close();
        this.trigger("saved");
    },
    cancel: function () {
        this.trigger("cancel");
    },
    close: function () {
        this.$el.modal('hide');
    },
    stop_escape: function(event) {
        if($(".modal.in").length>0 && event.which == 27){
            event.stopPropagation();
        }
    }
});

/**
 * alt widget. Lets users change a alt & title on a media
 */
var alt = Dialog.extend({
    template: 'web_editor.dialog.alt',
    init: function ($editable, media) {
        this.$editable = $editable;
        this.media = media;
        this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"');
        this.title = ($(this.media).attr('title') || "").replace(/"/g, '"');
        return this._super();
    },
    save: function () {
        var self = this;
        range.createFromNode(self.media).select();
        this.$editable.data('NoteHistory').recordUndo();
        var alt = this.$('#alt').val();
        var title = this.$('#title').val();
        $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null);
        setTimeout(function () {
            click_event(self.media, "mouseup");
        },0);
        return this._super();
    },
});

var click_event = function(el, type) {
    var evt = document.createEvent("MouseEvents");
    evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
    el.dispatchEvent(evt);
};

/**
 * MediaDialog widget. Lets users change a media, including uploading a
 * new image, font awsome or video and can change a media into an other
 * media
 * 
 * options: select_images: allow the selection of more of one image
 */
var MediaDialog = Dialog.extend({
    template: 'web_editor.dialog.media',
    events : _.extend({}, Dialog.prototype.events, {
        'input input#icon-search': 'search',
    }),
    init: function ($editable, media, options) {
        this._super();
        if ($editable) {
            this.$editable = $editable;
            this.rte = this.$editable.rte || this.$editable.data('rte');
        }
        this.options = options || {};
        this.old_media = media;
        this.media = media;
        this.isNewMedia = !media;
        this.range = range.create();
    },
    start: function () {
        var self = this;

        this.only_images = this.options.only_images || this.options.select_images || (this.media && $(this.media).parent().data("oe-field") === "image");
        if (this.only_images) {
            this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
        }

        if (this.media) {
            if (this.media.nodeName === "IMG") {
                this.$('[href="#editor-media-image"]').tab('show');
            } else if ($(this.media).is('a.o_image')) {
                this.$('[href="#editor-media-document"]').tab('show');
            } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
                this.$('[href="#editor-media-video"]').tab('show');
            } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
                this.media = this.media.parentNode;
                this.$('[href="#editor-media-video"]').tab('show');
            } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
                this.$('[href="#editor-media-icon"]').tab('show');
            }
        }

        this.imageDialog = new ImageDialog(this, this.media, this.options);
        this.imageDialog.appendTo(this.$("#editor-media-image"));
        this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
        this.documentDialog.appendTo(this.$("#editor-media-document"));
        if (!this.only_images) {
            this.iconDialog = new fontIconsDialog(this, this.media, this.options);
            this.iconDialog.appendTo(this.$("#editor-media-icon"));
            this.videoDialog = new VideoDialog(this, this.media, this.options);
            this.videoDialog.appendTo(this.$("#editor-media-video"));
        }

        this.active = this.imageDialog;

        $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
            if ($(event.target).is('[href="#editor-media-image"]')) {
                self.active = self.imageDialog;
                self.$('li.search, li.previous, li.next').removeClass("hidden");
            } if ($(event.target).is('[href="#editor-media-document"]')) {
                self.active = self.documentDialog;
                self.$('li.search, li.previous, li.next').removeClass("hidden");
            } else if ($(event.target).is('[href="#editor-media-icon"]')) {
                self.active = self.iconDialog;
                self.$('li.search, li.previous, li.next').removeClass("hidden");
                self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
            } else if ($(event.target).is('[href="#editor-media-video"]')) {
                self.active = self.videoDialog;
                self.$('.nav-tabs li.search').addClass("hidden");
            }
        });

        return this._super();
    },
    save: function () {
        if (this.options.select_images) {
            this.trigger("saved", this.active.save());
            this.close();
            return;
        }
        if(this.rte) {
            this.range.select();
            this.rte.historyRecordUndo(this.media);
        }

        var self = this;
        if (self.media) {
            this.media.innerHTML = "";
            if (this.active !== this.imageDialog) {
                this.imageDialog.clear();
            }
            if (this.active !== this.documentDialog) {
                this.documentDialog.clear();
            }
            // if not mode only_images
            if (this.iconDialog && this.active !== this.iconDialog) {
                this.iconDialog.clear();
            }
            if (this.videoDialog && this.active !== this.videoDialog) {
                this.videoDialog.clear();
            }
        } else {
            this.media = document.createElement("img");
            this.range.insertNode(this.media, true);
            this.active.media = this.media;
        }
        this.active.save();

        if (this.active.add_class) {
            $(this.active.media).addClass(this.active.add_class);
        }
        var media = this.active.media;

        $(document.body).trigger("media-saved", [media, self.old_media]);
        self.trigger("saved", [media, self.old_media]);
        setTimeout(function () {
            if (!media.parentNode) {
                return;
            }
            range.createFromNode(media).select();
            click_event(media, "mousedown");
            if (!this.only_images) {
                setTimeout(function () {
                    if($(media).parent().data("oe-field") !== "image") {
                        click_event(media, "click");
                    }
                    click_event(media, "mouseup");
                },0);
            }
        },0);

        this.close();
    },
    searchTimer: null,
    search: function () {
        var self = this;
        var needle = this.$("input#icon-search").val();
        clearTimeout(this.searchTimer);
        this.searchTimer = setTimeout(function () {
            self.active.search(needle || "");
        },250);
    }
});

/**
 * ImageDialog widget. Lets users change an image, including uploading a
 * new image in OpenERP or selecting the image style (if supported by
 * the caller).
 */
var IMAGES_PER_ROW = 6;
var IMAGES_ROWS = 2;
var ImageDialog = Widget.extend({
    template: 'web_editor.dialog.image',
    events: _.extend({}, Dialog.prototype.events, {
        'change .url-source': function (e) {
            this.changed($(e.target));
        },
        'click button.filepicker': function () {
            var filepicker = this.$('input[type=file]');
            if (!_.isEmpty(filepicker)){
                filepicker[0].click();
            }
        },
        'click .js_disable_optimization': function () {
            this.$('input[name="disable_optimization"]').val('1');
            var filepicker = this.$('button.filepicker');
            if (!_.isEmpty(filepicker)){
                filepicker[0].click();
            }
        },
        'change input[type=file]': 'file_selection',
        'submit form': 'form_submit',
        'change input.url': "change_input",
        'keyup input.url': "change_input",
        //'change select.image-style': 'preview_image',
        'click .existing-attachments [data-src]': 'select_existing',
        'click .existing-attachment-remove': 'try_remove',
        'keydown.dismiss.bs.modal': function(){},
    }),
    init: function (parent, media, options) {
        this._super();
        this.options = options || {};
        this.accept = this.options.accept || this.options.document ? "*/*" : "image/*";
        this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]];
        this.parent = parent;
        this.media = media;
        this.images = [];
        this.page = 0;
    },
    start: function () {
        this.$preview = this.$('.preview-container').detach();
        var self = this;
        var res = this._super();
        var o = { url: null, alt: null };

        if ($(this.media).is("img")) {
            o.url = this.media.getAttribute('src');
        } else if ($(this.media).is("a.o_image")) {
            o.url = this.media.getAttribute('href').replace(/[?].*/, '');
            o.id = +o.url.match(/\/web\/content\/([0-9]*)/, '')[1];
        }
        this.parent.$(".pager > li").click(function (e) {
            e.preventDefault();
            var $target = $(e.currentTarget);
            if ($target.hasClass('disabled')) {
                return;
            }
            self.page += $target.hasClass('previous') ? -1 : 1;
            self.display_attachments();
        });
        this.fetch_existing().then(function () {
            self.set_image(_.find(self.records, function (record) { return record.url === o.url;}) || o);
        });
        return res;
    },
    push: function (attachment) {
        if (this.options.select_images) {
            var img = _.select(this.images, function (v) { return v.id == attachment.id;});
            if (img.length) {
                this.images.splice(this.images.indexOf(img[0]),1);
                return;
            }
        } else {
            this.images = [];
        }
        this.images.push(attachment);
    },
    save: function () {
        if (this.options.select_images) {
            this.parent.trigger("save", this.images);
            return this.images;
        }
        this.parent.trigger("save", this.media);

        var img = this.images[0];
        if (!img) {
            var id = this.$(".existing-attachments [data-src]:first").data('id');
            img = _.find(this.images, function (img) { return img.id === id;});
        }

        if (!img.is_document) {
            if(this.media.tagName !== "IMG") {
                var media = document.createElement('img');
                $(this.media).replaceWith(media);
                this.media = media;
                this.add_class = "img-responsive pull-left";
            }
            this.media.setAttribute('src', img.src);
        } else {
            if (this.media.tagName !== "A") {
                $('.note-control-selection').hide();
                var media = document.createElement('a');
                $(this.media).replaceWith(media);
                this.media = media;
            }
            this.media.setAttribute('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true');
            $(this.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
        }

        $(this.media).attr('alt', img.alt);
        var style = this.style;
        if (style) { this.media.addClass(style); }

        return this.media;
    },
    clear: function () {
        this.media.className = this.media.className.replace(/(^|\s+)(img(\s|$)|img-(?!circle|rounded|thumbnail)[^\s]*)/g, ' ');
    },
    cancel: function () {
        this.trigger('cancel');
    },
    change_input: function (e) {
        var $input = $(e.target);
        var $button = $input.parent().find("button");
        if ($input.val() === "") {
            $button.addClass("btn-default").removeClass("btn-primary");
        } else {
            $button.removeClass("btn-default").addClass("btn-primary");
        }
    },
    search: function (needle) {
        var self = this;
        this.fetch_existing(needle).then(function () {
            self.selected_existing();
        });
    },
    set_image: function (attachment, error) {
        var self = this;
        this.push(attachment);
        this.$('input.url').val('');
        this.fetch_existing().then(function () {
            self.selected_existing();
        });
    },
    form_submit: function (event) {
        var self = this;
        var $form = this.$('form[action="/web_editor/attachment/add"]');
        if (!$form.find('input[name="upload"]').val().length) {
            var url = $form.find('input[name="url"]').val();
            if (this.selected_existing().size()) {
                event.preventDefault();
                return false;
            }
        }
        $form.find('.well > div').hide().last().after('<span class="fa fa-spin fa-3x fa-refresh"/>');

        var callback = _.uniqueId('func_');
        this.$('input[name=func]').val(callback);
        window[callback] = function (attachments, error) {
            delete window[callback];
            $form.find('.well > span').remove();
            $form.find('.well > div').show();
            _.each(attachments, function (record) {
                record.src = record.url || '/web/image/' + record.id;
                record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
            });
            if (error || !attachments.length) {
                self.file_selected(null, error || !attachments.length);
            }
            self.images = attachments;
            for (var i=0; i<attachments.length; i++) {
                self.file_selected(attachments[i], error);
            }
        };
    },
    file_selection: function () {
        this.$el.addClass('nosave');
        this.$('form').removeClass('has-error').find('.help-block').empty();
        this.$('button.filepicker').removeClass('btn-danger btn-success');
        this.$('form').submit();
    },
    file_selected: function(attachment, error) {
        var $button = this.$('button.filepicker');
        if (!error) {
            $button.addClass('btn-success');
        } else {
            this.$('form').addClass('has-error')
                .find('.help-block').text(error);
            $button.addClass('btn-danger');
            this.set_image(attachment, error);
        }

        if (!this.options.select_images) {
            // auto save and close popup
            this.parent.save();
        }
    },
    fetch_existing: function (needle) {
        var domain = [['res_model', '=', 'ir.ui.view']].concat(this.domain);
        if (needle && needle.length) {
            domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
        }
        return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
            model: 'ir.attachment',
            method: 'search_read',
            args: [],
            kwargs: {
                domain: domain,
                fields: ['name', 'mimetype', 'checksum', 'url'], // if we want to use /web/image/xxx with redirect for image url, remove 'url'
                order: 'id desc',
                context: base.get_context()
            }
        }).then(this.proxy('fetched_existing'));
    },
    fetched_existing: function (records) {
        this.records = records;
        _.each(records, function (record) {
            record.src = record.url || '/web/image/' + record.id;
            record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
        });
        this.display_attachments();
    },
    display_attachments: function () {
        var self = this;
        var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
        var from = this.page * per_screen;
        var records = this.records;

        // Create rows of 3 records
        var rows = _(records).chain()
            .slice(from, from + per_screen)
            .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
            .values()
            .value();

        this.$('.help-block').empty();

        this.$('.existing-attachments').replaceWith(
            QWeb.render(
                'web_editor.dialog.image.existing.content', {rows: rows}));
        this.parent.$('.pager')
            .find('li.previous').toggleClass('disabled', (from === 0)).end()
            .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));

        this.$el.find('.o_image').each(function () {
            var $div = $(this);
            if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) {
                var $img = $('<img/>').addClass('img img-responsive').attr('src', $div.data('url') || $div.data('src'));
                $div.addClass('o_webimage').append($img);
            }
        });
        this.selected_existing();
    },
    select_existing: function (e) {
        var $img = $(e.currentTarget);
        var attachment = _.find(this.records, function (record) { return record.id === $img.data('id'); });
        this.push(attachment);
        this.selected_existing();
    },
    selected_existing: function () {
        var self = this;
        this.$('.existing-attachment-cell.media_selected').removeClass("media_selected");
        var $select = this.$('.existing-attachment-cell [data-src]').filter(function () {
            var $img = $(this);
            return !!_.find(self.images, function (v) {
                return (v.url === $img.data("src") || ($img.data("url") && v.url === $img.data("url")) || v.id === $img.data("id"));
            });
        });
        $select.closest('.existing-attachment-cell').addClass("media_selected");
        return $select;
    },
    try_remove: function (e) {
        var $help_block = this.$('.help-block').empty();
        var self = this;
        var $a = $(e.target);
        var id = parseInt($a.data('id'), 10);
        var attachment = _.findWhere(this.records, {id: id});
        var $both = $a.parent().children();

        $both.css({borderWidth: "5px", borderColor: "#f00"});

        return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) {
            if (_.isEmpty(prevented)) {
                self.records = _.without(self.records, attachment);
                self.display_attachments();
                return;
            }
            $both.css({borderWidth: "", borderColor: ""});
            $help_block.replaceWith(QWeb.render(
                'web_editor.dialog.image.existing.error', {
                    views: prevented[id]
                }
            ));
        });
    },
});


var cacheCssSelectors = {};
var getCssSelectors = function(filter) {
    var css = [];
    if (cacheCssSelectors[filter]) {
        return cacheCssSelectors[filter];
    }
    var sheets = document.styleSheets;
    for(var i = 0; i < sheets.length; i++) {
        var rules;
        if (sheets[i].rules) {
            rules = sheets[i].rules;
        } else {
            //try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
            try {
                rules = sheets[i].cssRules;
            } catch(e) {
                console.warn("Can't read the css rules of: " + sheets[i].href, e);
                continue;
            }
        }
        if (rules) {
            for(var r = 0; r < rules.length; r++) {
                var selectorText = rules[r].selectorText;
                if (selectorText) {
                    selectorText = selectorText.split(/\s*,\s*/);
                    var data = null;
                    for(var s = 0; s < selectorText.length; s++) {
                        var match = selectorText[s].match(filter);
                        if (match) {
                            var clean = match[1].slice(1).replace(/::?before$/, '');
                            if (!data) {
                                data = [match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''), clean, [clean]];
                            } else {
                                data[3].push(clean);
                            }
                        }
                    }
                    if (data) {
                        css.push(data);
                    }
                }
            }
        }
    }
    return cacheCssSelectors[filter] = css;
};
var computeFonts = _.once(function() {
    _.each(fontIcons, function (data) {
        data.cssData = getCssSelectors(data.parser);
        data.alias = [];
        data.icons = _.map(data.cssData, function (css) {
            data.alias.push.apply(data.alias, css[3]);
            return css[2];
        });
    });
});

rte.Class.include({
    init: function (EditorBar) {
        this._super.apply(this, arguments);
        computeFonts();
    }
});

/* list of font icons to load by editor. The icons are displayed in the media editor and
 * identified like font and image (can be colored, spinned, resized with fa classes).
 * To add font, push a new object {base, parser}
 * - base: class who appear on all fonts (eg: fa fa-refresh)
 * - parser: regular expression used to select all font in css style sheets
 */
var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}];


/**
 * FontIconsDialog widget. Lets users change a font awsome, suport all
 * font awsome loaded in the css files.
 */
var fontIconsDialog = Widget.extend({
    template: 'web_editor.dialog.font-icons',
    events : _.extend({}, Dialog.prototype.events, {
        'click .font-icons-icon': function (e) {
            e.preventDefault();
            e.stopPropagation();

            this.$('#fa-icon').val(e.target.getAttribute('data-id'));
            $(".font-icons-icon").removeClass("font-icons-selected");
            $(event.target).addClass("font-icons-selected");
        },
        'keydown.dismiss.bs.modal': function(){},
    }),

    // extract list of font (like awsome) from the cheatsheet.
    renderElement: function() {
        this.iconsParser = fontIcons;
        this.icons = _.flatten(_.map(fontIcons, function (data) {
                return data.icons;
            }));
        this._super();
    },

    init: function (parent, media) {
        this._super();
        this.parent = parent;
        this.media = media;
        computeFonts();
    },
    start: function () {
        return this._super().then(this.proxy('load_data'));
    },
    search: function (needle) {
        var iconsParser = this.iconsParser;
        if (needle) {
            var iconsParser = [];
            _.filter(this.iconsParser, function (data) {
                var cssData = _.filter(data.cssData, function (cssData) {
                    return _.find(cssData[3], function (alias) {
                        return alias.indexOf(needle) !== -1;
                    });
                });
                if (cssData.length) {
                    iconsParser.push({
                        base: data.base,
                        cssData: cssData
                    });
                }
            });
        }
        this.$('div.font-icons-icons').html(
            QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
    },
    /**
     * Removes existing FontAwesome classes on the bound element, and sets
     * all the new ones if necessary.
     */
    save: function () {
        var self = this;
        this.parent.trigger("save", this.media);
        var icons = this.icons;
        var style = this.media.attributes.style ? this.media.attributes.style.value : '';
        var classes = (this.media.className||"").split(/\s+/);
        var custom_classes = /^fa(-[1-5]x|spin|rotate-(9|18|27)0|flip-(horizont|vertic)al|fw|border)?$/;
        var non_fa_classes = _.reject(classes, function (cls) {
            return self.getFont(cls) || custom_classes.test(cls);
        });
        var final_classes = non_fa_classes.concat(this.get_fa_classes());
        if (this.media.tagName !== "SPAN") {
            var media = document.createElement('span');
            $(media).data($(this.media).data());
            $(this.media).replaceWith(media);
            this.media = media;
            style = style.replace(/\s*width:[^;]+/, '');
        }
        $(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style);
    },
    /**
     * return the data font object (with base, parser and icons) or null
     */
    getFont: function (classNames) {
        if (!(classNames instanceof Array)) {
            classNames = (classNames||"").split(/\s+/);
        }
        var fontIcon, cssData;
        for (var k=0; k<this.iconsParser.length; k++) {
            fontIcon = this.iconsParser[k];
            for (var s=0; s<fontIcon.cssData.length; s++) {
                cssData = fontIcon.cssData[s];
                if (_.intersection(classNames, cssData[3]).length) {
                    return {
                        'base': fontIcon.base,
                        'parser': fontIcon.parser,
                        'icons': fontIcon.icons,
                        'font': cssData[2]
                    };
                }
            }
        }
        return null;
    },

    /**
     * Looks up the various FontAwesome classes on the bound element and
     * sets the corresponding template/form elements to the right state.
     * If multiple classes of the same category are present on an element
     * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
     * which may not match the visual look of the element.
     */
    load_data: function () {
        var classes = (this.media&&this.media.className||"").split(/\s+/);
        for (var i = 0; i < classes.length; i++) {
            var cls = classes[i];
            switch(cls) {
                case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
                    // size classes
                    this.$('#fa-size').val(cls);
                    continue;
                case 'fa-spin':
                case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
                case 'fa-flip-horizontal':case 'fa-rotate-vertical':
                    this.$('#fa-rotation').val(cls);
                    continue;
                case 'fa-fw':
                    continue;
                case 'fa-border':
                    this.$('#fa-border').prop('checked', true);
                    continue;
                case '': continue;
                default:
                    $(".font-icons-icon").removeClass("font-icons-selected").filter("[data-alias*=',"+cls+",']").addClass("font-icons-selected");
                    for (var k=0; k<this.icons.length; k++) {
                        if (this.icons.indexOf(cls) !== -1) {
                            this.$('#fa-icon').val(cls);
                            break;
                        }
                    }
            }
        }
    },
    /**
     * Serializes the dialog to an array of FontAwesome classes. Includes
     * the base ``fa``.
     */
    get_fa_classes: function () {
        var font = this.getFont(this.$('#fa-icon').val());
        return [
            font ? font.base : 'fa',
            font ? font.font : "",
            this.$('#fa-size').val(),
            this.$('#fa-rotation').val(),
            this.$('#fa-border').prop('checked') ? 'fa-border' : ''
        ];
    },
    clear: function () {
        this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
    },
});


function createVideoNode(url) {
    // video url patterns(youtube, instagram, vimeo, dailymotion, youku)
    var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
    var ytMatch = url.match(ytRegExp);

    var igRegExp = /\/\/instagram.com\/p\/(.[a-zA-Z0-9]*)/;
    var igMatch = url.match(igRegExp);

    var vRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
    var vMatch = url.match(vRegExp);

    var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
    var vimMatch = url.match(vimRegExp);

    var dmRegExp = /.+dailymotion.com\/(video|hub)\/([^_]+)[^#]*(#video=([^_&]+))?/;
    var dmMatch = url.match(dmRegExp);

    var youkuRegExp = /\/\/v\.youku\.com\/v_show\/id_(\w+)\.html/;
    var youkuMatch = url.match(youkuRegExp);

    var $video = $('<iframe>');
    if (ytMatch && ytMatch[1].length === 11) {
      var youtubeId = ytMatch[1];
      $video = $('<iframe>')
        .attr('src', '//www.youtube.com/embed/' + youtubeId)
        .attr('width', '640').attr('height', '360');
    } else if (igMatch && igMatch[0].length) {
      $video = $('<iframe>')
        .attr('src', igMatch[0] + '/embed/')
        .attr('width', '612').attr('height', '710')
        .attr('scrolling', 'no')
        .attr('allowtransparency', 'true');
    } else if (vMatch && vMatch[0].length) {
      $video = $('<iframe>')
        .attr('src', vMatch[0] + '/embed/simple')
        .attr('width', '600').attr('height', '600')
        .attr('class', 'vine-embed');
    } else if (vimMatch && vimMatch[3].length) {
      $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
        .attr('src', '//player.vimeo.com/video/' + vimMatch[3])
        .attr('width', '640').attr('height', '360');
    } else if (dmMatch && dmMatch[2].length) {
      $video = $('<iframe>')
        .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2])
        .attr('width', '640').attr('height', '360');
    } else if (youkuMatch && youkuMatch[1].length) {
      $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>')
        .attr('height', '498')
        .attr('width', '510')
        .attr('src', '//player.youku.com/embed/' + youkuMatch[1]);
    } else {
      // this is not a known video link. Now what, Cat? Now what?
    }

    $video.attr('frameborder', 0);

    return $video;
}

/**
 * VideoDialog widget. Lets users change a video, support all summernote
 * video, and embled iframe
 */
var VideoDialog = Widget.extend({
    template: 'web_editor.dialog.video',
    events : _.extend({}, Dialog.prototype.events, {
        'click input#urlvideo ~ button': 'get_video',
        'click input#embedvideo ~ button': 'get_embed_video',
        'change input#urlvideo': 'change_input',
        'keyup input#urlvideo': 'change_input',
        'change input#embedvideo': 'change_input',
        'keyup input#embedvideo': 'change_input',
        'keydown.dismiss.bs.modal': function(){},
    }),
    init: function (parent, media) {
        this._super();
        this.parent = parent;
        this.media = media;
    },
    start: function () {
        this.$preview = this.$('.preview-container').detach();
        this.$iframe = this.$("iframe");
        var $media = $(this.media);
        if ($media.hasClass("media_iframe_video")) {
            var src = $media.data('src');
            this.$("input#urlvideo").val(src);
            this.get_video();
        } else {
            this.add_class = "pull-left";
        }
        return this._super();
    },
    change_input: function (e) {
        var $input = $(e.target);
        var $button = $input.parent().find("button");
        if ($input.val() === "") {
            $button.addClass("btn-default").removeClass("btn-primary");
        } else {
            $button.removeClass("btn-default").addClass("btn-primary");
        }
    },
    get_embed_video: function (event) {
        event.preventDefault();
        var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/);
        if (embedvideo) {
            this.$("input#urlvideo").val(embedvideo[1]);
            this.get_video(event);
        }
        return false;
    },
    get_video: function (event) {
        if (event) event.preventDefault();
        var $video = createVideoNode(this.$("input#urlvideo").val());
        this.$iframe.replaceWith($video);
        this.$iframe = $video;
        return false;
    },
    save: function () {
        this.parent.trigger("save", this.media);
        var video_id = this.$("#video_id").val();
        if (!video_id) {
            this.$("button.btn-primary").click();
            video_id = this.$("#video_id").val();
        }
        var video_type = this.$("#video_type").val();
        var $iframe = $(
            '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+
                '<div class="css_editable_mode_display">&nbsp;</div>'+
                '<div class="media_iframe_video_size" contentEditable="false">&nbsp;</div>'+
                '<iframe src="'+this.$iframe.attr("src")+'" frameborder="0" allowfullscreen="allowfullscreen" contentEditable="false"></iframe>'+
            '</div>');
        $(this.media).replaceWith($iframe);
        this.media = $iframe[0];
    },
    clear: function () {
        if (this.media.dataset.src) {
            try {
                delete this.media.dataset.src;
            } catch(e) {
                this.media.dataset.src = undefined;
            }
        }
        this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
    },
});

/* ----- EDITOR: LINK & MEDIA ---- */
var LinkDialog = Dialog.extend({
    template: 'web_editor.dialog.link',
    events: _.extend({}, Dialog.prototype.events, {
        'change :input.url-source': 'changed',
        'keyup :input.url': 'onkeyup',
        'keyup :input': 'preview',
        'click button.remove': 'remove_link',
        'change input#link-text': function (e) {
            this.text = $(e.target).val();
        },
        'change .link-style': function (e) {
            this.preview();
        },
    }),
    init: function (editable, linkInfo) {
        this._super(editable, linkInfo);
        this.editable = editable;
        this.data = linkInfo || {};

        this.data.className = "";
        if (this.data.range) {
            this.data.iniClassName = $(this.data.range.sc).filter("a").attr("class") || "";
            this.data.className = this.data.iniClassName.replace(/(^|\s+)btn(-[a-z0-9_-]*)?/gi, ' ');

            var is_link = this.data.range.isOnAnchor();
            var r = this.data.range;

            var sc = r.sc;
            var so = r.so;
            var ec = r.ec;
            var eo = r.eo;

            var nodes;
            if (!is_link) {
                if (sc.tagName) {
                    sc = dom.firstChild(so ? sc.childNodes[so] : sc);
                    so = 0;
                } else if (so !== sc.textContent.length) {
                    if (sc === ec) {
                        ec = sc = sc.splitText(so);
                        eo -= so;
                    } else {
                        sc = sc.splitText(so);
                    }
                    so = 0;
                }
                if (ec.tagName) {
                    ec = dom.lastChild(eo ? ec.childNodes[eo-1] : ec);
                    eo = ec.textContent.length;
                } else if (eo !== ec.textContent.length) {
                    ec.splitText(eo);
                }
                
                nodes = dom.listBetween(sc, ec);

                // browsers can't target a picture or void node
                if (dom.isVoid(sc) || dom.isImg(sc)) {
                  so = dom.listPrev(sc).length-1;
                  sc = sc.parentNode;
                }
                if (dom.isBR(ec)) {
                  eo = dom.listPrev(ec).length-1;
                  ec = ec.parentNode;
                } else if (dom.isVoid(ec) || dom.isImg(sc)) {
                  eo = dom.listPrev(ec).length;
                  ec = ec.parentNode;
                }

                this.data.range = range.create(sc, so, ec, eo);
                this.data.range.select();
            } else {
                nodes = dom.ancestor(sc, dom.isAnchor).childNodes;
            }

            if (dom.isImg(sc) && nodes.indexOf(sc) === -1) {
                nodes.push(sc);
            }
            if (nodes.length > 1 || dom.ancestor(nodes[0], dom.isImg)) {
                var text = "";
                this.data.images = [];
                for (var i=0; i<nodes.length; i++) {
                    if (dom.ancestor(nodes[i], dom.isImg)) {
                        this.data.images.push(dom.ancestor(nodes[i], dom.isImg));
                        text += '[IMG]';
                    } else if (!is_link && i===0) {
                        text += nodes[i].textContent.slice(so, Infinity);
                    } else if (!is_link && i===nodes.length-1) {
                        text += nodes[i].textContent.slice(0, eo);
                    } else {
                        text += nodes[i].textContent;
                    }
                }
                this.data.text = text;
            }
        }

        this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
    },
    start: function () {
        this.bind_data();
        this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active');
        return this._super();
    },
    get_data: function (test) {
        var self = this;
        var def = new $.Deferred();
        var $e = this.$('.active input.url-source');
        if (!$e.length) {
            $e = this.$('input.url-source:first');
        }
        var val = $e.val();
        var label = this.$('#link-text').val() || val;

        if (label && this.data.images) {
            for(var i=0; i<this.data.images.length; i++) {
                label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
            }
        }

        if (!test && (!val || !$e[0].checkValidity())) {
            // FIXME: error message
            $e.closest('.form-group').addClass('has-error');
            $e.focus();
            def.reject();
        }

        var style = this.$("input[name='link-style-type']:checked").val() || '';
        var size = this.$("input[name='link-style-size']:checked").val() || '';
        var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
        var isNewWindow = this.$('input.window-new').prop('checked');

        if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
            self.get_data_buy_mail(def, $e, isNewWindow, label, classes);
        } else {
            self.get_data_buy_url(def, $e, isNewWindow, label, classes);
        }
        return def;
    },
    get_data_buy_mail: function (def, $e, isNewWindow, label, classes) {
        var val = $e.val();
        def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
    },
    get_data_buy_url: function (def, $e, isNewWindow, label, classes) {
        def.resolve($e.val(), isNewWindow, label, classes);
    },
    save: function () {
        var self = this;
        var _super = this._super.bind(this);
        return this.get_data()
            .then(function (url, new_window, label, classes) {
                self.data.url = url;
                self.data.newWindow = new_window;
                self.data.text = label;
                self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');

                self.trigger("save", self.data);
            }).then(_super);
    },
    bind_data: function () {
        var href = this.data.url;
        var new_window = this.data.isNewWindow;
        var text = this.data.text;
        var classes = this.data.iniClassName;

        this.$('input#link-text').val(text);
        this.$('input.window-new').prop('checked', new_window);

        if (classes) {
            this.$('input[value!=""]').each(function () {
                var $option = $(this);
                if (classes.indexOf($option.val()) !== -1) {
                    $option.attr("checked", "checked");
                }
            });
        }

        if (href) {
            var match;
            if(match = /mailto:(.+)/.exec(href)) {
                this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : '');
            } else {
                this.$('input.url').val(href);
            }
        }
        this.preview();
    },
    changed: function (e) {
        $(e.target).closest('.list-group-item')
            .addClass('active')
            .siblings().removeClass('active')
            .addBack().removeClass('has-error');
        this.preview();
    },
    onkeyup: function (e) {
        var $e = $(e.target);
        var is_link = ($e.val()||'').length && $e.val().indexOf("@") === -1;
        this.$('input.window-new').closest("div").toggle(is_link);
        this.preview();
    },
    preview: function () {
        var $preview = this.$("#link-preview");
        this.get_data(true).then(function (url, new_window, label, classes) {
            $preview.attr("target", new_window ? '_blank' : "")
                .attr("href", url && url.length ? url : "#")
                .html((label && label.length ? label : url))
                .attr("class", classes.replace(/pull-\w+/, ''));
        });
    }
});

return {
    'getCssSelectors': getCssSelectors,
    'Dialog': Dialog,
    'alt': alt,
    'MediaDialog': MediaDialog,
    'fontIcons': fontIcons,
    'LinkDialog': LinkDialog
};

});
Exemple #2
0
odoo.define('web_editor.translate', function (require) {
'use strict';

var core = require('web.core');
var Model = require('web.Model');
var ajax = require('web.ajax');
var Class = require('web.Class');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var rte = require('web_editor.rte');
var editor_widget = require('web_editor.widget');

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

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


var translatable = !!$('html').data('translatable');
var edit_translations = !!$('html').data('edit_translations');

$.fn.extend({
  prependEvent: function (events, selector, data, handler) {
    this.on(events, selector, data, handler);
    events = events.split(' ');
    this.each(function () {
        var el = this;
        _.each(events, function (event) {
            var handler = $._data(el, 'events')[event].pop();
            $._data(el, 'events')[event].unshift(handler);
        });
    });
    return this;
  }
});

var RTE_Translate = rte.Class.extend({
    saveElement: function ($el, context) {
        // remove multi edition
        var key = $el.data('oe-translation-id') ? 'translation:'+$el.data('oe-translation-id') :
                ($el.data('oe-model') ? $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression') : false);
        if (!key || this.__saved[key]) return true;
        this.__saved[key] = true;

        if ($el.data('oe-translation-id')) {
            var translation_content = this.getEscapedElement($el).html();

            return ajax.jsonRpc('/web/dataset/call', 'call', {
                model: 'ir.translation',
                method: 'write',
                args: [
                    [+$el.data('oe-translation-id')],
                    {'value': translation_content, 'state': 'translated'},
                    context || base.get_context()
                ],
            });
        } else {
            var markup = this.getEscapedElement($el).prop('outerHTML');

            return ajax.jsonRpc('/web/dataset/call', 'call', {
                model: 'ir.ui.view',
                method: 'save',
                args: [
                    $el.data('oe-id'),
                    markup,
                    $el.data('oe-xpath') || null,
                    context || base.get_context()
                ],
            });
        }
    },
});

var Translate_Modal = editor_widget.Dialog.extend({
    template: 'web_editor.translator.attributes',
    init: function (parent, node) {
        this._super();
        this.parent = parent;
        this.$target = $(node);
        this.translation = $(node).data('translation');
    },
    start: function () {
        var self = this;
        this._super();
        var $group = this.$el.find('.form-group');
        _.each(this.translation, function (node, attr) {
            var $node = $(node);
            var $label = $('<label class="control-label"></label>').text(attr);
            var $input = $('<input class="form-control"/>').val($node.html());
            $input.on('change keyup', function () {
                var value = $input.val();
                $node.html(value).trigger('change', node);
                $node.data('$node').attr($node.data('attribute'), value).trigger('translate');
                self.parent.rte_changed(node);
            });
            $group.append($label).append($input);
        });
    }
});

var Translate = Widget.extend({
    events: {
        'click [data-action="save"]': 'save_and_reload',
        'click [data-action="cancel"]': 'cancel',
    },
    template: 'web_editor.translator',
    init: function (parent, $target, lang) {
        this.parent = parent;
        this.ir_translation = new Model('ir.translation');
        this.lang = lang || base.get_context().lang;
        this.setTarget($target);
        this._super();

        this.rte = new RTE_Translate(this, this.config);
        this.rte.on('change', this, this.rte_changed);
    },
    start: function () {
        this._super();
        this.$('button[data-action=save]').prop('disabled', true);
        return this.edit();
    },
    setTarget: function ($target) {
        var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
        $edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);

        this.$target = $edit.not('[data-oe-readonly]');

        // attributes

        var attrs = ['placeholder', 'title', 'alt'];
        _.each(attrs, function (attr) {
            $target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
                var $node = $(this);
                var translation = $node.data('translation') || {};
                var trans = $node.attr(attr);
                var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
                var $trans = $(trans).addClass('hidden o_editable o_editable_translatable_attribute').appendTo('body');
                $trans.data('$node', $node).data('attribute', attr);
                translation[attr] = $trans[0];
                $node.attr(attr, match[2]);

                var select2 = $node.data('select2');
                if (select2) {
                    select2.blur();
                    $node.on('translate', function () {
                        select2.blur();
                    });
                    $node = select2.container.find('input');
                }
                $node.addClass('o_translatable_attribute').data('translation', translation);
            });
        });
        this.$target_attr = $target.find('.o_translatable_attribute');
        this.$target_attribute = $('.o_editable_translatable_attribute');
    },
    find: function (selector) {
        return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target;
    },
    edit: function () {
        var flag = false;
        window.onbeforeunload = function(event) {
            if ($('.o_editable.o_dirty').length && !flag) {
                flag = true;
                setTimeout(function () {flag=false;},0);
                return _t('This document is not saved!');
            }
        };
        this.$target.addClass("o_editable");
        this.rte.start();
        this.translations = [];
        this.markTranslatableNodes();
        this.onTranslateReady();
    },
    onTranslateReady: function () {
        this.$el.show();
        this.trigger("edit");
    },
    rte_changed: function (node) {
        var $node = $(node);
        var trans = this.getTranlationObject($node[0]);
        $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
        this.$('button[data-action=save]').prop('disabled', !$('.o_editable.o_dirty').length);
    },
    getTranlationObject: function (node) {
        var $node = $(node);
        var id = +$node.data('oe-translation-id');
        if (!id) {
            id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
        }
        var trans = _.find(this.translations, function (trans) {
            return trans.id === id;
        });
        if (!trans) {
            this.translations.push(trans = {'id': id});
        }
        return trans;
    },
    __unbind_click: function (event) {
        if (event.ctrlKey || !$(event.target).is(':o_editable')) {
            return;
        }
        event.preventDefault();
        event.stopPropagation();
    },
    __translate_attribute: function (event) {
        if (event.ctrlKey) {
            return;
        }
        event.preventDefault();
        event.stopPropagation();
        if (event.type !== 'mousedown') {
            return;
        }

        new Translate_Modal(event.data, event.target).appendTo('body');
    },
    markTranslatableNodes: function (node) {
        var self = this;
        this.$target.add(this.$target_attribute).each(function () {
            var $node = $(this);
            var trans = self.getTranlationObject(this);
            trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
        });
        this.$target.parent().prependEvent('click', this, this.__unbind_click);

        // attributes

        this.$target_attr.each(function () {
            var $node = $(this);
            var translation = $node.data('translation');
            _.each(translation, function (node, attr) {
                var trans = self.getTranlationObject(node);
                trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
                $node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
            });
        });

        this.$target_attr.prependEvent('mousedown click mouseup', this, this.__translate_attribute);

        console.info('Click on CTRL when you click in an translatable area to have the default behavior');
    },
    unarkTranslatableNode: function () {
        this.$target.removeClass('o_editable').removeAttr('contentEditable');
        this.$target.parent().off('click', this.__unbind_click);
        this.$target_attr.off('mousedown click mouseup', this.__translate_attribute);
    },
    save_and_reload: function () {
        return this.save().then(function () {
            window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
        });
    },
    save: function () {
        var context = base.get_context();
        context.lang = this.lang;
        return this.rte.save(context);
    },
    cancel: function () {
        var self = this;
        this.rte.cancel();
        this.$target.each(function () {
            $(this).html(self.getTranlationObject(this).value);
        });
        this.unarkTranslatableNode();
        this.trigger("cancel");
        this.$el.hide();
        window.onbeforeunload = null;
    },
    destroy: function () {
        this.cancel();
        this.$el.remove();
        this._super();
    },

    config: function ($editable) {
        if ($editable.data('oe-model')) {
            return {
                'airMode' : true,
                'focus': false,
                'airPopover': [
                    ['history', ['undo', 'redo']],
                ],
                'styleWithSpan': false,
                'inlinemedia' : ['p'],
                'lang': "odoo",
                'onChange': function (html, $editable) {
                    $editable.trigger("content_changed");
                }
            };
        }
        return {
            'airMode' : true,
            'focus': false,
            'airPopover': [
                ['font', ['bold', 'italic', 'underline', 'clear']],
                ['fontsize', ['fontsize']],
                ['color', ['color']],
                ['history', ['undo', 'redo']],
            ],
            'styleWithSpan': false,
            'inlinemedia' : ['p'],
            'lang': "odoo",
            'onChange': function (html, $editable) {
                $editable.trigger("content_changed");
            }
        };
    }
});


if (edit_translations) {
    base.ready().then(function () {
        data.instance = new Translate(this, $('#wrapwrap'));
        data.instance.prependTo(document.body);

        $('a[href*=edit_translations]').each(function () {
            this.href = this.href.replace(/[$?]edit_translations[^&?]+/, '');
        });
        $('form[action*=edit_translations]').each(function () {
            this.action = this.action.replace(/[$?]edit_translations[^&?]+/, '');
        });

        $('title').html($('title').html().replace(/&lt;span data-oe-model.+?&gt;(.+?)&lt;\/span&gt;/, '\$1'));
    });
}

var data = {
    'translatable': translatable,
    'edit_translations': edit_translations,
    'Class': Translate,
};
return data;

});
Exemple #3
0
odoo.define('web_editor.iframe', function (require) {
'use strict';

var core = require('web.core');
var editor = require('web_editor.editor');
var translator = require('web_editor.translate');
var rte = require('web_editor.rte');

var callback = window ? window['callback'] : undefined;
window.top.odoo[callback + '_updown'] = function (value, fields_values, field_name) {
    var $editable = $('#editable_area');
    if (value === $editable.prop('innerHTML')) {
        return;
    }

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

    $editable.html(value);

    if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
        $editable.trigger('content_changed');
    }
};

editor.Class.include({
    start: function () {
        this.on('rte:start', this, function () {
            this.$('form').hide();

            if (window.top.odoo[callback + '_editor']) {
                window.top.odoo[callback + '_editor'](this);
            }

            var $editable = $('#editable_area');
            setTimeout(function () {
                $($editable.find('*').filter(function () {return !this.children.length;}).first()[0] || $editable)
                    .focusIn().trigger('mousedown').trigger('keyup');
            },0);

            $editable.on('content_changed', this, function () {
                if (window.top.odoo[callback + '_downup']) {
                    window.top.odoo[callback + '_downup']($editable.prop('innerHTML'));
                }
            });
        });

        return this._super.apply(this, arguments).then(function () {
            $(window.top).trigger('resize'); // TODO check, probably useless
        });
    }
});

rte.Class.include({
    /**
     * @override
     */
    _getDefaultConfig: function ($editable) {
        var config = this._super.apply(this, arguments);
        if ($.deparam($.param.querystring()).debug !== undefined) {
            config.airPopover.splice(7, 0, ['view', ['codeview']]);
        }
        return config;
    },
});

translator.Class.include({
    start: function () {
        var res = this._super.apply(this, arguments);
        $('button[data-action=save]').hide();
        if (window.top.odoo[callback + '_editor']) {
            window.top.odoo[callback + '_editor'](this);
        }
        return res;
    },
});
});
Exemple #4
0
odoo.define('web_editor.rte.summernote', function (require) {
'use strict';

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

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

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

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

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

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

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

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

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

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

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

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

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

    if (window === window.top) {
        $popover.children().addClass("hidden-xs");
    }

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    return $popover;
};

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

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

    $container.find('.note-color').removeClass("hidden");

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

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

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

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

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

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

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

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

            $container.find('.note-color').addClass("hidden");
        }

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

        $(oStyle.image).trigger('attributes_change');
    }
};

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

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

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

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

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

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

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

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

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

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

    var def = new $.Deferred();
    core.bus.trigger('link_dialog_demand', {
        $editable: $editable,
        linkInfo: linkInfo,
        onSave: function (linkInfo) {
            linkInfo.range.select();
            $editable.data('range', linkInfo.range);
            def.resolve(linkInfo);
            $editable.trigger('keyup');
            $('.note-popover .note-link-popover').show();
        },
        onCancel: def.reject.bind(def),
    });
    return def;
};
eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
    var r = $editable.data('range');
    if (r.sc.tagName && r.sc.childNodes.length) {
        r.sc = r.sc.childNodes[r.so];
    }
    var media = $(r.sc).parents().addBack().filter(function (i, el) {
        return dom.isImg(el);
    })[0];
    core.bus.trigger('media_dialog_demand', {
        $editable: $editable,
        media: media,
        options: {
            lastFilters: ['background'],
            onUpload: $editable.data('callbacks').onUpload,
        },
    });
    return new $.Deferred().reject();
};
$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
    var $editable = layoutInfo.editable();
    var $selection = layoutInfo.handle().find('.note-control-selection');
    core.bus.trigger('alt_dialog_demand', {
        $editable: $editable,
        media: $selection.data('target'),
    });
};
$.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) {
    var $editable = layoutInfo.editable();
    var $selection = layoutInfo.handle().find('.note-control-selection');
    core.bus.trigger('crop_image_dialog_demand', {
        $editable: $editable,
        media: $selection.data('target'),
    });
};

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

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

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

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

    // reduce => merge inline style + text

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    initial_data.event = event;

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

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

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

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

    last_div = div;

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

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

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

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

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

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

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

    var clone_data = false;

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

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

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

            var nodes = $node.get();

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    /**
     * Called when a demand to open a alt dialog is received on the bus.
     *
     * @private
     * @param {Object} data
     */
    _onAltDialogDemand: function (data) {
        if (data.__alreadyDone) {
            return;
        }
        data.__alreadyDone = true;
        var altDialog = new weWidgets.AltDialog(this,
            data.options || {},
            data.$editable,
            data.media
        );
        if (data.onSave) {
            altDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            altDialog.on('cancel', this, data.onCancel);
        }
        altDialog.open();
    },

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

        var mediaDialog = new weWidgets.MediaDialog(this,
            _.extend({
                res_model: data.$editable.data('oe-model'),
                res_id: data.$editable.data('oe-id'),
                domain: data.$editable.data('oe-media-domain'),
            }, data.options),
            data.$editable,
            data.media
        );
        if (data.onSave) {
            mediaDialog.on('save', this, data.onSave);
        }
        if (data.onCancel) {
            mediaDialog.on('cancel', this, data.onCancel);
        }
        mediaDialog.open();
    },
});
/**
 * @todo cannot do this without include because it would make a loop in the
 * JS module dependencies otherwise.
 */
rte.Class.include({
    /**
     * @override
     */
    start: function () {
        this._summernoteManager = new SummernoteManager(this);
        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    cancel: function () {
        this._super.apply(this, arguments);
        this._summernoteManager.destroy();
    },
});
return SummernoteManager;
});
Exemple #5
0
odoo.define('website_blog.editor', function (require) {
"use strict";

    var ajax = require('web.ajax');
    var widget = require('web_editor.widget');
    var options = require('web_editor.snippets.options');
    var rte = require('web_editor.rte');

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

    rte.Class.include({
        saveElement: function ($el, context) {
            if ($el.is('.website_blog #title')) {
                return ajax.jsonRpc("/blog/post_change_background", 'call', {
                    'post_id' : +$el.find('#blog_post_name').data('oe-id'),
                    'cover_properties' : {
                        "background-image": $el.find('#js_blogcover').css("background-image").replace(/"/g, ''),
                        "background-color": $el.find('#js_blogcover').attr("class"),
                        "opacity": $el.find('#js_blogcover').css("opacity"),
                        "resize_class": $el.attr('class'),
                    }
                });
            }
            return this._super($el, context);
        },
    });


    options.registry.many2one.include({
        select_record: function (li) {
            var self = this;
            this._super(li);
            if (this.$target.data('oe-field') === "author_id") {
                var $nodes = $('[data-oe-model="blog.post"][data-oe-id="'+this.$target.data('oe-id')+'"][data-oe-field="author_avatar"]');
                $nodes.each(function () {
                    var $img = $(this).find("img");
                    var css = window.getComputedStyle($img[0]);
                    $img.css({ width: css.width, height: css.height });
                    $img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
                });
                setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
            }
        }
    });

    options.registry.website_blog = options.Class.extend({
        start : function() {
            this.$cover = this.$target.find('#js_blogcover');
            this.src = this.$target.css("background-image").replace(/url\(|\)|"|'/g,'').replace(/.*none$/,'');
            this.$image = $('<image src="'+this.src+'">');
            this._super();
        },
        clear : function(type, value, $li) {
            if (type !== 'click') return;
            this.src = null;
            this.$cover.css({"background-image": '', 'min-height': ''});
            this.$image.removeAttr("src");
            this.$target.removeClass('cover cover_full');
        },
        change : function(type, value, $li) {
            if (type !== 'click') return;
            var self = this;
            var editor  = new widget.MediaDialog(this.$image, this.$image[0], {only_images: true});
            editor.appendTo('body');
            editor.on('saved', self, function (event, img) {
                var url = self.$image.attr('src');
                self.$cover.css({"background-image": url ? 'url(' + url + ')' : "", 'min-height': $(window).height()-this.$cover.offset().top});
                self.$target.addClass('o_dirty cover cover_full');
                self.set_active();
            });
        },
        cover_class : function(type, value, $li) {
            this.$target.attr("class", (type === 'over' || type === 'click') ? value : this.class);
            this.$target.addClass('o_dirty');
        },
        opacity : function(type, value, $li) {
            this.$cover.css("opacity", (type === 'over' || type === 'click') ? value : this.value);
            this.$target.addClass('o_dirty');
        },
        bgcolor : function(type, value, $li) {
            this.$cover.attr("class", (type === 'over' || type === 'click') ? value : this.background);
            this.$target.addClass('o_dirty');
        },
        set_active: function(){
            this._super();
            this.background = this.$cover.attr("class");
            this.class = this.$target.attr('class');
            this.value = this.$cover.css('opacity');
            this.$el.parent().find('.snippet-option-website_blog:not(li[data-change])').toggleClass("hidden", !this.$target.hasClass("cover"));
            this.$el.find('li[data-bgcolor], li[data-opacity], li[data-cover_class]').removeClass("active");
            this.$el.find('[data-bgcolor="' + this.background + '"], [data-opacity="' + parseFloat(this.value).toFixed(1) + '"], [data-cover_class*="' + ((this.class||'').indexOf('cover_full') === -1 ? 'container' : 'cover_full') + '"]').addClass("active");
        },
    });

});
Exemple #6
0
odoo.define('website_blog.editor', function (require) {
    "use strict";

    var ajax = require('web.ajax');
    var widget = require('web_editor.widget');
    var options = require('web_editor.snippets.options');
    var rte = require('web_editor.rte');

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

    rte.Class.include({
        // Destroy popOver and stop listening mouseup event on edit mode
        start: function () {
            $(".js_tweet, .js_comment").off('mouseup').trigger('mousedown');
            return this._super.apply(this, arguments);
        },
        saveElement: function ($el, context) {
            if ($el.is('.o_blog_cover_container')) {
                return ajax.jsonRpc("/blog/post_change_background", 'call', {
                    'post_id' : parseInt($el.closest("[name=\"blog_post\"], .website_blog").find("[data-oe-model=\"blog.post\"]").first().data("oe-id"), 10),
                    'cover_properties' : {
                        "background-image": $el.children(".o_blog_cover_image").css("background-image").replace(/"/g, '').replace(location.protocol + "//" + location.host, ''),
                        "background-color": $el.data("filter_color"),
                        "opacity": $el.data("filter_value"),
                        "resize_class": $el.data("cover_class"),
                    }
                });
            }
            return this._super.apply(this, arguments);
        },
    });

    options.registry.many2one.include({
        select_record: function (li) {
            var self = this;
            this._super(li);
            if (this.$target.data('oe-field') === "author_id") {
                var $nodes = $('[data-oe-model="blog.post"][data-oe-id="'+this.$target.data('oe-id')+'"][data-oe-field="author_avatar"]');
                $nodes.each(function () {
                    var $img = $(this).find("img");
                    var css = window.getComputedStyle($img[0]);
                    $img.css({ width: css.width, height: css.height });
                    $img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
                });
                setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
            }
        }
    });

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

            this.$image = this.$target.children(".o_blog_cover_image");
            this.$filter = this.$target.children(".o_blog_cover_filter");

            this.$filter_value_options = this.$el.find('li[data-filter_value]');
            this.$filter_color_options = this.$el.find('li[data-filter_color]');

            this.filter_color_classes = this.$filter_color_options.map(function () {
                return $(this).data("filter_color");
            }).get().join(" ");
        },
        clear: function (type, value, $li) {
            if (type !== 'click') return;

            this.select_class(type, "", $());
            this.$image.css("background-image", "");
            this.$target.addClass("o_dirty");
        },
        change: function (type, value, $li) {
            if (type !== 'click') return;

            var $image = $("<img/>", {src: this.$image.css("background-image")});

            var editor = new widget.MediaDialog(null, {only_images: true}, $image, $image[0]).open();
            editor.on("save", this, function (event, img) {
                var src = $image.attr("src");
                this.$image.css("background-image", src ? ("url(" + src + ")") : "");
                if (!this.$target.hasClass("cover")) {
                    var $li = this.$el.find("[data-select_class]").first();
                    this.select_class(type, $li.data("select_class"), $li);
                }
                this.set_active();
                this.$target.addClass("o_dirty");
            });
        },
        filter_value: function (type, value, $li) {
            this.$filter.css("opacity", value);
            this.$target.addClass('o_dirty');
        },
        filter_color: function (type, value, $li) {
            this.$filter.removeClass(this.filter_color_classes);
            if (value) {
                this.$filter.addClass(value);
            }
            this.$target.addClass("o_dirty");

            var $first_visible_filter_option = this.$filter_value_options.eq(1);
            if (parseFloat(this.$filter.css('opacity')) < parseFloat($first_visible_filter_option.data("filter_value"))) {
                this.filter_value(type, $first_visible_filter_option.data("filter_value"), $first_visible_filter_option);
            }
        },
        set_active: function () {
            this._super.apply(this, arguments);
            var self = this;

            this.$el.filter(":not([data-change])").toggleClass("hidden", !this.$target.hasClass("cover"));
            this.$el.filter("li:has(li[data-select_class])").toggleClass("hidden", this.$target.hasClass("o_list_cover"));

            this.$filter_value_options.removeClass("active");
            this.$filter_color_options.removeClass("active");

            var active_filter_value = this.$filter_value_options
                .filter(function () {
                    return (parseFloat($(this).data('filter_value')).toFixed(1) === parseFloat(self.$filter.css('opacity')).toFixed(1));
                }).addClass("active").data("filter_value");

            var active_filter_color = this.$filter_color_options
                .filter(function () {
                    return self.$filter.hasClass($(this).data("filter_color"));
                }).addClass("active").data("filter_color");

            this.$target.data("cover_class", this.$el.find(".active[data-select_class]").data("select_class") || "");
            this.$target.data("filter_value", active_filter_value || 0.0);
            this.$target.data("filter_color", active_filter_color || "");
        },
    });
});
Exemple #7
0
odoo.define('web_editor.iframe', function (require) {
'use strict';

var ajax = require("web.ajax");
var core = require("web.core");
var editor = require('web_editor.editor');
var translator = require('web_editor.translate');
var rte = require('web_editor.rte');
var snippet_editor = require('web_editor.snippet.editor');

var callback = window ? window["callback"] : undefined;
window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) {
    var $editable = $("#editable_area");
    if(value !== $editable.prop("innerHTML")) {
        if ($('body').hasClass('editor_enable')) {
            if (value !== fields_values[field_name]) {
                rte.history.recordUndo($editable);
            }
            snippet_editor.instance.make_active(false);
        }

        $editable.html(value);

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

editor.Class.include({
    start: function () {
        this.on('rte:start', this, function () {
            this.$('form').hide();

            if (window.top.odoo[callback+"_editor"]) {
                window.top.odoo[callback+"_editor"](this);
            }

            var $editable = $("#editable_area");
            setTimeout(function () {
                $($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable)
                    .focusIn().trigger("mousedown").trigger("keyup");
            },0);

            $editable.on('content_changed', this, function () {
                if (window.top.odoo[callback+"_downup"]) {
                    window.top.odoo[callback+"_downup"]($editable.prop('innerHTML'));
                }
            });
        });

        this.on("snippets:ready", this, function () {
            $(window.top).trigger("resize");
        });
        return this._super.apply(this, arguments);
    }
});

snippet_editor.Class.include({
    _get_snippet_url: function () {
        return snippets_url;
    }
});

rte.Class.include({
    config: function ($editable) {
        var config = this._super.apply(this, arguments);
        if ($.deparam($.param.querystring()).debug !== undefined) {
            config.airPopover.splice(7, 0, ['view', ['codeview']]);
        }
        return config;
    }
});

translator.Class.include({
    start: function () {
        var res = this._super.apply(this, arguments);
        $('button[data-action=save]').hide();
        if (window.top.odoo[callback+"_editor"]) {
            window.top.odoo[callback+"_editor"](this);
        }
        return res;
    }
});

});
Exemple #8
0
odoo.define('web_editor.translate', function (require) {
'use strict';

var core = require('web.core');
var Dialog = require('web.Dialog');
var localStorage = require('web.local_storage');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
var weWidgets = require('web_editor.widget');

var _t = core._t;

var localStorageNoDialogKey = 'website_translator_nodialog';

var RTETranslatorWidget = rte.Class.extend({

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

    /**
     * If the element holds a translation, saves it. Otherwise, fallback to the
     * standard saving but with the lang kept.
     *
     * @override
     */
    _saveElement: function ($el, context, withLang) {
        if ($el.data('oe-translation-id')) {
            return this._rpc({
                model: 'ir.translation',
                method: 'save_html',
                args: [
                    [+$el.data('oe-translation-id')],
                    this._getEscapedElement($el).html(),
                    context || weContext.get()
                ],
            });
        }
        return this._super($el, context, withLang === undefined ? true : withLang);
    },
});

var AttributeTranslateDialog = weWidgets.Dialog.extend({
    /**
     * @constructor
     */
    init: function (parent, options, node) {
        this._super(parent, _.extend({
            title: _t("Translate Attribute"),
            buttons: [
                {text:  _t("Close"), classes: 'btn-primary o_save_button', click: this.save}
            ],
        }, options || {}));
        this.$target = $(node);
        this.translation = $(node).data('translation');
    },
    /**
     * @override
     */
    start: function () {
        var self = this;
        var $group = $('<div/>', {class: 'form-group'}).appendTo(this.$el);
        _.each(this.translation, function (node, attr) {
            var $node = $(node);
            var $label = $('<label class="control-label"></label>').text(attr);
            var $input = $('<input class="form-control"/>').val($node.html());
            $input.on('change keyup', function () {
                var value = $input.val();
                $node.html(value).trigger('change', node);
                $node.data('$node').attr($node.data('attribute'), value).trigger('translate');
                self.trigger_up('rte_change', {target: node});
            });
            $group.append($label).append($input);
        });
        return this._super.apply(this, arguments);
    }
});

var TranslatorInfoDialog = Dialog.extend({
    template: 'web_editor.TranslatorInfoDialog',
    xmlDependencies: Dialog.prototype.xmlDependencies.concat(
        ['/web_editor/static/src/xml/translator.xml']
    ),

    /**
     * @constructor
     */
    init: function (parent, options) {
        this._super(parent, _.extend({
            title: _t("Translation Info"),
            buttons: [
                {text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
                {text: _t("Ok"), close: true}
            ],
        }, options || {}));
    },

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

    /**
     * Called when the "strong" ok is clicked -> adapt localstorage to make sure
     * the dialog is never displayed again.
     *
     * @private
     */
    _onStrongOk: function () {
        localStorage.setItem(localStorageNoDialogKey, true);
    },
});

var TranslatorMenuBar = Widget.extend({
    template: 'web_editor.editorbar',
    xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
    events: {
        'click [data-action="save"]': '_onSaveClick',
        'click [data-action="cancel"]': '_onCancelClick',
    },
    custom_events: {
        'rte_change': '_onRTEChange',
    },

    /**
     * @constructor
     */
    init: function (parent, $target, lang) {
        this._super.apply(this, arguments);

        var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
        $edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
        this.$target = $edit.not('[data-oe-readonly]');
        var attrs = ['placeholder', 'title', 'alt'];
        _.each(attrs, function (attr) {
            $target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
                var $node = $(this);
                var translation = $node.data('translation') || {};
                var trans = $node.attr(attr);
                var match = trans.match(/<span [^>]*data-oe-translation-id="([0-9]+)"[^>]*>(.*)<\/span>/);
                var $trans = $(trans).addClass('hidden o_editable o_editable_translatable_attribute').appendTo('body');
                $trans.data('$node', $node).data('attribute', attr);
                translation[attr] = $trans[0];
                $node.attr(attr, match[2]);

                var select2 = $node.data('select2');
                if (select2) {
                    select2.blur();
                    $node.on('translate', function () {
                        select2.blur();
                    });
                    $node = select2.container.find('input');
                }
                $node.addClass('o_translatable_attribute').data('translation', translation);
            });
        });
        this.$target_attr = $target.find('.o_translatable_attribute');
        this.$target_attribute = $('.o_editable_translatable_attribute');

        this.lang = lang || weContext.get().lang;

        this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
    },
    /**
     * @override
     */
    start: function () {
        this.$('#web_editor-toolbars').remove();

        var flag = false;
        window.onbeforeunload = function (event) {
            if ($('.o_editable.o_dirty').length && !flag) {
                flag = true;
                setTimeout(function () {
                    flag = false;
                }, 0);
                return _t('This document is not saved!');
            }
        };
        this.$target.addClass('o_editable');
        this.rte.start();
        this.translations = [];
        this._markTranslatableNodes();
        this.$el.show(); // TODO seems useless
        this.trigger('edit');

        if (!localStorage.getItem(localStorageNoDialogKey)) {
            new TranslatorInfoDialog(this).open();
        }

        return this._super.apply(this, arguments);
    },
    /**
     * @override
     */
    destroy: function () {
        this._cancel();
        this._super.apply(this, arguments);
    },

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

    /**
     * Leaves translation mode by hiding the translator bar.
     *
     * @todo should be destroyed?
     * @private
     */
    _cancel: function () {
        var self = this;
        this.rte.cancel();
        this.$target.each(function () {
            $(this).html(self._getTranlationObject(this).value);
        });
        this._unmarkTranslatableNode();
        this.trigger('cancel');
        this.$el.hide();
        window.onbeforeunload = null;
    },
    /**
     * Returns the RTE summernote configuration for translation mode.
     *
     * @private
     * @param {jQuery} $editable
     */
    _getRTEConfig: function ($editable) {
        return {
            airMode : true,
            focus: false,
            airPopover: $editable.data('oe-model') ? [
                ['history', ['undo', 'redo']],
            ] : [
                ['font', ['bold', 'italic', 'underline', 'clear']],
                ['fontsize', ['fontsize']],
                ['color', ['color']],
                ['history', ['undo', 'redo']],
            ],
            styleWithSpan: false,
            inlinemedia : ['p'],
            lang: 'odoo',
            onChange: function (html, $editable) {
                $editable.trigger('content_changed');
            },
        };
    },
    /**
     * @private
     */
    _getTranlationObject: function (node) {
        var $node = $(node);
        var id = +$node.data('oe-translation-id');
        if (!id) {
            id = $node.data('oe-model')+','+$node.data('oe-id')+','+$node.data('oe-field');
        }
        var trans = _.find(this.translations, function (trans) {
            return trans.id === id;
        });
        if (!trans) {
            this.translations.push(trans = {'id': id});
        }
        return trans;
    },
    /**
     * @private
     */
    _markTranslatableNodes: function () {
        var self = this;
        this.$target.add(this.$target_attribute).each(function () {
            var $node = $(this);
            var trans = self._getTranlationObject(this);
            trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
        });
        this.$target.parent().prependEvent('click.translator', function (ev) {
            if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
                return;
            }
            ev.preventDefault();
            ev.stopPropagation();
        });

        // attributes

        this.$target_attr.each(function () {
            var $node = $(this);
            var translation = $node.data('translation');
            _.each(translation, function (node, attr) {
                var trans = self._getTranlationObject(node);
                trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
                $node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
            });
        });

        this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
            if (ev.ctrlKey) {
                return;
            }
            ev.preventDefault();
            ev.stopPropagation();
            if (ev.type !== 'mousedown') {
                return;
            }

            new AttributeTranslateDialog(self, {}, ev.target).open();
        });
    },
    /**
     * Saves the translation and reloads the page to leave edit mode.
     *
     * @private
     * @returns {Deferred} (never resolved as the page is reloading anyway)
     */
    _save: function () {
        return this.rte.save(weContext.get({lang: this.lang})).then(function () {
            window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
            return $.Deferred();
        });
    },
    /**
     * @private
     */
    _unmarkTranslatableNode: function () {
        this.$target.removeClass('o_editable').removeAttr('contentEditable');
        this.$target.parent().off('.translator');
        this.$target_attr.off('.translator');
    },

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

    /**
     * Called when the "cancel" button is clicked -> undo changes and leaves
     * edition.
     *
     * @private
     */
    _onCancelClick: function () {
        this._cancel();
    },
    /**
     * Called when text is edited -> make sure text is not messed up and mark
     * the element as dirty.
     *
     * @private
     * @param {OdooEvent} ev
     */
    _onRTEChange: function (ev) {
        var $node = $(ev.data.target);
        $node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
            var $p = $(this);
            $p.after($p.html()).remove();
        });
        var trans = this._getTranlationObject($node[0]);
        $node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
    },
    /**
     * Called when the "save" button is clicked -> saves the translations.
     *
     * @private
     */
    _onSaveClick: function () {
        this._save();
    },
});

return {
    Class: TranslatorMenuBar,
};
});