odoo.define('web_editor.rte.summernote', function (require) { 'use strict'; var Class = require('web.Class'); const concurrency = require('web.concurrency'); var core = require('web.core'); // Use the top window's core.bus for dialog events so that they take the whole window // instead of being confined to an iframe. This means that the event triggered on // the bus by summernote in an iframe will be caught by the wysiwyg's SummernoteManager // outside the iframe. const topBus = window.top.odoo.__DEBUG__.services['web.core'].bus; const {ColorpickerWidget} = require('web.Colorpicker'); var ColorPaletteWidget = require('web_editor.ColorPalette').ColorPaletteWidget; var mixins = require('web.mixins'); var fonts = require('wysiwyg.fonts'); var rte = require('web_editor.rte'); var ServicesMixin = require('web.ServicesMixin'); var weWidgets = require('wysiwyg.widgets'); var _t = core._t; // 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; // Summernote uses execCommand and, worth, obsolete queryCommandState function // to customize the edited content. Here we try to hack the function to solve // some problems by making the DOM and style easier to understand for the // base function for the duration of their executions. This won't obviously // solves all problems but this is an improvement while waiting for the new // Odoo editor coming in future versions. function protectCommand(callback) { return function () { var rng = range.create(); var $sc = (rng && rng.sc) ? $(rng.sc).parents(':o_editable').last() : $(); var $ec = (rng && rng.ec) ? $(rng.ec).parents(':o_editable').last() : $(); $sc.addClass('o_we_command_protector'); $ec.addClass('o_we_command_protector'); var restore = function () { $sc.removeClass('o_we_command_protector'); $ec.removeClass('o_we_command_protector'); }; var result; try { result = callback.apply(this, arguments); } catch (err) { restore(); throw err; } restore(); return result; }; } document.execCommand = protectCommand(document.execCommand); document.queryCommandState = protectCommand(document.queryCommandState); var tplButton = renderer.getTemplate().button; var tplIconButton = renderer.getTemplate().iconButton; var tplDropdown = renderer.getTemplate().dropdown; const processAndApplyColor = function (target, eventName, color, preview) { if (!color) { color = 'inherit'; } else if (!ColorpickerWidget.isCSSColor(color)) { color = (eventName === "foreColor" ? 'text-' : 'bg-') + color; } var layoutInfo = dom.makeLayoutInfo(target); $.summernote.pluginEvents[eventName](undefined, eventHandler.modules.editor, layoutInfo, color, preview); }; // Update and change the popovers content, and add history button renderer.createPalette = function ($container, options) { const $dropdownContent = $container.find(".colorPalette"); // The editor's root widget can be website or web's root widget and cannot be properly retrieved... const parent = odoo.__DEBUG__.services['root.widget']; _.each($dropdownContent, elem => { const eventName = elem.dataset.eventName; let colorpicker = null; const mutex = new concurrency.MutexedDropPrevious(); const $dropdown = $(elem).closest('.btn-group, .dropdown'); let manualOpening = false; // Prevent dropdown closing on colorpicker click $dropdown.on('hide.bs.dropdown', ev => { return !(ev.clickEvent && ev.clickEvent.originalEvent && ev.clickEvent.originalEvent.__isColorpickerClick); }); $dropdown.on('show.bs.dropdown', () => { if (manualOpening) { return true; } mutex.exec(() => { const oldColorpicker = colorpicker; const hookEl = oldColorpicker ? oldColorpicker.el : elem; const r = range.create(); const targetNode = r.sc; const targetElement = targetNode.nodeType === Node.ELEMENT_NODE ? targetNode : targetNode.parentNode; colorpicker = new ColorPaletteWidget(parent, { excluded: ['transparent_grayscale'], $editable: rte.Class.prototype.editable(), // Our parent is the root widget, we can't retrieve the editable section from it... selectedColor: $(targetElement).css(eventName === "foreColor" ? 'color' : 'backgroundColor'), }); colorpicker.on('custom_color_picked color_picked', null, ev => { processAndApplyColor(ev.data.target, eventName, ev.data.color); }); colorpicker.on('color_hover color_leave', null, ev => { processAndApplyColor(ev.data.target, eventName, ev.data.color, true); }); colorpicker.on('enter_key_color_colorpicker', null, () => { $dropdown.children('.dropdown-toggle').dropdown('hide'); }); return colorpicker.replace(hookEl).then(() => { if (oldColorpicker) { oldColorpicker.destroy(); } manualOpening = true; $dropdown.children('.dropdown-toggle').dropdown('show'); manualOpening = false; }); }); return false; }); }); }; var fn_tplPopovers = renderer.tplPopovers; renderer.tplPopovers = function (lang, options) { var $popover = $(fn_tplPopovers.call(this, lang, options)); var $imagePopover = $popover.find('.note-image-popover'); var $linkPopover = $popover.find('.note-link-popover'); var $airPopover = $popover.find('.note-air-popover'); //////////////// image popover // add center button for images $(tplIconButton('fa fa-align-center', { title: _t('Center'), event: 'floatMe', value: 'center' })).insertAfter($imagePopover.find('[data-event="floatMe"][data-value="left"]')); $imagePopover.find('button[data-event="removeMedia"]').parent().remove(); $imagePopover.find('button[data-event="floatMe"][data-value="none"]').remove(); // padding button var $padding = $('
'); $padding.insertBefore($imagePopover.find('.btn-group:first')); var dropdown_content = [ '
  • '+_t('None')+'
  • ', '
  • '+_t('Small')+'
  • ', '
  • '+_t('Medium')+'
  • ', '
  • '+_t('Large')+'
  • ', '
  • '+_t('Xl')+'
  • ', ]; $(tplIconButton('fa fa-plus-square-o', { title: _t('Padding'), dropdown: tplDropdown(dropdown_content) })).appendTo($padding); // circle, boxed... options became toggled $imagePopover.find('[data-event="imageShape"]:not([data-value])').remove(); var $button = $(tplIconButton('fa fa-sun-o', { title: _t('Shadow'), event: 'imageShape', value: 'shadow' })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="rounded-circle"]')); // add spin for fa var $spin = $('
    ').insertAfter($button.parent()); $(tplIconButton('fa fa-refresh', { title: _t('Spin'), event: 'imageShape', value: 'fa-spin' })).appendTo($spin); // resize for fa var $resizefa = $('
    ') .insertAfter($imagePopover.find('.btn-group:has([data-event="resize"])')); for (var size=1; size<=5; size++) { $(tplButton(''+size+'x', { title: size+"x", event: 'resizefa', value: size+'' })).appendTo($resizefa); } var $colorfa = $airPopover.find('.note-color').clone(); $colorfa.find(".dropdown-menu").css('min-width', '172px'); $resizefa.after($colorfa); // show dialog box and delete var $imageprop = $('
    '); $imageprop.appendTo($imagePopover.find('.popover-body')); $(tplIconButton('fa fa-file-image-o', { title: _t('Edit'), event: 'showImageDialog' })).appendTo($imageprop); $(tplIconButton('fa fa-trash-o', { title: _t('Remove'), event: 'delete' })).appendTo($imageprop); $(tplIconButton('fa fa-crop', { title: _t('Crop Image'), event: 'cropImage', })).insertAfter($imagePopover.find('[data-event="imageShape"][data-value="img-thumbnail"]')); $imagePopover.find('.popover-body').append($airPopover.find(".note-history").clone()); $imagePopover.find('[data-event="showImageDialog"]').before($airPopover.find('[data-event="showLinkDialog"]').clone()); var $alt = $('
    '); $alt.appendTo($imagePopover.find('.popover-body')); $alt.append(''); //////////////// link popover $linkPopover.find('.popover-body').append($airPopover.find(".note-history").clone()); $linkPopover.find('button[data-event="showLinkDialog"] i').attr("class", "fa fa-link"); $linkPopover.find('button[data-event="unlink"]').before($airPopover.find('button[data-event="showImageDialog"]').clone()); //////////////// text/air popover //// highlight the text format $airPopover.find('.note-style .dropdown-toggle').on('mousedown', function () { var $format = $airPopover.find('[data-event="formatBlock"]'); var node = range.create().sc; var formats = $format.map(function () { return $(this).data("value"); }).get(); while (node && (!node.tagName || (!node.tagName || formats.indexOf(node.tagName.toLowerCase()) === -1))) { node = node.parentNode; } $format.removeClass('active'); $format.filter('[data-value="'+(node ? node.tagName.toLowerCase() : "p")+'"]') .addClass("active"); }); //////////////// tooltip setTimeout(function () { $airPopover.add($linkPopover).add($imagePopover).find("button") .tooltip('dispose') .tooltip({ container: 'body', trigger: 'hover', placement: 'bottom' }).on('click', function () {$(this).tooltip('hide');}); }); return $popover; }; var fn_boutton_update = eventHandler.modules.popover.button.update; eventHandler.modules.popover.button.update = function ($container, oStyle) { // stop animation when edit content var previous = $(".note-control-selection").data('target'); if (previous) { var $previous = $(previous); $previous.css({"-webkit-animation-play-state": "", "animation-play-state": "", "-webkit-transition": "", "transition": "", "-webkit-animation": "", "animation": ""}); $previous.find('.o_we_selected_image').addBack('.o_we_selected_image').removeClass('o_we_selected_image'); } // end fn_boutton_update.call(this, $container, oStyle); $container.find('.note-color').removeClass('d-none'); if (oStyle.image) { $container.find('[data-event]').removeClass("active"); $container.find('a[data-event="padding"][data-value="small"]').toggleClass("active", $(oStyle.image).hasClass("padding-small")); $container.find('a[data-event="padding"][data-value="medium"]').toggleClass("active", $(oStyle.image).hasClass("padding-medium")); $container.find('a[data-event="padding"][data-value="large"]').toggleClass("active", $(oStyle.image).hasClass("padding-large")); $container.find('a[data-event="padding"][data-value="xl"]').toggleClass("active", $(oStyle.image).hasClass("padding-xl")); $container.find('a[data-event="padding"][data-value=""]').toggleClass("active", !$container.find('li a.active[data-event="padding"]').length); $(oStyle.image).addClass('o_we_selected_image'); if (dom.isImgFont(oStyle.image)) { $container.find('.note-fore-color-preview > button > .caret').css('border-bottom-color', $(oStyle.image).css('color')); $container.find('.note-back-color-preview > button > .caret').css('border-bottom-color', $(oStyle.image).css('background-color')); $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-value="img-thumbnail"])').addClass('d-none'); $container.find('.only_fa').removeClass('d-none'); $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x")); $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x")); $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x")); $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x")); $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length); $container.find('button[data-event="cropImage"]').addClass('d-none'); $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin")); $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow")); $container.find('.btn-group:has(button[data-event="imageShape"])').removeClass("d-none"); } else { $container.find('.d-none:not(.only_fa, .note-recent-color)').removeClass('d-none'); $container.find('button[data-event="cropImage"]').removeClass('d-none'); $container.find('.only_fa').addClass('d-none'); var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/); if (width) { width = width[2]; } $container.find('button[data-event="resize"][data-value="auto"]').toggleClass("active", width !== "100%" && width !== "50%" && width !== "25%"); $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%"); $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%"); $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%"); $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow")); if (!$(oStyle.image).is("img")) { $container.find('.btn-group:has(button[data-event="imageShape"])').addClass('d-none'); } $container.find('.note-color').addClass('d-none'); } $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("float-left")); $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("d-block mx-auto")); $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("float-right")); $(oStyle.image).trigger('attributes_change'); } else { $container.find('.note-fore-color-preview > button > .caret').css('border-bottom-color', oStyle.color); $container.find('.note-back-color-preview > button > .caret').css('border-bottom-color', oStyle['background-color']); } }; var fn_toolbar_boutton_update = eventHandler.modules.toolbar.button.update; eventHandler.modules.toolbar.button.update = function ($container, oStyle) { fn_toolbar_boutton_update.call(this, $container, oStyle); $container.find('button[data-event="insertUnorderedList"]').toggleClass("active", $(oStyle.ancestors).is('ul:not(.o_checklist)')); $container.find('button[data-event="insertOrderedList"]').toggleClass("active", $(oStyle.ancestors).is('ol')); $container.find('button[data-event="insertCheckList"]').toggleClass("active", $(oStyle.ancestors).is('ul.o_checklist')); }; 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(); } const $externalHistoryButtons = $('.o_we_external_history_buttons'); if ($externalHistoryButtons.length) { const $noteHistory = $('.note-history'); $noteHistory.addClass('d-none'); $externalHistoryButtons.find(':first-child').prop('disabled', $noteHistory.find('[data-event=undo]').prop('disabled')); $externalHistoryButtons.find(':last-child').prop('disabled', $noteHistory.find('[data-event=redo]').prop('disabled')); } $popover.trigger('summernote_popover_update_call'); }; var fn_handle_update = eventHandler.modules.handle.update; eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) { fn_handle_update.call(this, $handle, oStyle, isAirMode); if (oStyle.image) { $handle.find('.note-control-selection').hide(); } }; // Hack for image and link editor function getImgTarget($editable) { var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined; return $(".note-control-selection", $handle).data('target'); } eventHandler.modules.editor.padding = function ($editable, sValue) { var $target = $(getImgTarget($editable)); var paddings = "small medium large xl".split(/\s+/); $editable.data('NoteHistory').recordUndo(); if (sValue.length) { paddings.splice(paddings.indexOf(sValue),1); $target.toggleClass('padding-'+sValue); } $target.removeClass("padding-" + paddings.join(" padding-")); }; eventHandler.modules.editor.resize = function ($editable, sValue) { var $target = $(getImgTarget($editable)); $editable.data('NoteHistory').recordUndo(); var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/); if (width) { width = width[2]/100; } $target.css('width', (width !== sValue && sValue !== "auto") ? (sValue * 100) + '%' : ''); }; eventHandler.modules.editor.resizefa = function ($editable, sValue) { var $target = $(getImgTarget($editable)); $editable.data('NoteHistory').recordUndo(); $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, '')); if (+sValue > 1) { $target.addClass('fa-'+sValue+'x'); } }; eventHandler.modules.editor.floatMe = function ($editable, sValue) { var $target = $(getImgTarget($editable)); $editable.data('NoteHistory').recordUndo(); switch (sValue) { case 'center': $target.toggleClass('d-block mx-auto').removeClass('float-right float-left'); break; case 'left': $target.toggleClass('float-left').removeClass('float-right d-block mx-auto'); break; case 'right': $target.toggleClass('float-right').removeClass('float-left d-block mx-auto'); break; } }; eventHandler.modules.editor.imageShape = function ($editable, sValue) { var $target = $(getImgTarget($editable)); $editable.data('NoteHistory').recordUndo(); $target.toggleClass(sValue); }; eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, linkInfo) { $editable.data('range').select(); $editable.data('NoteHistory').recordUndo(); var commonAncestor = linkInfo.range.commonAncestor(); if (commonAncestor && commonAncestor.closest) { var link = commonAncestor.closest('a'); linkInfo.className = link && link.className; } var def = new $.Deferred(); topBus.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; }; var originalShowImageDialog = eventHandler.modules.imageDialog.showImageDialog; eventHandler.modules.imageDialog.showImageDialog = function ($editable) { var options = $editable.closest('.o_editable, .note-editor').data('options'); if (options.disableFullMediaDialog) { return originalShowImageDialog.apply(this, arguments); } 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]; topBus.trigger('media_dialog_demand', { $editable: $editable, media: media, options: { onUpload: $editable.data('callbacks').onUpload, noVideos: options && options.noVideos, }, onSave: function (newMedia) { if (!newMedia) { return; } if (media) { $(media).replaceWith(newMedia); } else { r.insertNode(newMedia); } }, }); return new $.Deferred().reject(); }; $.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) { var $editable = layoutInfo.editable(); var $selection = layoutInfo.handle().find('.note-control-selection'); topBus.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'); topBus.trigger('crop_image_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

    ')[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(//); 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) { 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 = $('