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"> </div>'+ '<div class="media_iframe_video_size" contentEditable="false"> </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(/</, "<").replace(/>/, ">").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 }; });
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(/<span data-oe-model.+?>(.+?)<\/span>/, '\$1')); }); } var data = { 'translatable': translatable, 'edit_translations': edit_translations, 'Class': Translate, }; return data; });
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; }, }); });
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(/"/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; });
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"); }, }); });
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 || ""); }, }); });
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; } }); });
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, }; });