',
+ ];
+ $(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('[data-event="customColor"][data-value="foreColor"]').attr('data-color', $(oStyle.image).css('color'));
+ $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', $(oStyle.image).css('background-color'));
+ $container.find('.note-fore-color-preview > button').css('border-bottom-color', $(oStyle.image).css('color'));
+ $container.find('.note-back-color-preview > button').css('border-bottom-color', $(oStyle.image).css('background-color'));
+
+ $container.find('.btn-group:not(.only_fa):has(button[data-event="resize"],button[data-value="img-thumbnail"])').addClass('d-none');
+ $container.find('.only_fa').removeClass('d-none');
+ $container.find('button[data-event="resizefa"][data-value="2"]').toggleClass("active", $(oStyle.image).hasClass("fa-2x"));
+ $container.find('button[data-event="resizefa"][data-value="3"]').toggleClass("active", $(oStyle.image).hasClass("fa-3x"));
+ $container.find('button[data-event="resizefa"][data-value="4"]').toggleClass("active", $(oStyle.image).hasClass("fa-4x"));
+ $container.find('button[data-event="resizefa"][data-value="5"]').toggleClass("active", $(oStyle.image).hasClass("fa-5x"));
+ $container.find('button[data-event="resizefa"][data-value="1"]').toggleClass("active", !$container.find('.active[data-event="resizefa"]').length);
+
+ $container.find('button[data-event="imageShape"][data-value="fa-spin"]').toggleClass("active", $(oStyle.image).hasClass("fa-spin"));
+ $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
+ $container.find('.btn-group:has(button[data-event="imageShape"])').removeClass("d-none");
+
+ } else {
+ $container.find('.d-none:not(.only_fa, .note-recent-color)').removeClass('d-none');
+ $container.find('.only_fa').addClass('d-none');
+ var width = ($(oStyle.image).attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+%)/);
+ if (width) {
+ width = width[2];
+ }
+ $container.find('button[data-event="resize"][data-value="auto"]').toggleClass("active", width !== "100%" && width !== "50%" && width !== "25%");
+ $container.find('button[data-event="resize"][data-value="1"]').toggleClass("active", width === "100%");
+ $container.find('button[data-event="resize"][data-value="0.5"]').toggleClass("active", width === "50%");
+ $container.find('button[data-event="resize"][data-value="0.25"]').toggleClass("active", width === "25%");
+
+ $container.find('button[data-event="imageShape"][data-value="shadow"]').toggleClass("active", $(oStyle.image).hasClass("shadow"));
+
+ if (!$(oStyle.image).is("img")) {
+ $container.find('.btn-group:has(button[data-event="imageShape"])').addClass('d-none');
+ }
+
+ $container.find('.note-color').addClass('d-none');
+ }
+
+ $container.find('button[data-event="floatMe"][data-value="left"]').toggleClass("active", $(oStyle.image).hasClass("float-left"));
+ $container.find('button[data-event="floatMe"][data-value="center"]').toggleClass("active", $(oStyle.image).hasClass("d-block mx-auto"));
+ $container.find('button[data-event="floatMe"][data-value="right"]').toggleClass("active", $(oStyle.image).hasClass("float-right"));
+
+ $(oStyle.image).trigger('attributes_change');
+ } else {
+ $container.find('[data-event="customColor"][data-value="foreColor"]').attr('data-color', oStyle.color);
+ $container.find('[data-event="customColor"][data-value="backColor"]').attr('data-color', oStyle['background-color']);
+ $container.find('.note-fore-color-preview > button').css('border-bottom-color', oStyle.color);
+ $container.find('.note-back-color-preview > button').css('border-bottom-color', oStyle['background-color']);
+ }
+};
+
+var fn_popover_update = eventHandler.modules.popover.update;
+eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
+ var $imagePopover = $popover.find('.note-image-popover');
+ var $linkPopover = $popover.find('.note-link-popover');
+ var $airPopover = $popover.find('.note-air-popover');
+
+ fn_popover_update.call(this, $popover, oStyle, isAirMode);
+
+ if (oStyle.image) {
+ if (oStyle.image.parentNode.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
+ oStyle.image = oStyle.image.parentNode;
+ }
+ var alt = $(oStyle.image).attr("alt");
+
+ $imagePopover.find('.o_image_alt').text( (alt || "").replace(/"/g, '"') ).parent().toggle(oStyle.image.tagName === "IMG");
+ $imagePopover.show();
+
+ // for video tag (non-void) we select the range over the tag,
+ // for other media types we get the first descendant leaf element
+ var target_node = oStyle.image;
+ if (!oStyle.image.className.match(/(^|\s)media_iframe_video(\s|$)/i)) {
+ target_node = dom.firstChild(target_node);
+ }
+ range.createFromNode(target_node).select();
+ // save range on the editor so it is not lost if restored
+ eventHandler.modules.editor.saveRange(dom.makeLayoutInfo(target_node).editable());
+ } else {
+ $(".note-control-selection").hide();
+ }
+
+ if (oStyle.image || (oStyle.range && (!oStyle.range.isCollapsed() || (oStyle.range.sc.tagName && !dom.isAnchor(oStyle.range.sc)))) || (oStyle.image && !$(oStyle.image).closest('a').length)) {
+ $linkPopover.hide();
+ oStyle.anchor = false;
+ }
+
+ if (oStyle.image || oStyle.anchor || (oStyle.range && !$(oStyle.range.sc).closest('.note-editable').length)) {
+ $airPopover.hide();
+ } else {
+ $airPopover.show();
+ }
+};
+
+var fn_handle_update = eventHandler.modules.handle.update;
+eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) {
+ fn_handle_update.call(this, $handle, oStyle, isAirMode);
+ if (oStyle.image) {
+ $handle.find('.note-control-selection').hide();
+ }
+};
+
+// Hack for image and link editor
+function getImgTarget($editable) {
+ var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined;
+ return $(".note-control-selection", $handle).data('target');
+}
+eventHandler.modules.editor.padding = function ($editable, sValue) {
+ var $target = $(getImgTarget($editable));
+ var paddings = "small medium large xl".split(/\s+/);
+ $editable.data('NoteHistory').recordUndo();
+ if (sValue.length) {
+ paddings.splice(paddings.indexOf(sValue),1);
+ $target.toggleClass('padding-'+sValue);
+ }
+ $target.removeClass("padding-" + paddings.join(" padding-"));
+};
+eventHandler.modules.editor.resize = function ($editable, sValue) {
+ var $target = $(getImgTarget($editable));
+ $editable.data('NoteHistory').recordUndo();
+ var width = ($target.attr('style') || '').match(/(^|;|\s)width:\s*([0-9]+)%/);
+ if (width) {
+ width = width[2]/100;
+ }
+ $target.css('width', (width !== sValue && sValue !== "auto") ? (sValue * 100) + '%' : '');
+};
+eventHandler.modules.editor.resizefa = function ($editable, sValue) {
+ var $target = $(getImgTarget($editable));
+ $editable.data('NoteHistory').recordUndo();
+ $target.attr('class', $target.attr('class').replace(/\s*fa-[0-9]+x/g, ''));
+ if (+sValue > 1) {
+ $target.addClass('fa-'+sValue+'x');
+ }
+};
+eventHandler.modules.editor.floatMe = function ($editable, sValue) {
+ var $target = $(getImgTarget($editable));
+ $editable.data('NoteHistory').recordUndo();
+ switch (sValue) {
+ case 'center': $target.toggleClass('d-block mx-auto').removeClass('float-right float-left'); break;
+ case 'left': $target.toggleClass('float-left').removeClass('float-right d-block mx-auto'); break;
+ case 'right': $target.toggleClass('float-right').removeClass('float-left d-block mx-auto'); break;
+ }
+};
+eventHandler.modules.editor.imageShape = function ($editable, sValue) {
+ var $target = $(getImgTarget($editable));
+ $editable.data('NoteHistory').recordUndo();
+ $target.toggleClass(sValue);
+};
+
+eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, linkInfo) {
+ $editable.data('range').select();
+ $editable.data('NoteHistory').recordUndo();
+
+ var def = new $.Deferred();
+ core.bus.trigger('link_dialog_demand', {
+ $editable: $editable,
+ linkInfo: linkInfo,
+ onSave: function (linkInfo) {
+ linkInfo.range.select();
+ $editable.data('range', linkInfo.range);
+ def.resolve(linkInfo);
+ $editable.trigger('keyup');
+ $('.note-popover .note-link-popover').show();
+ },
+ onCancel: def.reject.bind(def),
+ });
+ return def;
+};
+eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
+ var r = $editable.data('range');
+ if (r.sc.tagName && r.sc.childNodes.length) {
+ r.sc = r.sc.childNodes[r.so];
+ }
+ var media = $(r.sc).parents().addBack().filter(function (i, el) {
+ return dom.isImg(el);
+ })[0];
+ core.bus.trigger('media_dialog_demand', {
+ $editable: $editable,
+ media: media,
+ options: {
+ lastFilters: ['background'],
+ onUpload: $editable.data('callbacks').onUpload,
+ noVideos: $editable.data('oe-model') === "mail.compose.message",
+ },
+ });
+ return new $.Deferred().reject();
+};
+$.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) {
+ var $editable = layoutInfo.editable();
+ var $selection = layoutInfo.handle().find('.note-control-selection');
+ core.bus.trigger('alt_dialog_demand', {
+ $editable: $editable,
+ media: $selection.data('target'),
+ });
+};
+$.summernote.pluginEvents.customColor = function (event, editor, layoutInfo, customColor) {
+ var defaultColor = event.target.dataset.color;
+ core.bus.trigger('color_picker_dialog_demand', {
+ color: defaultColor === 'rgba(0, 0, 0, 0)' ? 'rgb(255, 0, 0)' : defaultColor,
+ onSave: function (color) {
+ var $palettes = $(event.currentTarget).find('.note-custom-color-palette > .note-color-row')
+ .append((''));
+ $palettes.filter(':odd').find('button:not([data-event])').attr('data-event', 'backColor');
+ $palettes.filter(':even').find('button:not([data-event])').attr('data-event', 'foreColor');
+ if (customColor === 'foreColor') {
+ $(event.currentTarget).find('.note-fore-color-preview > button').css('border-bottom-color', color);
+ $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, color);
+ } else {
+ $(event.currentTarget).find('.note-back-color-preview > button').css('border-bottom-color', color);
+ $.summernote.pluginEvents.backColor(event, editor, layoutInfo, color);
+ }
+ }
+ });
+};
+$.summernote.pluginEvents.cropImage = function (event, editor, layoutInfo, sorted) {
+ var $editable = layoutInfo.editable();
+ var $selection = layoutInfo.handle().find('.note-control-selection');
+ core.bus.trigger('crop_image_dialog_demand', {
+ $editable: $editable,
+ media: $selection.data('target'),
+ });
+};
+
+// Utils
+var fn_is_void = dom.isVoid || function () {};
+dom.isVoid = function (node) {
+ return fn_is_void(node) || dom.isImgFont(node) || (node && node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i));
+};
+var fn_is_img = dom.isImg || function () {};
+dom.isImg = function (node) {
+ return fn_is_img(node) || dom.isImgFont(node) || (node && (node.nodeName === "IMG" || (node.className && node.className.match(/(^|\s)(media_iframe_video|o_image)(\s|$)/i)) ));
+};
+var fn_is_forbidden_node = dom.isForbiddenNode || function () {};
+dom.isForbiddenNode = function (node) {
+ if (node.tagName === "BR") {
+ return false;
+ }
+ return fn_is_forbidden_node(node) || $(node).is(".media_iframe_video");
+};
+var fn_is_img_font = dom.isImgFont || function () {};
+dom.isImgFont = function (node) {
+ if (fn_is_img_font(node)) return true;
+
+ var nodeName = node && node.nodeName.toUpperCase();
+ var className = (node && node.className || "");
+ if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
+ var classNames = className.split(/\s+/);
+ for (var k=0; k
')[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 = $('').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 () {
+ // Do not show tooltip on double-click and if there is already one
+ if (!show_tooltip || $target.attr('title') !== undefined) {
+ return;
+ }
+ $target.tooltip({title: _t('Double-click to edit'), trigger: 'manuel', container: 'body'}).tooltip('show');
+ setTimeout(function () {
+ $target.tooltip('dispose');
+ }, 800);
+ }, 400);
+ });
+ }
+};
+var fn_detach = eventHandler.detach;
+eventHandler.detach = function (oLayoutInfo, options) {
+ fn_detach.call(this, oLayoutInfo, options);
+ oLayoutInfo.editable().off('mousedown');
+ oLayoutInfo.editor().off("dragstart");
+ oLayoutInfo.editor().off('click');
+ $(document).off('mousedown', summernote_mousedown);
+ $(document).off('mouseup', summernote_mouseup);
+ oLayoutInfo.editor().off("dblclick");
+ $(document).off("keyup", reRangeSelectKey);
+};
+
+// Translation for odoo
+$.summernote.lang.odoo = {
+ font: {
+ bold: _t('Bold'),
+ italic: _t('Italic'),
+ underline: _t('Underline'),
+ strikethrough: _t('Strikethrough'),
+ subscript: _t('Subscript'),
+ superscript: _t('Superscript'),
+ clear: _t('Remove Font Style'),
+ height: _t('Line Height'),
+ name: _t('Font Family'),
+ size: _t('Font Size')
+ },
+ image: {
+ image: _t('File / Image'),
+ insert: _t('Insert Image'),
+ resizeFull: _t('Resize Full'),
+ resizeHalf: _t('Resize Half'),
+ resizeQuarter: _t('Resize Quarter'),
+ floatLeft: _t('Float Left'),
+ floatRight: _t('Float Right'),
+ floatNone: _t('Float None'),
+ dragImageHere: _t('Drag an image here'),
+ selectFromFiles: _t('Select from files'),
+ url: _t('Image URL'),
+ remove: _t('Remove Image')
+ },
+ link: {
+ link: _t('Link'),
+ insert: _t('Insert Link'),
+ unlink: _t('Unlink'),
+ edit: _t('Edit'),
+ textToDisplay: _t('Text to display'),
+ url: _t('To what URL should this link go?'),
+ openInNewWindow: _t('Open in new window')
+ },
+ video: {
+ video: _t('Video'),
+ videoLink: _t('Video Link'),
+ insert: _t('Insert Video'),
+ url: _t('Video URL?'),
+ providers: _t('(YouTube, Vimeo, Vine, Instagram, DailyMotion or Youku)')
+ },
+ table: {
+ table: _t('Table')
+ },
+ hr: {
+ insert: _t('Insert Horizontal Rule')
+ },
+ style: {
+ style: _t('Style'),
+ normal: _t('Normal'),
+ blockquote: _t('Quote'),
+ pre: _t('Code'),
+ small: _t('Small'),
+ h1: _t('Header 1'),
+ h2: _t('Header 2'),
+ h3: _t('Header 3'),
+ h4: _t('Header 4'),
+ h5: _t('Header 5'),
+ h6: _t('Header 6')
+ },
+ lists: {
+ unordered: _t('Unordered list'),
+ ordered: _t('Ordered list')
+ },
+ options: {
+ help: _t('Help'),
+ fullscreen: _t('Full Screen'),
+ codeview: _t('Code View')
+ },
+ paragraph: {
+ paragraph: _t('Paragraph'),
+ outdent: _t('Outdent'),
+ indent: _t('Indent'),
+ left: _t('Align left'),
+ center: _t('Align center'),
+ right: _t('Align right'),
+ justify: _t('Justify full')
+ },
+ color: {
+ custom: _t('Custom Color'),
+ background: _t('Background Color'),
+ foreground: _t('Font Color'),
+ transparent: _t('Transparent'),
+ setTransparent: _t('None'),
+ },
+ shortcut: {
+ shortcuts: _t('Keyboard shortcuts'),
+ close: _t('Close'),
+ textFormatting: _t('Text formatting'),
+ action: _t('Action'),
+ paragraphFormatting: _t('Paragraph formatting'),
+ documentStyle: _t('Document Style')
+ },
+ history: {
+ undo: _t('Undo'),
+ redo: _t('Redo')
+ }
+};
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+/**
+ * @todo get rid of this. This has been implemented as a fix to be able to
+ * instantiate media, link and alt dialogs outside the main editor: in the
+ * simple HTML fields and forum textarea.
+ */
+var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
+ /**
+ * @constructor
+ */
+ init: function (parent) {
+ mixins.EventDispatcherMixin.init.call(this);
+ this.setParent(parent);
+
+ core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
+ core.bus.on('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
+ core.bus.on('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
+ core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
+ core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
+ },
+ /**
+ * @override
+ */
+ destroy: function () {
+ mixins.EventDispatcherMixin.destroy.call(this);
+
+ core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
+ core.bus.off('color_picker_dialog_demand', this, this._onColorPickerDialogDemand);
+ core.bus.off('crop_image_dialog_demand', this, this._onCropImageDialogDemand);
+ core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
+ core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * Called when a demand to open a alt dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onAltDialogDemand: function (data) {
+ if (data.__alreadyDone) {
+ return;
+ }
+ data.__alreadyDone = true;
+ var altDialog = new weWidgets.AltDialog(this,
+ data.options || {},
+ data.$editable,
+ data.media
+ );
+ if (data.onSave) {
+ altDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ altDialog.on('cancel', this, data.onCancel);
+ }
+ altDialog.open();
+ },
+/**
+ * Called when a demand to open a color picker dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onColorPickerDialogDemand: function (data) {
+ if (data.__alreadyDone) {
+ return;
+ }
+ data.__alreadyDone = true;
+ var colorpicker = new ColorpickerDialog(this, {
+ defaultColor: data.color,
+ });
+ if (data.onSave) {
+ colorpicker.on('colorpicker:saved', this, function (ev) {
+ data.onSave(ev.data.cssColor);
+ });
+ }
+ colorpicker.open();
+ },
+ /**
+ * Called when a demand to open a crop dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onCropImageDialogDemand: function (data) {
+ if (data.__alreadyDone) {
+ return;
+ }
+ data.__alreadyDone = true;
+ var cropImageDialog = new weWidgets.CropImageDialog(this,
+ _.extend({
+ res_model: data.$editable.data('oe-model'),
+ res_id: data.$editable.data('oe-id'),
+ }, data.options || {}),
+ data.$editable,
+ data.media
+ );
+ if (data.onSave) {
+ cropImageDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ cropImageDialog.on('cancel', this, data.onCancel);
+ }
+ cropImageDialog.open();
+ },
+ /**
+ * Called when a demand to open a link dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onLinkDialogDemand: function (data) {
+ if (data.__alreadyDone) {
+ return;
+ }
+ data.__alreadyDone = true;
+ var linkDialog = new weWidgets.LinkDialog(this,
+ data.options || {},
+ data.$editable,
+ data.linkInfo
+ );
+ if (data.onSave) {
+ linkDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ linkDialog.on('cancel', this, data.onCancel);
+ }
+ linkDialog.open();
+ },
+ /**
+ * Called when a demand to open a media dialog is received on the bus.
+ *
+ * @private
+ * @param {Object} data
+ */
+ _onMediaDialogDemand: function (data) {
+ if (data.__alreadyDone) {
+ return;
+ }
+ data.__alreadyDone = true;
+
+ var mediaDialog = new weWidgets.MediaDialog(this,
+ _.extend({
+ res_model: data.$editable.data('oe-model'),
+ res_id: data.$editable.data('oe-id'),
+ domain: data.$editable.data('oe-media-domain'),
+ }, data.options),
+ data.$editable,
+ data.media
+ );
+ if (data.onSave) {
+ mediaDialog.on('save', this, data.onSave);
+ }
+ if (data.onCancel) {
+ mediaDialog.on('cancel', this, data.onCancel);
+ }
+ mediaDialog.open();
+ },
+});
+/**
+ * @todo cannot do this without include because it would make a loop in the
+ * JS module dependencies otherwise.
+ */
+rte.Class.include({
+ /**
+ * @override
+ */
+ start: function () {
+ this._summernoteManager = new SummernoteManager(this);
+ return this._super.apply(this, arguments);
+ },
+ /**
+ * @override
+ */
+ cancel: function () {
+ this._super.apply(this, arguments);
+ this._summernoteManager.destroy();
+ },
+});
+return SummernoteManager;
+});
diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js b/addons/web_editor/static/src/js/editor/snippets.editor.js
similarity index 84%
rename from addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
rename to addons/web_editor/static/src/js/editor/snippets.editor.js
index 4193aef5332..776d9992bb7 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.editor.js
+++ b/addons/web_editor/static/src/js/editor/snippets.editor.js
@@ -5,7 +5,6 @@ var core = require('web.core');
var dom = require('web.dom');
var Widget = require('web.Widget');
var options = require('web_editor.snippets.options');
-var Wysiwyg = require('web_editor.wysiwyg');
var _t = core._t;
@@ -25,7 +24,6 @@ var SnippetEditor = Widget.extend({
'click .oe_snippet_parent': '_onParentButtonClick',
'click .oe_snippet_clone': '_onCloneClick',
'click .oe_snippet_remove': '_onRemoveClick',
- 'mousedown .oe_options [data-toggle="dropdown"]:first': '_onOpenCusomize',
},
custom_events: {
cover_update: '_onCoverUpdate',
@@ -37,15 +35,9 @@ var SnippetEditor = Widget.extend({
* @param {Widget} parent
* @param {Element} target
* @param templateOptions
- * @param {jQuery} $editable
- * @param {Object} options
*/
- init: function (parent, target, templateOptions, $editable, options) {
+ init: function (parent, target, templateOptions) {
this._super.apply(this, arguments);
- this.options = options;
- this.$editable = $editable;
- this.ownerDocument = this.$editable[0].ownerDocument;
- this.$body = $(this.ownerDocument.body);
this.$target = $(target);
this.$target.data('snippet-editor', this);
this.templateOptions = templateOptions;
@@ -67,13 +59,13 @@ var SnippetEditor = Widget.extend({
defs.push(this._initializeOptions());
// Initialize move/clone/remove buttons
- if (!this.options.isEditableNode(this.$target[0])) {
+ if (!this.$target.parent().is(':o_editable')) {
this.$el.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove();
} else {
this.dropped = false;
this.$el.draggable({
greedy: true,
- appendTo: this.$body,
+ appendTo: 'body',
cursor: 'move',
handle: '.oe_snippet_move',
cursorAt: {
@@ -84,7 +76,7 @@ var SnippetEditor = Widget.extend({
var $clone = $(this).clone().css({width: '24px', height: '24px', border: 0});
$clone.find('.oe_overlay_options >:not(:contains(.oe_snippet_move)), .o_handle').remove();
$clone.find(':not(.glyphicon)').css({position: 'absolute', top: 0, left: 0});
- $clone.appendTo(self.$body).removeClass('d-none');
+ $clone.appendTo('body').removeClass('d-none');
return $clone;
},
start: _.bind(self._onDragAndDropStart, self),
@@ -102,7 +94,7 @@ var SnippetEditor = Widget.extend({
}
});
- return Promise.all(defs);
+ return $.when.apply($, defs);
},
/**
* @override
@@ -158,7 +150,7 @@ var SnippetEditor = Widget.extend({
top: offset.top,
});
this.$('.o_handles').css('height', this.$target.outerHeight());
- this.$el.toggleClass('o_top_cover', offset.top < this.$editable.offset().top);
+ this.$el.toggleClass('o_top_cover', offset.top < 15);
},
/**
* Removes the associated snippet from the DOM and destroys the associated
@@ -177,12 +169,13 @@ var SnippetEditor = Widget.extend({
});
var $parent = this.$target.parent();
- this.$target.find('*').addBack().tooltip('dispose');
+ this.$target.find('*').andSelf().tooltip('dispose');
this.$target.remove();
this.$el.remove();
var node = $parent[0];
if (node && node.firstChild) {
+ $.summernote.core.dom.removeSpace(node, node.firstChild, 0, node.lastChild, 1);
if (!node.firstChild.tagName && node.firstChild.textContent === ' ') {
node.removeChild(node.firstChild);
}
@@ -204,12 +197,11 @@ var SnippetEditor = Widget.extend({
}
// clean editor if they are image or table in deleted content
- this.$body.find('.note-control-selection').hide();
- this.$body.find('.o_table_handler').remove();
+ $('.note-control-selection').hide();
+ $('.o_table_handler').remove();
this.trigger_up('snippet_removed');
this.destroy();
- $parent.trigger('content_changed');
},
/**
* Displays/Hides the editor overlay and notifies the associated snippet
@@ -219,9 +211,6 @@ var SnippetEditor = Widget.extend({
* @param {boolean} focus - true to display, false to hide
*/
toggleFocus: function (focus) {
- if (!this.$el) {
- return;
- }
var do_action = (focus ? _do_action_focus : _do_action_blur);
// Attach own and parent options on the current overlay
@@ -249,10 +238,6 @@ var SnippetEditor = Widget.extend({
this.cover();
this.$el.toggleClass('oe_active', !!focus);
- if (focus) {
- this.trigger_up('snippet_focused');
- }
-
function _do_action_focus(style, $dest) {
style.$el.insertAfter($dest);
style.onFocus();
@@ -313,8 +298,7 @@ var SnippetEditor = Widget.extend({
self,
val.base_target ? self.$target.find(val.base_target).eq(0) : self.$target,
self.$el,
- val.data,
- self.options
+ val.data
);
self.styles[optionName || _.uniqueId('option')] = option;
option.__order = i++;
@@ -341,7 +325,7 @@ var SnippetEditor = Widget.extend({
this.$el.find('[data-toggle="dropdown"]').dropdown();
- return Promise.all(defs);
+ return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
@@ -356,13 +340,13 @@ var SnippetEditor = Widget.extend({
*/
_onCloneClick: function (ev) {
ev.preventDefault();
- this.trigger_up('cover_will_change');
this.trigger_up('snippet_will_be_cloned', {$target: this.$target});
var $clone = this.$target.clone(false);
this.trigger_up('request_history_undo_record', {$target: this.$target});
+
this.$target.after($clone);
this.trigger_up('call_for_each_child_snippet', {
$snippet: $clone,
@@ -375,7 +359,6 @@ var SnippetEditor = Widget.extend({
},
});
this.trigger_up('snippet_cloned', {$target: $clone, $origin: this.$target});
- $clone.trigger('content_changed');
},
/**
* Called when the overlay dimensions/positions should be recomputed.
@@ -383,7 +366,6 @@ var SnippetEditor = Widget.extend({
* @private
*/
_onCoverUpdate: function () {
- this.trigger_up('cover_will_change');
this.cover();
},
/**
@@ -420,11 +402,11 @@ var SnippetEditor = Widget.extend({
$selectorChildren: $selectorChildren,
});
- this.$body.addClass('move-important');
+ $('body').addClass('move-important');
- this.$editable.find('.oe_drop_zone').droppable({
+ $('.oe_drop_zone').droppable({
over: function () {
- self.$editable.find('.oe_drop_zone.hide').removeClass('hide');
+ $('.oe_drop_zone.hide').removeClass('hide');
$(this).addClass('hide').first().after(self.$target);
self.dropped = true;
},
@@ -434,8 +416,6 @@ var SnippetEditor = Widget.extend({
self.dropped = false;
},
});
-
- this.trigger_up('cover_will_change');
},
/**
* Called when the snippet is dropped after being dragged thanks to the
@@ -446,7 +426,7 @@ var SnippetEditor = Widget.extend({
* @param {Object} ui
*/
_onDragAndDropStop: function (ev, ui) {
- this.$editable.find('.oe_drop_zone').droppable('destroy').remove();
+ var self = this;
// TODO lot of this is duplicated code of the d&d feature of snippets
if (!this.dropped) {
@@ -457,21 +437,22 @@ var SnippetEditor = Widget.extend({
}
}
+ $('.oe_drop_zone').droppable('destroy').remove();
+
var prev = this.$target.first()[0].previousSibling;
var next = this.$target.last()[0].nextSibling;
var $parent = this.$target.parent();
- var $clone = this.$editable.find('.oe_drop_clone');
+ var $clone = $('.oe_drop_clone');
if (prev === $clone[0]) {
prev = $clone[0].previousSibling;
} else if (next === $clone[0]) {
next = $clone[0].nextSibling;
}
$clone.after(this.$target);
- var $from = $clone.parent();
this.$el.removeClass('d-none');
- this.$body.removeClass('move-important');
+ $('body').removeClass('move-important');
$clone.remove();
if (this.dropped) {
@@ -488,13 +469,10 @@ var SnippetEditor = Widget.extend({
for (var i in this.styles) {
this.styles[i].onMove();
}
-
- this.$target.trigger('content_changed');
- $from.trigger('content_changed');
}
- this.trigger_up('drag_and_drop_stop', {
- $snippet: this.$target,
+ self.trigger_up('drag_and_drop_stop', {
+ $snippet: self.$target,
});
},
/**
@@ -505,7 +483,6 @@ var SnippetEditor = Widget.extend({
* @param {OdooEvent} ev
*/
_onOptionUpdate: function (ev) {
- this.trigger_up('cover_will_change');
// If multiple option names are given, we suppose it should not be
// propagated to parent editor
if (ev.data.optionNames) {
@@ -528,16 +505,6 @@ var SnippetEditor = Widget.extend({
}
}
},
- /**
- * Called when the user opens the customize menu.
- *
- * @private
- * @param {OdooEvent} ev
- */
- _onOpenCusomize: function (ev) {
- this.trigger_up('request_history_undo_record', {$target: this.$target});
- },
-
/**
* Called when the 'parent' button is clicked.
*
@@ -558,7 +525,6 @@ var SnippetEditor = Widget.extend({
*/
_onRemoveClick: function (ev) {
ev.preventDefault();
- this.trigger_up('cover_will_change');
this.trigger_up('request_history_undo_record', {$target: this.$target});
this.removeSnippet();
},
@@ -569,7 +535,6 @@ var SnippetEditor = Widget.extend({
*/
var SnippetsMenu = Widget.extend({
id: 'oe_snippets',
- cacheSnippetTemplate: {},
activeSnippets: [],
custom_events: {
activate_insertion_zones: '_onActivateInsertionZones',
@@ -579,34 +544,15 @@ var SnippetsMenu = Widget.extend({
go_to_parent: '_onGoToParent',
remove_snippet: '_onRemoveSnippet',
snippet_removed: '_onSnippetRemoved',
- reload_snippet_dropzones: '_disableUndroppableSnippets',
},
/**
- * @param {Widget} parent
- * @param {Object} [options]
- * @param {string} [options.snippets]
- * URL of the snippets template. This URL might have been set
- * in the global 'snippets' variable, otherwise this function
- * assigns a default one.
- * default: 'web_editor.snippets'
- *
* @constructor
*/
- init: function (parent, options) {
+ init: function (parent, $editable) {
this._super.apply(this, arguments);
- options = options || {};
- this.trigger_up('getRecordInfo', {
- recordInfo: options,
- callback: function (recordInfo) {
- _.defaults(options, recordInfo);
- },
- });
- this.options = options;
- if (!this.options.snippets) {
- this.options.snippets = 'web_editor.snippets';
- }
+ this.$editable = $editable;
this.$activeSnippet = false;
this.snippetEditors = [];
},
@@ -616,14 +562,12 @@ var SnippetsMenu = Widget.extend({
start: function () {
var self = this;
var defs = [this._super.apply(this, arguments)];
- this.ownerDocument = this.$el[0].ownerDocument;
- this.$document = $(this.ownerDocument);
- this.window = this.ownerDocument.defaultView;
- this.$window = $(this.window);
+ var $document = $(document);
+ var $window = $(window);
// Fetch snippet templates and compute it
-
- defs.push(this.loadSnippets().then(function (html) {
+ var url = this._getSnippetURL();
+ defs.push(this._rpc({route: url}).then(function (html) {
return self._computeSnippetTemplates(html);
}));
@@ -634,8 +578,8 @@ var SnippetsMenu = Widget.extend({
// Active snippet editor on click in the page
var lastClickedElement;
- this.$document.on('click.snippets_menu', '*', function (ev) {
- var srcElement = ev.target || (ev.originalEvent && (ev.originalEvent.target || ev.originalEvent.originalTarget)) || ev.srcElement;
+ $document.on('click.snippets_menu', '*', function (ev) {
+ var srcElement = ev.srcElement || (ev.originalEvent && (ev.originalEvent.originalTarget || ev.originalEvent.target) || ev.target);
if (lastClickedElement === srcElement || !srcElement) {
return;
}
@@ -654,21 +598,27 @@ var SnippetsMenu = Widget.extend({
core.bus.on('deactivate_snippet', this, this._onDeactivateSnippet);
core.bus.on('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
+ // Some summernote customization
+ var _isNotBreakable = $.summernote.core.dom.isNotBreakable;
+ $.summernote.core.dom.isNotBreakable = function (node) {
+ return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node));
+ };
+
// Adapt overlay covering when the window is resized / content changes
var debouncedCoverUpdate = _.debounce(function () {
- self.updateCurrentSnippetEditorOverlay();
+ self._updateCurrentSnippetEditorOverlay();
}, 200);
- this.$window.on('resize.snippets_menu', debouncedCoverUpdate);
- this.$window.on('content_changed.snippets_menu', debouncedCoverUpdate);
+ $window.on('resize.snippets_menu', debouncedCoverUpdate);
+ $window.on('content_changed.snippets_menu', debouncedCoverUpdate);
// On keydown add a class on the active overlay to hide it and show it
// again when the mouse moves
- this.$document.on('keydown.snippets_menu', function () {
+ $document.on('keydown.snippets_menu', function () {
if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
self.$activeSnippet.data('snippet-editor').$el.addClass('o_keypress');
}
});
- this.$document.on('mousemove.snippets_menu, mousedown.snippets_menu', function () {
+ $document.on('mousemove.snippets_menu', function () {
if (self.$activeSnippet && self.$activeSnippet.data('snippet-editor')) {
self.$activeSnippet.data('snippet-editor').$el.removeClass('o_keypress');
}
@@ -676,21 +626,20 @@ var SnippetsMenu = Widget.extend({
// Auto-selects text elements with a specific class and remove this
// on text changes
- this.$document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
+ $document.on('click.snippets_menu', '.o_default_snippet_text', function (ev) {
$(ev.target).selectContent();
- $(ev.target).removeClass('o_default_snippet_text');
});
- this.$document.on('keyup.snippets_menu', function () {
- var range = Wysiwyg.getRange(this);
- $(range && range.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
+ $document.on('keyup.snippets_menu', function () {
+ var r = $.summernote.core.range.create();
+ $(r && r.sc).closest('.o_default_snippet_text').removeClass('o_default_snippet_text');
});
- return Promise.all(defs).then(function () {
+ return $.when.apply($, defs).then(function () {
// Trigger a resize event once entering edit mode as the snippets
// menu will take part of the screen width (delayed because of
// animation). (TODO wait for real animation end)
setTimeout(function () {
- self.$window.trigger('resize');
+ $window.trigger('resize');
}, 1000);
});
},
@@ -699,14 +648,11 @@ var SnippetsMenu = Widget.extend({
*/
destroy: function () {
this._super.apply(this, arguments);
- if (this.$window) {
- this.$snippetEditorArea.remove();
- this.$window.off('.snippets_menu');
- this.$document.off('.snippets_menu');
- }
+ this.$snippetEditorArea.remove();
+ $(window).off('.snippets_menu');
+ $(document).off('.snippets_menu');
core.bus.off('deactivate_snippet', this, this._onDeactivateSnippet);
core.bus.off('snippet_editor_clean_for_save', this, this._onCleanForSaveDemand);
- delete this.cacheSnippetTemplate[this.options.snippets];
},
//--------------------------------------------------------------------------
@@ -719,69 +665,16 @@ var SnippetsMenu = Widget.extend({
* - Remove the 'contentEditable' attributes
*/
cleanForSave: function () {
- this._activateSnippet(false);
this.trigger_up('ready_to_clean_for_save');
this._destroyEditors();
- this.getEditableArea().find('[contentEditable]')
+ this.$editable.find('[contentEditable]')
.removeAttr('contentEditable')
.removeProp('contentEditable');
- this.getEditableArea().find('.o_we_selected_image')
+ this.$editable.find('.o_we_selected_image')
.removeClass('o_we_selected_image');
},
- /**
- * Load snippets.
- */
- loadSnippets: function () {
- if (this.cacheSnippetTemplate[this.options.snippets]) {
- this._defLoadSnippets = this.cacheSnippetTemplate[this.options.snippets];
- return this._defLoadSnippets;
- }
- this._defLoadSnippets = this._rpc({
- model: 'ir.ui.view',
- method: 'render_template',
- args: [this.options.snippets, {}],
- kwargs: {
- context: this.options.context,
- },
- });
- this.cacheSnippetTemplate[this.options.snippets] = this._defLoadSnippets;
- return this.cacheSnippetTemplate[this.options.snippets];
- },
- /**
- * Sets the instance variables $editor, $body and selectorEditableArea.
- *
- * @param {JQuery} $editor
- * @param {String} selectorEditableArea
- */
- setSelectorEditableArea: function ($editor, selectorEditableArea) {
- this.selectorEditableArea = selectorEditableArea;
- this.$editor = $editor;
- this.$body = $editor.closest('body');
- },
- /**
- * Get the editable area.
- *
- * @returns {JQuery}
- */
- getEditableArea: function () {
- return this.$editor.find(this.selectorEditableArea);
- },
- /**
- * Updates the cover dimensions of the current snippet editor.
- */
- updateCurrentSnippetEditorOverlay: function () {
- if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
- this.$activeSnippet.data('snippet-editor').cover();
- }
- this.snippetEditors = _.filter(this.snippetEditors, function (snippetEditor) {
- if (snippetEditor.$target.closest('body').length) {
- return true;
- }
- snippetEditor.destroy();
- });
- },
//--------------------------------------------------------------------------
// Private
@@ -799,7 +692,6 @@ var SnippetsMenu = Widget.extend({
* child
*/
_activateInsertionZones: function ($selectorSiblings, $selectorChildren) {
- var self = this;
var zone_template = $('', {
class: 'oe_drop_zone oe_insert',
});
@@ -811,8 +703,8 @@ var SnippetsMenu = Widget.extend({
if ($selectorChildren) {
$selectorChildren.each(function () {
var $zone = $(this);
- var css = self.window.getComputedStyle(this);
- var parentCss = self.window.getComputedStyle($zone.parent()[0]);
+ var css = window.getComputedStyle(this);
+ var parentCss = window.getComputedStyle($zone.parent()[0]);
var float = css.float || css.cssFloat;
var parentDisplay = parentCss.display;
var parentFlex = parentCss.flexDirection;
@@ -823,7 +715,7 @@ var SnippetsMenu = Widget.extend({
var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
if (test) {
$drop.addClass('oe_vertical').css({
- height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight),
+ height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
float: 'none',
display: 'inline-block',
});
@@ -841,7 +733,7 @@ var SnippetsMenu = Widget.extend({
test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === 'BR'));
if (test) {
$drop.addClass('oe_vertical').css({
- height: parseInt(self.window.getComputedStyle($zone[0]).lineHeight),
+ height: parseInt(window.getComputedStyle($zone[0]).lineHeight),
float: 'none',
display: 'inline-block'
});
@@ -864,8 +756,8 @@ var SnippetsMenu = Widget.extend({
$selectorSiblings.filter(':not(.oe_drop_zone):not(.oe_drop_clone)').each(function () {
var $zone = $(this);
var $drop;
- var css = self.window.getComputedStyle(this);
- var parentCss = self.window.getComputedStyle($zone.parent()[0]);
+ var css = window.getComputedStyle(this);
+ var parentCss = window.getComputedStyle($zone.parent()[0]);
var float = css.float || css.cssFloat;
var parentDisplay = parentCss.display;
var parentFlex = parentCss.flexDirection;
@@ -897,14 +789,14 @@ var SnippetsMenu = Widget.extend({
var $zones;
do {
count = 0;
- $zones = this.getEditableArea().find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
+ $zones = this.$editable.find('.oe_drop_zone > .oe_drop_zone').remove(); // no recursive zones
count += $zones.length;
$zones.remove();
} while (count > 0);
// Cleaning consecutive zone and up zones placed between floating or
// inline elements. We do not like these kind of zones.
- $zones = this.getEditableArea().find('.oe_drop_zone:not(.oe_vertical)');
+ $zones = this.$editable.find('.oe_drop_zone:not(.oe_vertical)');
$zones.each(function () {
var zone = $(this);
var prev = zone.prev();
@@ -938,8 +830,7 @@ var SnippetsMenu = Widget.extend({
* @param {jQuery|false} $snippet
* The DOM element whose editor need to be enabled. Only disable the
* current one if false is given.
- * @returns {Promise}
- * (might be async when an editor must be created)
+ * @returns {Deferred} (might be async when an editor must be created)
*/
_activateSnippet: function ($snippet) {
if ($snippet) {
@@ -947,27 +838,25 @@ var SnippetsMenu = Widget.extend({
$snippet = globalSelector.closest($snippet);
}
if (this.$activeSnippet && this.$activeSnippet[0] === $snippet[0]) {
- return Promise.resolve($snippet.data('snippet-editor'));
+ return $.when();
}
}
- var editor = null;
if (this.$activeSnippet) {
- editor = this.$activeSnippet.data('snippet-editor');
- if (editor) {
- editor.toggleFocus(false);
+ if (this.$activeSnippet.data('snippet-editor')) {
+ this.$activeSnippet.data('snippet-editor').toggleFocus(false);
}
this.$activeSnippet = false;
}
if ($snippet && $snippet.length) {
var self = this;
- this.trigger_up('activate_snippet', $snippet);
- return this._createSnippetEditor($snippet).then(function (editor) {
+ return this._createSnippetEditor($snippet).then(function () {
self.$activeSnippet = $snippet;
- editor.toggleFocus(true);
- return editor;
+ if (self.$activeSnippet.data('snippet-editor')) {
+ self.$activeSnippet.data('snippet-editor').toggleFocus(true);
+ }
});
}
- return Promise.resolve(editor);
+ return $.when();
},
/**
* @private
@@ -977,6 +866,16 @@ var SnippetsMenu = Widget.extend({
snippetEditor.destroy();
});
},
+ /**
+ * Updates the cover dimensions of the current snippet editor.
+ *
+ * @private
+ */
+ _updateCurrentSnippetEditorOverlay: function () {
+ if (this.$activeSnippet && this.$activeSnippet.data('snippet-editor')) {
+ this.$activeSnippet.data('snippet-editor').cover();
+ }
+ },
/**
* Calls a given callback 'on' the given snippet and all its child ones if
* any (DOM element with options).
@@ -988,7 +887,7 @@ var SnippetsMenu = Widget.extend({
* @param {function} callback
* Given two arguments: the snippet editor associated to the snippet
* being managed and the DOM element of this snippet.
- * @returns {Promise} (might be async if snippet editors need to be created
+ * @returns {Deferred} (might be async if snippet editors need to be created
* and/or the callback is async)
*/
_callForEachChildSnippet: function ($snippet, callback) {
@@ -1001,7 +900,7 @@ var SnippetsMenu = Widget.extend({
}
});
});
- return Promise.all(defs);
+ return $.when.apply($, defs);
},
/**
* Creates and returns a set of helper functions which can help finding
@@ -1056,7 +955,7 @@ var SnippetsMenu = Widget.extend({
selectorConditions += ':has(' + target + ')';
}
if (!noCheck) {
- selectorConditions = (this.options.addDropSelector || '') + selectorConditions;
+ selectorConditions = ':o_editable' + selectorConditions;
}
// (Re)join the subselectors
@@ -1079,7 +978,7 @@ var SnippetsMenu = Widget.extend({
};
} else {
functions.closest = function ($from, parentNode) {
- var parents = self.getEditableArea().get();
+ var parents = self.$editable.get();
return $from.closest(selector, parentNode).filter(function () {
var node = this;
while (node.parentNode) {
@@ -1092,9 +991,9 @@ var SnippetsMenu = Widget.extend({
});
};
functions.all = isChildren ? function ($from) {
- return dom.cssFind($from || self.getEditableArea(), selector);
+ return dom.cssFind($from || self.$editable, selector);
} : function ($from) {
- $from = $from || self.getEditableArea();
+ $from = $from || self.$editable;
return $from.filter(selector).add(dom.cssFind($from, selector));
};
}
@@ -1221,6 +1120,7 @@ var SnippetsMenu = Widget.extend({
if (!this.$snippets.length) {
this.$el.detach();
}
+ $('body').toggleClass('editor_has_snippets', this.$snippets.length > 0);
// Register the text nodes that needs to be auto-selected on click
this._registerDefaultTexts();
@@ -1252,13 +1152,13 @@ var SnippetsMenu = Widget.extend({
*
* @private
* @param {jQuery} $snippet
- * @returns {Promise}
+ * @returns {Deferred}
*/
_createSnippetEditor: function ($snippet) {
var self = this;
var snippetEditor = $snippet.data('snippet-editor');
if (snippetEditor) {
- return Promise.resolve(snippetEditor);
+ return $.when(snippetEditor);
}
var def;
@@ -1267,8 +1167,8 @@ var SnippetsMenu = Widget.extend({
def = this._createSnippetEditor($parent);
}
- return Promise.resolve(def).then(function (parentEditor) {
- snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions, self.getEditableArea(), self.options);
+ return $.when(def).then(function (parentEditor) {
+ snippetEditor = new SnippetEditor(parentEditor || self, $snippet, self.templateOptions);
self.snippetEditors.push(snippetEditor);
return snippetEditor.appendTo(self.$snippetEditorArea);
}).then(function () {
@@ -1303,6 +1203,17 @@ var SnippetsMenu = Widget.extend({
$snippet.toggleClass('o_disabled', !check);
});
},
+ /**
+ * Returns the URL where to find the snippets template. This URL might have
+ * been set in the global 'snippetsURL' variable, otherwise this function
+ * returns a default one.
+ *
+ * @private
+ * @returns {string}
+ */
+ _getSnippetURL: function () {
+ return odoo.snippetsURL || '/web_editor/snippets';
+ },
/**
* Make given snippets be draggable/droppable thanks to their thumbnail.
*
@@ -1319,7 +1230,8 @@ var SnippetsMenu = Widget.extend({
$snippets.draggable({
greedy: true,
helper: 'clone',
- appendTo: this.$body,
+ zIndex: '1000',
+ appendTo: 'body',
cursor: 'move',
handle: '.oe_snippet_thumbnail',
distance: 30,
@@ -1337,10 +1249,12 @@ var SnippetsMenu = Widget.extend({
for (var k in temp) {
if ($base_body.is(temp[k].base_selector) && !$base_body.is(temp[k].base_exclude)) {
if (temp[k]['drop-near']) {
- $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
+ if (!$selectorSiblings) $selectorSiblings = temp[k]['drop-near'].all();
+ else $selectorSiblings = $selectorSiblings.add(temp[k]['drop-near'].all());
}
if (temp[k]['drop-in']) {
- $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
+ if (!$selectorChildren) $selectorChildren = temp[k]['drop-in'].all();
+ else $selectorChildren = $selectorChildren.add(temp[k]['drop-in'].all());
}
}
}
@@ -1355,12 +1269,11 @@ var SnippetsMenu = Widget.extend({
self._activateSnippet(false);
self._activateInsertionZones($selectorSiblings, $selectorChildren);
- self.getEditableArea().find('.oe_drop_zone').droppable({
+ $('.oe_drop_zone').droppable({
over: function () {
if (!dropped) {
dropped = true;
$(this).first().after($toInsert).addClass('d-none');
- $toInsert.removeClass('oe_snippet_body');
}
},
out: function () {
@@ -1369,25 +1282,22 @@ var SnippetsMenu = Widget.extend({
dropped = false;
$toInsert.detach();
$(this).removeClass('d-none');
- $toInsert.addClass('oe_snippet_body');
}
},
});
-
- self.trigger_up('cover_will_change');
},
stop: function (ev, ui) {
$toInsert.removeClass('oe_snippet_body');
if (!dropped && ui.position.top > 3 && ui.position.left + 50 > self.$el.outerWidth()) {
- var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone', {container: document.body}).first();
+ var $el = $.nearest({x: ui.position.left, y: ui.position.top}, '.oe_drop_zone').first();
if ($el.length) {
$el.after($toInsert);
dropped = true;
}
}
- self.getEditableArea().find('.oe_drop_zone').droppable('destroy').remove();
+ self.$editable.find('.oe_drop_zone').droppable('destroy').remove();
if (dropped) {
var prev = $toInsert.first()[0].previousSibling;
@@ -1408,6 +1318,8 @@ var SnippetsMenu = Widget.extend({
$parent.prepend($toInsert);
}
+ $toInsert.closest('.o_editable').trigger('content_changed');
+
var $target = $toInsert;
_.defer(function () {
@@ -1415,12 +1327,12 @@ var SnippetsMenu = Widget.extend({
self._disableUndroppableSnippets();
self._callForEachChildSnippet($target, function (editor, $snippet) {
- editor.buildSnippet();
+ _.defer(function () {
+ editor.buildSnippet();
+ });
}).then(function () {
- $target.trigger('content_changed');
- if ($target.closest('body').length) { // can be destroyed (eg in test)
- self._activateSnippet($target);
- }
+ $target.closest('.o_editable').trigger('content_changed');
+ self._activateSnippet($target);
});
});
} else {
diff --git a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
similarity index 93%
rename from addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
rename to addons/web_editor/static/src/js/editor/snippets.options.js
index 98180ea4fcb..f481061c9e2 100644
--- a/addons/web_editor/static/src/js/wysiwyg_snippets/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -4,9 +4,8 @@ odoo.define('web_editor.snippets.options', function (require) {
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
-var weWidgets = require('wysiwyg.widgets');
-var FontPlugin = require('web_editor.wysiwyg.plugin.font');
-var ColorpickerDialog = require('web.ColorpickerDialog');
+var summernoteCustomColors = require('web_editor.rte.summernote_custom_colors');
+var weWidgets = require('web_editor.widget');
var qweb = core.qweb;
var _t = core._t;
@@ -19,12 +18,10 @@ var _t = core._t;
var SnippetOption = Widget.extend({
events: {
'mouseenter': '_onLinkEnter',
- 'mouseenter a': '_onLinkEnter',
+ 'mouseenter .dropdown-item': '_onLinkEnter',
'click': '_onLinkClick',
- 'click a': '_onLinkClick',
'mouseleave': '_onMouseleave',
- 'mouseleave a': '_onMouseleave',
- 'mouseleave .dropdown-menu': '_onMouseleave',
+ 'mouseleave .dropdown-item': '_onMouseleave',
},
/**
* When editing a snippet, its options are shown alongside the ones of its
@@ -41,11 +38,9 @@ var SnippetOption = Widget.extend({
*
* @constructor
*/
- init: function (parent, $target, $overlay, data, options) {
+ init: function (parent, $target, $overlay, data) {
this._super.apply(this, arguments);
- this.options = options;
this.$target = $target;
- this.ownerDocument = this.$target[0].ownerDocument;
this.$overlay = $overlay;
this.data = data;
this.__methodNames = [];
@@ -245,7 +240,6 @@ var SnippetOption = Widget.extend({
if (!previewMode) {
this._reset();
this.trigger_up('request_history_undo_record', {$target: this.$target});
- this.$target.trigger('content_changed');
}
// Search for methods (data-...) (i.e. data-toggle-class) on the
@@ -373,7 +367,7 @@ var SnippetOption = Widget.extend({
*/
_onLinkClick: function (ev) {
var $opt = $(ev.target).closest('.dropdown-item');
- if (ev.isDefaultPrevented() || !$opt.length || !$opt.is(':hasData')) {
+ if (!$opt.length || !$opt.is(':hasData')) {
return;
}
@@ -460,7 +454,7 @@ registry.sizing = SnippetOption.extend({
var regClass = new RegExp('\\s*' + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
var cursor = $handle.css('cursor') + '-important';
- var $body = $(this.ownerDocument.body);
+ var $body = $(document.body);
$body.addClass(cursor);
var xy = ev['page' + XY];
@@ -667,19 +661,12 @@ registry.colorpicker = SnippetOption.extend({
}
if (!this.$el.find('.colorpicker').length) {
- // Add common colors to palettes if not excluded
- var fontPlugin = new FontPlugin({
- layoutInfo: {
- editable: $(''),
- editingArea: $(''),
- },
- options: {},
- });
+ // TODO remove old UI's code that survived
+ var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
+ var $clpicker = $(qweb.render('web_editor.colorpicker'));
- var $clpicker = fontPlugin.createPalette('backColor').find('.note-color-palette'); // don't use custom color
- $clpicker.find('.note-color-reset').remove();
- $clpicker.find('h6').each(function () {
- $(this).replaceWith($('').text($(this).text()));
+ _.each($clpicker.find('.o_colorpicker_section'), function (elem) {
+ $(elem).prepend("
" + elem.dataset.display + "
");
});
// Retrieve excluded palettes list
@@ -689,7 +676,7 @@ registry.colorpicker = SnippetOption.extend({
}
// Apply a custom title if specified
if (this.data.paletteTitle) {
- $clpicker.find('.note-palette-title').text(this.data.paletteTitle);
+ $pt.find('.note-palette-title').text(this.data.paletteTitle);
}
// Remove excluded palettes
@@ -697,29 +684,39 @@ registry.colorpicker = SnippetOption.extend({
$clpicker.find('[data-name="' + exc + '"]').remove();
});
- var $pt = $(qweb.render('web_editor.snippet.option.colorpicker'));
+ // Add common colors to palettes if not excluded
+ if (!('common' in excluded)) {
+ var $commonColorSection = $clpicker.find('[data-name="common"]');
+ _.each(summernoteCustomColors, function (colorRow, i) {
+ var $div = $('', {class: 'clearfix'}).appendTo($commonColorSection);
+ if (i === 0) {
+ // Ignore the summernote gray palette and use ours
+ return;
+ }
+ _.each(colorRow, function (color) {
+ $div.append('');
+ });
+ });
+ }
+
$pt.find('.o_colorpicker_section_tabs').append($clpicker);
this.$el.find('.dropdown-menu').append($pt);
}
- var bgColor = ColorpickerDialog.formatColor(self.$target.css('background-color'));
var classes = [];
this.$el.find('.colorpicker button').each(function () {
var $color = $(this);
var color = $color.data('color');
- if (color) {
- $color.addClass('bg-' + color);
- var className = self.colorPrefix + color;
- if (self.$target.hasClass(className)) {
- $color.addClass('selected');
- }
- classes.push(className);
- } else {
- color = $color.data('value');
- if (bgColor === ColorpickerDialog.formatColor(color)) {
- $color.addClass('selected');
- }
+ if (!color) {
+ return;
}
+
+ $color.addClass('bg-' + color);
+ var className = self.colorPrefix + color;
+ if (self.$target.hasClass(className)) {
+ $color.addClass('selected');
+ }
+ classes.push(className);
});
this.classes = classes.join(' ');
@@ -753,9 +750,6 @@ registry.colorpicker = SnippetOption.extend({
var color = $(ev.currentTarget).data('color');
if (color) {
this.$target.addClass(this.colorPrefix + color);
- } else if ($(ev.currentTarget).data('value')) {
- color = $(ev.currentTarget).data('value');
- this.$target.css('background-color', color);
} else if ($(ev.target).hasClass('o_custom_color')) {
this.$target
.removeClass(this.classes)
@@ -848,20 +842,18 @@ registry.background = SnippetOption.extend({
*/
chooseImage: function (previewMode, value, $opt) {
// Put fake image in the DOM, edit it and use it as background-image
- var $image = $('', {class: 'd-none', src: value === 'true' ? '' : value}).appendTo(this.$target);
+ var $image = $('', {class: 'd-none', src: value}).appendTo(this.$target);
var $editable = this.$target.closest('.o_editable');
var _editor = new weWidgets.MediaDialog(this, {
onlyImages: true,
- mediaWidth: 1920,
firstFilters: ['background'],
res_model: $editable.data('oe-model'),
res_id: $editable.data('oe-id'),
- }, $image[0]).open();
+ }, null, $image[0]).open();
- _editor.on('save', this, function (image) {
- this._setCustomBackground(image.src);
- this.$target.trigger('content_changed');
+ _editor.on('save', this, function () {
+ this._setCustomBackground($image.attr('src'));
});
_editor.on('closed', this, function () {
$image.remove();
@@ -916,8 +908,7 @@ registry.background = SnippetOption.extend({
if (value === undefined) {
value = this.$target.css('background-image');
}
- var srcValueWrapper = /url\(['"]*|['"]*\)|^none$/g;
- return value && value.replace(srcValueWrapper, '') || '';
+ return value.replace(/url\(['"]*|['"]*\)|^none$/g, '');
},
/**
* @override
@@ -1182,11 +1173,6 @@ registry.many2one = SnippetOption.extend({
*/
start: function () {
var self = this;
- this.trigger_up('getRecordInfo', _.extend(this.options, {
- callback: function (recordInfo) {
- _.defaults(self.options, recordInfo);
- },
- }));
this.Model = this.$target.data('oe-many2one-model');
this.ID = +this.$target.data('oe-many2one-id');
@@ -1219,7 +1205,6 @@ registry.many2one = SnippetOption.extend({
this.$search.find('input')
.focus()
.on('keyup', function (e) {
- self.$overlay.removeClass('o_keypress');
self._findExisting($(this).val());
});
@@ -1260,7 +1245,7 @@ registry.many2one = SnippetOption.extend({
*
* @private
* @param {string} name
- * @returns {Promise}
+ * @returns {Deferred}
*/
_findExisting: function (name) {
var self = this;
@@ -1286,7 +1271,6 @@ registry.many2one = SnippetOption.extend({
kwargs: {
order: [{name: 'name', asc: false}],
limit: 5,
- context: this.options.context,
},
}).then(function (result) {
self.$search.siblings().remove();
@@ -1325,7 +1309,6 @@ registry.many2one = SnippetOption.extend({
args: [[self.ID]],
kwargs: {
options: options,
- context: self.options.context,
},
}).then(function (html) {
$node.html(html);
diff --git a/addons/web_editor/static/src/js/editor/summernote.js b/addons/web_editor/static/src/js/editor/summernote.js
new file mode 100644
index 00000000000..e0968ecdb58
--- /dev/null
+++ b/addons/web_editor/static/src/js/editor/summernote.js
@@ -0,0 +1,2452 @@
+odoo.define('web_editor.summernote', function (require) {
+'use strict';
+
+var core = require('web.core');
+require('summernote/summernote'); // wait that summernote is loaded
+
+var _t = core._t;
+
+// Summernote Lib (neek hack to make accessible: method and object)
+// var agent = $.summernote.core.agent;
+var dom = $.summernote.core.dom;
+var range = $.summernote.core.range;
+var list = $.summernote.core.list;
+var key = $.summernote.core.key;
+var eventHandler = $.summernote.eventHandler;
+var editor = eventHandler.modules.editor;
+var renderer = $.summernote.renderer;
+var options = $.summernote.options;
+
+// Browser-unify execCommand
+var oldJustify = {};
+_.each(['Left', 'Right', 'Full', 'Center'], function (align) {
+ oldJustify[align] = editor['justify' + align];
+ editor['justify' + align] = function ($editable, value) {
+ // Before calling the standard function, check all elements which have
+ // an 'align' attribute and mark them with their value
+ var $align = $editable.find('[align]');
+ _.each($align, function (el) {
+ var $el = $(el);
+ $el.data('__align', $el.attr('align'));
+ });
+
+ // Call the standard function
+ oldJustify[align].apply(this, arguments);
+
+ // Then:
+
+ // Remove the text-align of elements which lost the 'align' attribute
+ var $newAlign = $editable.find('[align]');
+ $align.not($newAlign).css('text-align', '');
+
+ // Transform the 'align' attribute into the 'text-align' css
+ // property for elements which received the 'align' attribute or whose
+ // 'align' attribute changed
+ _.each($newAlign, function (el) {
+ var $el = $(el);
+
+ var oldAlignValue = $align.data('__align');
+ var alignValue = $el.attr('align');
+ if (oldAlignValue === alignValue) {
+ // If the element already had an 'align' attribute and that it
+ // did not changed, do nothing (compatibility)
+ return;
+ }
+
+ $el.removeAttr('align');
+ $el.css('text-align', alignValue);
+
+ // Note the first step (removing the text-align of elemnts which
+ // lost the 'align' attribute) is kinda the same as this one, but
+ // this one handles the elements which have been edited with chrome
+ // or with this new system
+ $el.find('*').css('text-align', '');
+ });
+
+ // Unmark the elements
+ $align.removeData('__align');
+ };
+});
+
+
+// Add methods to summernote
+
+dom.hasContentAfter = function (node) {
+ var next;
+ if (dom.isEditable(node)) return;
+ while (node.nextSibling) {
+ next = node.nextSibling;
+ if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
+ node = next;
+ }
+};
+dom.hasContentBefore = function (node) {
+ var prev;
+ if (dom.isEditable(node)) return;
+ while (node.previousSibling) {
+ prev = node.previousSibling;
+ if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
+ node = prev;
+ }
+};
+dom.ancestorHaveNextSibling = function (node, pred) {
+ pred = pred || dom.hasContentAfter;
+ while (!dom.isEditable(node) && (!node.nextSibling || !pred(node))) { node = node.parentNode; }
+ return node;
+};
+dom.ancestorHavePreviousSibling = function (node, pred) {
+ pred = pred || dom.hasContentBefore;
+ while (!dom.isEditable(node) && (!node.previousSibling || !pred(node))) { node = node.parentNode; }
+ return node;
+};
+dom.nextElementSibling = function (node) {
+ while (node) {
+ node = node.nextSibling;
+ if (node && node.tagName) {
+ break;
+ }
+ }
+ return node;
+};
+dom.previousElementSibling = function (node) {
+ while (node) {
+ node = node.previousSibling;
+ if (node && node.tagName) {
+ break;
+ }
+ }
+ return node;
+};
+dom.lastChild = function (node) {
+ while (node.lastChild) { node = node.lastChild; }
+ return node;
+};
+dom.firstChild = function (node) {
+ while (node.firstChild) { node = node.firstChild; }
+ return node;
+};
+dom.lastElementChild = function (node, deep) {
+ node = deep ? dom.lastChild(node) : node.lastChild;
+ return !node || node.tagName ? node : dom.previousElementSibling(node);
+};
+dom.firstElementChild = function (node, deep) {
+ node = deep ? dom.firstChild(node) : node.firstChild;
+ return !node || node.tagName ? node : dom.nextElementSibling(node);
+};
+dom.isEqual = function (prev, cur) {
+ if (prev.tagName !== cur.tagName) {
+ return false;
+ }
+ if ((prev.attributes ? prev.attributes.length : 0) !== (cur.attributes ? cur.attributes.length : 0)) {
+ return false;
+ }
+
+ function strip(text) {
+ return text && text.replace(/^\s+|\s+$/g, '').replace(/\s+/g, ' ');
+ }
+ var att, att2;
+ loop_prev:
+ for (var a in prev.attributes) {
+ att = prev.attributes[a];
+ for (var b in cur.attributes) {
+ att2 = cur.attributes[b];
+ if (att.name === att2.name) {
+ if (strip(att.value) !== strip(att2.value)) return false;
+ continue loop_prev;
+ }
+ }
+ return false;
+ }
+ return true;
+};
+dom.hasOnlyStyle = function (node) {
+ for (var i = 0; i < node.attributes.length; i++) {
+ var attr = node.attributes[i];
+ if (attr.attributeName !== 'style') {
+ return false;
+ }
+ }
+ return true;
+};
+dom.hasProgrammaticStyle = function (node) {
+ var styles = ["float", "display", "position", "top", "left", "right", "bottom"];
+ for (var i = 0; i < node.style.length; i++) {
+ var style = node.style[i];
+ if (styles.indexOf(style) !== -1) {
+ return true;
+ }
+ }
+ return false;
+};
+dom.mergeFilter = function (prev, cur, parent) {
+ // merge text nodes
+ if (prev && (dom.isText(prev) || (['H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'LI', 'P'].indexOf(prev.tagName) !== -1 && prev !== cur.parentNode)) && dom.isText(cur)) {
+ return true;
+ }
+ if (prev && prev.tagName === "P" && dom.isText(cur)) {
+ return true;
+ }
+ if (prev && dom.isText(cur) && !dom.isVisibleText(cur) && (dom.isText(prev) || dom.isVisibleText(prev))) {
+ return true;
+ }
+ if (prev && !dom.isBR(prev) && dom.isEqual(prev, cur) &&
+ ((prev.tagName && dom.getComputedStyle(prev).display === "inline" &&
+ cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
+ return true;
+ }
+ if (dom.isEqual(parent, cur) &&
+ ((parent.tagName && dom.getComputedStyle(parent).display === "inline" &&
+ cur.tagName && dom.getComputedStyle(cur).display === "inline"))) {
+ return true;
+ }
+ if (parent && cur.tagName === "FONT" && (!cur.firstChild || (!cur.attributes.getNamedItem('style') && !cur.className.length))) {
+ return true;
+ }
+ // On backspace, webkit browsers create a with a bunch of
+ // inline styles "remembering" where they come from.
+ // chances are we had e.g.
+ //
foo
+ //
bar
+ // merged the lines getting this in webkit
+ //
foobar
+ if (parent && cur.tagName === "SPAN" && dom.hasOnlyStyle(cur) && !dom.hasProgrammaticStyle(cur)) {
+ return true;
+ }
+};
+dom.doMerge = function (prev, cur) {
+ if (prev.tagName) {
+ if (prev.childNodes.length && !prev.textContent.match(/\S/) && dom.firstElementChild(prev) && dom.isBR(dom.firstElementChild(prev))) {
+ prev.removeChild(dom.firstElementChild(prev));
+ }
+ if (cur.tagName) {
+ while (cur.firstChild) {
+ prev.appendChild(cur.firstChild);
+ }
+ cur.parentNode.removeChild(cur);
+ } else {
+ prev.appendChild(cur);
+ }
+ } else {
+ if (cur.tagName) {
+ var deep = cur;
+ while (deep.tagName && deep.firstChild) {deep = deep.firstChild;}
+ prev.appendData(deep.textContent);
+ cur.parentNode.removeChild(cur);
+ } else {
+ prev.appendData(cur.textContent);
+ cur.parentNode.removeChild(cur);
+ }
+ }
+};
+dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
+ mergeFilter = mergeFilter || dom.mergeFilter;
+ var _merged = false;
+ var add = all || false;
+
+ if (!begin) {
+ begin = node;
+ while (begin.firstChild) {begin = begin.firstChild;}
+ so = 0;
+ } else if (begin.tagName && begin.childNodes[so]) {
+ begin = begin.childNodes[so];
+ so = 0;
+ }
+ if (!end) {
+ end = node;
+ while (end.lastChild) {end = end.lastChild;}
+ eo = end.textContent.length-1;
+ } else if (end.tagName && end.childNodes[so]) {
+ end = end.childNodes[so];
+ so = 0;
+ }
+
+ begin = dom.firstChild(begin);
+ if (dom.isText(begin) && so > begin.textContent.length) {
+ so = 0;
+ }
+ end = dom.firstChild(end);
+ if (dom.isText(end) && eo > end.textContent.length) {
+ eo = 0;
+ }
+
+ function __merge(node) {
+ var merged = false;
+ var prev;
+ for (var k=0; k 1 && dom.isText(cur) && !dom.isVisibleText(cur)) {
+ removed = true;
+ if (cur === begin) {
+ t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur));
+ if (t_begin) {
+ so = 0;
+ begin = dom.lastChild(t_begin);
+ }
+ }
+ if (cur === end) {
+ t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur));
+ if (t_end) {
+ eo = 1;
+ end = dom.firstChild(t_end);
+ if (dom.isText(end)) {
+ eo = end.textContent.length;
+ }
+ }
+ }
+ cur.parentNode.removeChild(cur);
+ begin = dom.lastChild(begin);
+ end = dom.lastChild(end);
+ k--;
+ continue;
+ }
+
+ // convert HTML space
+ if (dom.isText(cur)) {
+ var text;
+ var temp;
+ var _temp;
+ var exp1 = /[\t\n\r ]+/g;
+ var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
+ if (cur === begin) {
+ temp = cur.textContent.substr(0, so);
+ _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ so -= temp.length - _temp.length;
+ }
+ if (cur === end) {
+ temp = cur.textContent.substr(0, eo);
+ _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
+ eo -= temp.length - _temp.length;
+ }
+ text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
+ removed = removed || cur.textContent.length !== text.length;
+ cur.textContent = text;
+ }
+ }
+ })(node);
+
+ return {
+ removed: removed,
+ sc: begin,
+ ec: end,
+ so: !dom.isBR(begin) && so > 0 ? so : 0,
+ eo: dom.isBR(end) ? 0 : eo
+ };
+};
+dom.removeBetween = function (sc, so, ec, eo, towrite) {
+ var text;
+ if (ec.tagName) {
+ if (ec.childNodes[eo]) {
+ ec = ec.childNodes[eo];
+ eo = 0;
+ } else {
+ ec = dom.lastChild(ec);
+ eo = dom.nodeLength(ec);
+ }
+ }
+ if (sc.tagName) {
+ sc = sc.childNodes[so] || dom.firstChild(ec);
+ so = 0;
+ if (!dom.hasContentBefore(sc) && towrite) {
+ sc.parentNode.insertBefore(document.createTextNode('\u00A0'), sc);
+ }
+ }
+ if (!eo && sc !== ec) {
+ ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(ec)) || ec);
+ eo = ec.textContent.length;
+ }
+
+ var ancestor = dom.commonAncestor(sc.tagName ? sc.parentNode : sc, ec.tagName ? ec.parentNode : ec) || dom.ancestor(sc, dom.isEditable);
+
+ if (!dom.isContentEditable(ancestor)) {
+ return {
+ sc: sc,
+ so: so,
+ ec: sc,
+ eo: eo
+ };
+ }
+
+ if (ancestor.tagName) {
+ var ancestor_sc = sc;
+ var ancestor_ec = ec;
+ while (ancestor !== ancestor_sc && ancestor !== ancestor_sc.parentNode) { ancestor_sc = ancestor_sc.parentNode; }
+ while (ancestor !== ancestor_ec && ancestor !== ancestor_ec.parentNode) { ancestor_ec = ancestor_ec.parentNode; }
+
+
+ var node = dom.node(sc);
+ if (!dom.isNotBreakable(node) && !dom.isVoid(sc)) {
+ sc = dom.splitTree(ancestor_sc, {'node': sc, 'offset': so});
+ }
+ var before = dom.hasContentBefore(dom.ancestorHavePreviousSibling(sc));
+
+ var after;
+ if (ec.textContent.slice(eo, Infinity).match(/\S|\u00A0/)) {
+ after = dom.splitTree(ancestor_ec, {'node': ec, 'offset': eo});
+ } else {
+ after = dom.hasContentAfter(dom.ancestorHaveNextSibling(ec));
+ }
+
+ var nodes = dom.listBetween(sc, ec);
+
+ var ancestor_first_last = function (node) {
+ return node === before || node === after;
+ };
+
+ for (var i=0; i")[0];
+ node.appendChild(sc);
+ sc = br;
+ so = 0;
+ } else if (!ancestor.children.length && !ancestor.textContent.match(/\S|\u00A0/)) {
+ sc = $(" ")[0];
+ so = 0;
+ $(ancestor).prepend(sc);
+ } else if (dom.isText(sc)) {
+ text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
+ so = Math.min(so, text.length);
+ sc.textContent = text;
+ }
+ } else {
+ text = ancestor.textContent;
+ ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
+ }
+
+ eo = so;
+ if (!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
+ ancestor = dom.node(sc);
+ text = document.createTextNode('\u00A0');
+ $(sc).before(text);
+ sc = text;
+ so = 0;
+ eo = 1;
+ }
+
+ var parentNode = sc && sc.parentNode;
+ if (parentNode && sc.tagName === 'BR') {
+ sc = parentNode;
+ ec = parentNode;
+ }
+
+ return {
+ sc: sc,
+ so: so,
+ ec: sc,
+ eo: eo
+ };
+};
+dom.indent = function (node) {
+ var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
+ var margin = parseFloat(node.style[style] || 0)+1.5;
+ node.style[style] = margin + "em";
+ return margin;
+};
+dom.outdent = function (node) {
+ var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
+ var margin = parseFloat(node.style[style] || 0)-1.5;
+ node.style[style] = margin > 0 ? margin + "em" : "";
+ return margin;
+};
+dom.scrollIntoViewIfNeeded = function (node) {
+ node = dom.node(node);
+
+ var $span;
+ if (dom.isBR(node)) {
+ $span = $('').text('\u00A0');
+ $(node).after($span);
+ node = $span[0];
+ }
+
+ if (node.scrollIntoViewIfNeeded) {
+ node.scrollIntoViewIfNeeded(false);
+ } else {
+ var offsetParent = node.offsetParent;
+ while (offsetParent) {
+ var elY = 0;
+ var elH = node.offsetHeight;
+ var parent = node;
+
+ while (offsetParent && parent) {
+ elY += node.offsetTop;
+
+ // get if a parent have a scrollbar
+ parent = node.parentNode;
+ while (parent !== offsetParent &&
+ (parent.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(parent).overflowY) === -1)) {
+ parent = parent.parentNode;
+ }
+ node = parent;
+
+ if (parent !== offsetParent) {
+ elY -= parent.offsetTop;
+ parent = null;
+ }
+
+ offsetParent = node.offsetParent;
+ }
+
+ if ((node.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(node).overflowY) !== -1) &&
+ (node.scrollTop + node.clientHeight) < (elY + elH)) {
+ node.scrollTop = (elY + elH) - node.clientHeight;
+ }
+ }
+ }
+
+ if ($span) {
+ $span.remove();
+ }
+
+ return;
+};
+dom.moveTo = function (node, target, before) {
+ var nodes = [];
+ while (node.firstChild) {
+ nodes.push(node.firstChild);
+ if (before) {
+ target.insertBefore(node.firstChild, before);
+ } else {
+ target.appendChild(node.firstChild);
+ }
+ }
+ node.parentNode.removeChild(node);
+ return nodes;
+};
+dom.isMergable = function (node) {
+ return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isSplitable = function (node) {
+ return node.tagName && "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a font".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isRemovableEmptyNode = function (node) {
+ return "h1 h2 h3 h4 h5 h6 p b bold i u code sup strong small li a ul ol font span br".indexOf(node.tagName.toLowerCase()) !== -1;
+};
+dom.isForbiddenNode = function (node) {
+ return node.tagName === "BR" || $(node).is(".fa, img");
+};
+/**
+ * @todo 'so' and 'eo' were added as a bugfix and are not given everytime. They
+ * however should be as the function may be wrong without them (for example,
+ * when asking the list between an element and its parent, as there is no path
+ * from the beginning of the former to the beginning of the later).
+ */
+dom.listBetween = function (sc, ec, so, eo) {
+ var nodes = [];
+ var ancestor = dom.commonAncestor(sc, ec);
+ dom.walkPoint({'node': sc, 'offset': so || 0}, {'node': ec, 'offset': eo || 0}, function (point) {
+ if (ancestor !== point.node || ancestor === sc || ancestor === ec) {
+ nodes.push(point.node);
+ }
+ });
+ return list.unique(nodes);
+};
+dom.isNotBreakable = function (node) {
+ // avoid triple click => crappy dom
+ return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
+};
+dom.isContentEditable = function (node) {
+ return $(node).closest('[contenteditable]').prop('contenteditable') === 'true';
+};
+dom.isContentEditableFalse = function (node) {
+ return $(node).closest('[contenteditable]').prop('contenteditable') === 'false';
+};
+dom.isFont = function (node) {
+ var nodeName = node && node.nodeName.toUpperCase();
+ return node && (nodeName === "FONT" ||
+ (nodeName === "SPAN" && (
+ node.className.match(/(^|\s)fa(\s|$)/i) ||
+ node.className.match(/(^|\s)(text|bg)-/i) ||
+ (node.attributes.style && node.attributes.style.value.match(/(^|\s)(color|background-color|font-size):/i)))) );
+};
+dom.isVisibleText = function (textNode) {
+ return !!textNode.textContent.match(/\S|\u00A0/);
+};
+var old_isVisiblePoint = dom.isVisiblePoint;
+dom.isVisiblePoint = function (point) {
+ return point.node.nodeType !== 8 && old_isVisiblePoint.apply(this, arguments);
+};
+dom.orderStyle = function (node) {
+ var style = node.getAttribute('style');
+ if (!style) return null;
+ style = style.replace(/[\s\n\r]+/, ' ').replace(/^ ?;? ?| ?;? ?$/g, '').replace(/ ?; ?/g, ';');
+ if (!style.length) {
+ node.removeAttribute("style");
+ return null;
+ }
+ style = style.split(";");
+ style.sort();
+ style = style.join("; ")+";";
+ node.setAttribute('style', style);
+ return style;
+};
+dom.orderClass = function (node) {
+ var className = node.getAttribute && node.getAttribute('class');
+ if (!className) return null;
+ className = className.replace(/[\s\n\r]+/, ' ').replace(/^ | $/g, '').replace(/ +/g, ' ');
+ if (!className.length) {
+ node.removeAttribute("class");
+ return null;
+ }
+ className = className.split(" ");
+ className.sort();
+ className = className.join(" ");
+ node.setAttribute('class', className);
+ return className;
+};
+dom.node = function (node) {
+ return dom.isText(node) ? node.parentNode : node;
+};
+dom.moveContent = function (from, to) {
+ if (from === to) {
+ return;
+ }
+ if (from.parentNode === to) {
+ while (from.lastChild) {
+ dom.insertAfter(from.lastChild, from);
+ }
+ } else {
+ while (from.firstChild && from.firstChild !== to) {
+ to.appendChild(from.firstChild);
+ }
+ }
+};
+dom.getComputedStyle = function (node) {
+ return node.nodeType === Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
+};
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
+ var sc = this.sc;
+ var so = this.so;
+ var ec = this.ec;
+ var eo = this.eo;
+ isNotBreakable = isNotBreakable || dom.isNotBreakable;
+
+ // search the first snippet editable node
+ var start = keep_end ? ec : sc;
+ while (start) {
+ if (isNotBreakable(start, sc, so, ec, eo)) {
+ break;
+ }
+ start = start.parentNode;
+ }
+
+ // check if the end caret have the same node
+ var lastFilterEnd;
+ var end = keep_end ? sc : ec;
+ while (end) {
+ if (start === end) {
+ break;
+ }
+ if (isNotBreakable(end, sc, so, ec, eo)) {
+ lastFilterEnd = end;
+ }
+ end = end.parentNode;
+ }
+ if (lastFilterEnd) {
+ end = lastFilterEnd;
+ }
+ if (!end) {
+ end = document.getElementsByTagName('body')[0];
+ }
+
+ // if same node, keep range
+ if (start === end || !start) {
+ return this;
+ }
+
+ // reduce or extend the range to don't break a isNotBreakable area
+ if ($.contains(start, end)) {
+
+ if (keep_end) {
+ sc = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || sc);
+ so = sc.textContent.length;
+ } else if (!eo) {
+ ec = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(end)) || ec);
+ eo = ec.textContent.length;
+ } else {
+ ec = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(end)) || ec);
+ eo = 0;
+ }
+ } else {
+
+ if (keep_end) {
+ sc = dom.firstChild(start);
+ so = 0;
+ } else {
+ ec = dom.lastChild(start);
+ eo = ec.textContent.length;
+ }
+ }
+
+ return new range.WrappedRange(sc, so, ec, eo);
+};
+/**
+ * Returns the image the range is in or matches (if any, false otherwise).
+ *
+ * @todo this implementation may not cover all corner cases but should do the
+ * trick for all reproductible ones
+ * @returns {DOMElement|boolean}
+ */
+range.WrappedRange.prototype.isOnImg = function () {
+ // If not a selection but a cursor position, just check if a point's
+ // ancestor is an image or not
+ if (this.sc === this.ec && this.so === this.eo) {
+ return dom.ancestor(this.sc, dom.isImg);
+ }
+
+ var startPoint = {node: this.sc, offset: this.so};
+ var endPoint = {node: this.ec, offset: this.eo};
+
+ var nb = 0;
+ var image;
+ var textNode;
+ dom.walkPoint(startPoint, endPoint, function (point) {
+ // If the element has children (not a text node and not empty node),
+ // the element cannot be considered as selected (these children will
+ // be processed to determine that)
+ if (dom.hasChildren(point.node)) {
+ return;
+ }
+
+ // Check if an ancestor of the current point is an image
+ var pointImg = dom.ancestor(point.node, dom.isImg);
+ var isText = dom.isText(point.node);
+
+ // Check if a visible element is selected, i.e.
+ // - If an ancestor of the current is an image we did not see yet
+ // - If the point is not in a br or a text (so a node with no children)
+ // - If the point is in a non empty text node we already saw
+ if (pointImg ?
+ (image !== pointImg) :
+ ((!dom.isBR(point.node) && !isText) || (textNode === point.node && point.node.textContent.match(/\S|\u00A0/)))) {
+ nb++;
+ }
+
+ // If an ancestor of the current point is an image, then save it as the
+ // image we are looking for
+ if (pointImg) {
+ image = pointImg;
+ }
+ // If the current point is a text node save it as the last text node
+ // seen (if we see it again, this might mean it is selected)
+ if (isText) {
+ textNode = point.node;
+ }
+ });
+
+ return nb === 1 && image;
+};
+range.WrappedRange.prototype.deleteContents = function (towrite) {
+ if (this.sc === this.ec && this.so === this.eo) {
+ return this;
+ }
+
+ var r;
+ var image = this.isOnImg();
+ if (image) {
+ // If the range matches/is in an image, then the image is to be removed
+ // and the cursor moved to its previous position
+ var parentNode = image.parentNode;
+ var index = _.indexOf(parentNode.childNodes, image);
+ parentNode.removeChild(image);
+ r = new range.WrappedRange(parentNode, index, parentNode, index);
+ } else {
+ r = dom.removeBetween(this.sc, this.so, this.ec, this.eo, towrite);
+ }
+
+ $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ return new range.WrappedRange(r.sc, r.so, r.ec, r.eo);
+};
+range.WrappedRange.prototype.clean = function (mergeFilter, all) {
+ var node = dom.node(this.sc === this.ec ? this.sc : this.commonAncestor());
+ node = node || $(this.sc).closest('[contenteditable]')[0];
+ if (node.childNodes.length <=1) {
+ return this;
+ }
+
+ var merge = dom.merge(node, this.sc, this.so, this.ec, this.eo, mergeFilter, all);
+ var rem = dom.removeSpace(node.parentNode, merge.sc, merge.so, merge.ec, merge.eo);
+
+ if (merge.merged || rem.removed) {
+ return range.create(rem.sc, rem.so, rem.ec, rem.eo);
+ }
+ return this;
+};
+range.WrappedRange.prototype.remove = function (mergeFilter) {
+};
+range.WrappedRange.prototype.isOnCellFirst = function () {
+ var node = dom.ancestor(this.sc, function (node) {return ["LI", "DIV", "TD","TH"].indexOf(node.tagName) !== -1;});
+ return node && ["TD","TH"].indexOf(node.tagName) !== -1;
+};
+range.WrappedRange.prototype.isContentEditable = function () {
+ return dom.isContentEditable(this.sc) && (this.sc === this.ec || dom.isContentEditable(this.ec));
+};
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+renderer.tplButtonInfo.fontsize = function (lang, options) {
+ var items = options.fontSizes.reduce(function (memo, v) {
+ return memo + '' +
+ ' ' + v +
+ '';
+ }, '');
+
+ var sLabel = '11';
+ return renderer.getTemplate().button(sLabel, {
+ title: lang.font.size,
+ dropdown: '
' + items + '
'
+ });
+};
+
+renderer.tplButtonInfo.color = function (lang, options) {
+ var foreColorButtonLabel = '';
+ var backColorButtonLabel = '';
+ // TODO Remove recent color button if possible.
+ // It is still put to avoid JS errors when clicking other buttons as the
+ // editor still expects it to exist.
+ var recentColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
+ className: 'note-recent-color d-none',
+ title: lang.color.foreground,
+ event: 'color',
+ value: '{"backColor":"#B35E9B"}'
+ });
+ var foreColorItems = [
+ '
',
+ '',
+ '
',
+ lang.color.custom + '
',
+ '',
+ '
',
+ ];
+ var backColorItems = [
+ '
',
+ '
',
+ lang.color.setTransparent + '
',
+ '',
+ '
',
+ lang.color.custom + '
',
+ '',
+ '
',
+ ];
+ var foreColorButton = renderer.getTemplate().button(foreColorButtonLabel, {
+ className: 'note-fore-color-preview mx-1',
+ title: lang.color.foreground,
+ dropdown: renderer.getTemplate().dropdown(foreColorItems)
+ });
+ var backColorButton = renderer.getTemplate().button(backColorButtonLabel, {
+ className: 'note-back-color-preview mx-1',
+ title: lang.color.background,
+ dropdown: renderer.getTemplate().dropdown(backColorItems)
+ });
+ return recentColorButton + foreColorButton + backColorButton;
+},
+
+//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
+
+key.nameFromCode[46] = 'DELETE';
+key.nameFromCode[27] = 'ESCAPE';
+
+options.keyMap.pc['BACKSPACE'] = 'backspace';
+options.keyMap.pc['DELETE'] = 'delete';
+options.keyMap.pc['ENTER'] = 'enter';
+options.keyMap.pc['ESCAPE'] = 'cancel';
+options.keyMap.mac['SHIFT+TAB'] = 'untab';
+options.keyMap.pc['UP'] = 'up';
+options.keyMap.pc['DOWN'] = 'down';
+
+options.keyMap.mac['BACKSPACE'] = 'backspace';
+options.keyMap.mac['DELETE'] = 'delete';
+options.keyMap.mac['ENTER'] = 'enter';
+options.keyMap.mac['ESCAPE'] = 'cancel';
+options.keyMap.mac['UP'] = 'up';
+options.keyMap.mac['DOWN'] = 'down';
+
+options.styleTags = ['p', 'pre', 'small', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'blockquote'];
+
+$.summernote.pluginEvents.insertTable = function (event, editor, layoutInfo, sDim) {
+ var $editable = layoutInfo.editable();
+ var dimension = sDim.split('x');
+ var r = range.create();
+ if (!r) return;
+ r = r.deleteContents(true);
+
+ var table = editor.table.createTable(dimension[0], dimension[1]);
+ var parent = r.sc;
+ while (dom.isText(parent.parentNode) || dom.isRemovableEmptyNode(parent.parentNode)) {
+ parent = parent.parentNode;
+ }
+ var node = dom.splitTree(parent, {'node': r.sc, 'offset': r.so}) || r.sc;
+ node.parentNode.insertBefore(table, node);
+
+ if ($(node).text() === '' || node.textContent === '\u00A0') {
+ node.parentNode.removeChild(node);
+ }
+
+ editor.afterCommand($editable);
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, 'tab');
+ var r = range.create();
+ outdent = outdent || false;
+ event.preventDefault();
+
+ if (r && (dom.ancestor(r.sc, dom.isCell) || dom.ancestor(r.ec, dom.isCell))) {
+ if (r.isCollapsed() && r.isOnCell() && r.isOnCellFirst()) {
+ var td = dom.ancestor(r.sc, dom.isCell);
+ if (!outdent && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode)) {
+ var last = dom.lastChild(td);
+ range.create(last, dom.nodeLength(last), last, dom.nodeLength(last)).select();
+ $.summernote.pluginEvents.enter(event, editor, layoutInfo);
+ } else if (outdent && !dom.previousElementSibling(td) && !$(td.parentNode).text().match(/\S/)) {
+ $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
+ } else {
+ editor.table.tab(r, outdent);
+ }
+ } else {
+ $.summernote.pluginEvents.indent(event, editor, layoutInfo, outdent);
+ }
+ } else if (r && r.isCollapsed()) {
+ if (!r.sc.textContent.slice(0,r.so).match(/\S/) && r.isOnList()) {
+ if (outdent) {
+ $.summernote.pluginEvents.outdent(event, editor, layoutInfo);
+ } else {
+ $.summernote.pluginEvents.indent(event, editor, layoutInfo);
+ }
+ } else {
+ var next;
+ if (!outdent) {
+ if (dom.isText(r.sc)) {
+ next = r.sc.splitText(r.so);
+ } else {
+ next = document.createTextNode('');
+ $(r.sc.childNodes[r.so]).before(next);
+ }
+ editor.typing.insertTab($editable, r, options.tabsize);
+ r = range.create(next, 0, next, 0);
+ r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
+ range.create(r.sc, r.so, r.ec, r.eo).select();
+ } else {
+ r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
+ r = range.create(r.sc, r.so, r.ec, r.eo);
+ if (r.sc.splitText) {
+ next = r.sc.splitText(r.so);
+ r.sc.textContent = r.sc.textContent.replace(/(\u00A0)+$/g, '');
+ next.textContent = next.textContent.replace(/^(\u00A0)+/g, '');
+ range.create(r.sc, r.sc.textContent.length, r.sc, r.sc.textContent.length).select();
+ }
+ }
+ }
+ }
+ return false;
+};
+$.summernote.pluginEvents.untab = function (event, editor, layoutInfo) {
+ return $.summernote.pluginEvents.tab(event, editor, layoutInfo, true);
+};
+$.summernote.pluginEvents.up = function (event, editor, layoutInfo) {
+ var r = range.create();
+ var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
+ if (!r.isOnCell()) {
+ return;
+ }
+ // check if an ancestor between node and cell has content before
+ var ancestor = dom.ancestor(node, function (ancestorNode) {
+ return dom.hasContentBefore(ancestorNode) || dom.isCell(ancestorNode);
+ });
+ if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentBefore(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentBefore(dom.hasContentBefore(ancestor)))) {
+ return;
+ }
+ event.preventDefault();
+ var td = dom.ancestor(r.sc, dom.isCell);
+ var tr = td.parentNode;
+ var target = tr.previousElementSibling && tr.previousElementSibling.children[_.indexOf(tr.children, td)];
+ if (!target) {
+ target = (dom.ancestorHavePreviousSibling(tr) || tr).previousSibling;
+ }
+ if (target) {
+ range.create(dom.lastChild(target), dom.lastChild(target).textContent.length).select();
+ }
+};
+$.summernote.pluginEvents.down = function (event, editor, layoutInfo) {
+ var r = range.create();
+ var node = dom.firstChild(r.sc.childNodes[r.so] || r.sc);
+ if (!r.isOnCell()) {
+ return;
+ }
+ // check if an ancestor between node and cell has content after
+ var ancestor = dom.ancestor(node, function (ancestorNode) {
+ return dom.hasContentAfter(ancestorNode) || dom.isCell(ancestorNode);
+ });
+ if (!dom.isCell(ancestor) && (!dom.isBR(dom.hasContentAfter(ancestor)) || !dom.isText(node) || dom.isVisibleText(node) || dom.hasContentAfter(dom.hasContentAfter(ancestor)))) {
+ return;
+ }
+ event.preventDefault();
+ var td = dom.ancestor(r.sc, dom.isCell);
+ var tr = td.parentNode;
+ var target = tr.nextElementSibling && tr.nextElementSibling.children[_.indexOf(tr.children, td)];
+ if (!target) {
+ target = (dom.ancestorHaveNextSibling(tr) || tr).nextSibling;
+ }
+ if (target) {
+ range.create(dom.firstChild(target), 0).select();
+ }
+};
+$.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, 'enter');
+
+ var r = range.create();
+ if (!r.isContentEditable()) {
+ event.preventDefault();
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ r = r.deleteContents();
+ r.select();
+ }
+
+ var br = $(" ")[0];
+
+ // set selection outside of A if range is at beginning or end
+ var elem = dom.isBR(elem) ? elem.parentNode : dom.node(r.sc);
+ if (elem.tagName === "A") {
+ if (r.so === 0 && dom.firstChild(elem) === r.sc) {
+ r.ec = r.sc = dom.hasContentBefore(elem) || $(dom.createText('')).insertBefore(elem)[0];
+ r.eo = r.so = dom.nodeLength(r.sc);
+ r.select();
+ } else if (dom.nodeLength(r.sc) === r.so && dom.lastChild(elem) === r.sc) {
+ r.ec = r.sc = dom.hasContentAfter(elem) || dom.insertAfter(dom.createText(''), elem);
+ r.eo = r.so = 0;
+ r.select();
+ }
+ }
+
+ var node;
+ var $node;
+ var $clone;
+ var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
+ if (!contentBefore && dom.isText(r.sc)) {
+ node = r.sc.previousSibling;
+ while (!contentBefore && node && dom.isText(node)) {
+ contentBefore = dom.isVisibleText(node);
+ node = node.previousSibling;
+ }
+ }
+
+ node = dom.node(r.sc);
+ var exist = r.sc.childNodes[r.so] || r.sc;
+ exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);
+
+ // table: add a tr
+ var td = dom.ancestor(node, dom.isCell);
+ if (td && !dom.nextElementSibling(node) && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode) && (!dom.isText(r.sc) || !r.sc.textContent.slice(r.so).match(/\S|\u00A0/))) {
+ $node = $(td.parentNode);
+ $clone = $node.clone();
+ $clone.children().html(dom.blank);
+ $node.after($clone);
+ node = dom.firstElementChild($clone[0]) || $clone[0];
+ range.create(node, 0, node, 0).select();
+ dom.scrollIntoViewIfNeeded(br);
+ event.preventDefault();
+ return false;
+ }
+
+ var last = node;
+ while (node && dom.isSplitable(node) && !dom.isList(node)) {
+ last = node;
+ node = node.parentNode;
+ }
+
+ if (last === node && !dom.isBR(node)) {
+ node = r.insertNode(br, true);
+ if (isFormatNode(last.firstChild) && $(last).closest(options.styleTags.join(',')).length) {
+ dom.moveContent(last.firstChild, last);
+ last.removeChild(last.firstChild);
+ }
+ do {
+ node = dom.hasContentAfter(node);
+ } while (node && dom.isBR(node));
+
+ // create an other br because the user can't see the new line with only br in a block
+ if (!node && (!br.nextElementSibling || !dom.isBR(br.nextElementSibling))) {
+ $(br).before($(" ")[0]);
+ }
+ node = br.nextSibling || br;
+ } else if (last === node && dom.isBR(node)) {
+ $(node).after(br);
+ node = br;
+ } else if (!r.so && r.isOnList() && !r.sc.textContent.length && !dom.ancestor(r.sc, dom.isLi).nextElementSibling) {
+ // double enter on the end of a list = new line out of the list
+ $('').append(br).insertAfter(dom.ancestor(r.sc, dom.isList));
+ node = br;
+ } else if (dom.isBR(exist) && $(r.sc).closest('blockquote, pre').length && !dom.hasContentAfter($(exist.parentNode).closest('blockquote *, pre *').length ? exist.parentNode : exist)) {
+ // double enter on the end of a blockquote & pre = new line out of the list
+ $('').append(br).insertAfter($(r.sc).closest('blockquote, pre'));
+ node = br;
+ } else if (dom.isEditable(dom.node(r.sc))) {
+ // if we are directly in an editable, only SHIFT + ENTER should add a newline
+ node = null;
+ } else if (last === r.sc) {
+ if (dom.isBR(last)) {
+ last = last.parentNode;
+ }
+ $node = $(last);
+ $clone = $node.clone().text("");
+ $node.after($clone);
+ node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
+ $(node).html(br);
+ node = br;
+ } else {
+ node = dom.splitTree(last, {'node': r.sc, 'offset': r.so}) || r.sc;
+ if (!contentBefore) {
+ // dom.node chooses the parent if node is text
+ var cur = dom.node(dom.lastChild(node.previousSibling));
+ if (!dom.isBR(cur)) {
+ // We should concat what was before with a
+ $(cur).html(cur.innerHTML + br.outerHTML);
+ }
+ }
+ if (!dom.isVisibleText(node)) {
+ node = dom.firstChild(node);
+ $(dom.node( dom.isBR(node) ? node.parentNode : node )).html(br);
+ node = br;
+ }
+ }
+
+ if (node) {
+ node = dom.firstChild(node);
+ if (dom.isBR(node)) {
+ range.createFromNode(node).select();
+ } else {
+ range.create(node,0).select();
+ }
+ dom.scrollIntoViewIfNeeded(node);
+ }
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "visible");
+
+ var r = range.create();
+ if (!r) return;
+
+ if (!r.isCollapsed()) {
+ if ((dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) && dom.node(r.sc) !== dom.node(r.ec)) {
+ remove_table_content(r);
+ r = range.create(r.ec, 0);
+ }
+ r.select();
+ }
+
+ // don't write in forbidden tag (like span for font awsome)
+ var node = dom.firstChild(r.sc.tagName && r.so ? r.sc.childNodes[r.so] || r.sc : r.sc);
+ while (node.parentNode) {
+ if (dom.isForbiddenNode(node)) {
+ var text = node.previousSibling;
+ if (text && dom.isText(text) && dom.isVisibleText(text)) {
+ range.create(text, text.textContent.length, text, text.textContent.length).select();
+ } else {
+ text = node.parentNode.insertBefore(document.createTextNode( "." ), node);
+ range.create(text, 1, text, 1).select();
+ setTimeout(function () {
+ var text = range.create().sc;
+ text.textContent = text.textContent.replace(/^./, '');
+ range.create(text, text.textContent.length, text, text.textContent.length).select();
+ },0);
+ }
+ break;
+ }
+ node = node.parentNode;
+ }
+
+ return true;
+};
+
+function remove_table_content(r) {
+ var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
+ if (dom.isText(r.sc)) {
+ r.sc.textContent = r.sc.textContent.slice(0, r.so);
+ }
+ if (dom.isText(r.ec)) {
+ r.ec.textContent = r.ec.textContent.slice(r.eo);
+ }
+ for (var i in nodes) {
+ var node = nodes[i];
+ if (node === r.sc || node === r.ec || $.contains(node, r.sc) || $.contains(node, r.ec)) {
+ continue;
+ } else if (dom.isCell(node)) {
+ $(node).html(" ");
+ } else if (node.parentNode) {
+ do {
+ var parent = node.parentNode;
+ parent.removeChild(node);
+ node = parent;
+ } while (!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
+ !dom.isCell(node) &&
+ node.parentNode && !$(node.parentNode).hasClass('o_editable'));
+ }
+ }
+ return false;
+}
+
+$.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "delete");
+
+ var r = range.create();
+ if (!r) return;
+ if (!r.isContentEditable()) {
+ event.preventDefault();
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
+ remove_table_content(r);
+ range.create(r.ec, 0).select();
+ } else {
+ r = r.deleteContents();
+ r.select();
+ }
+ event.preventDefault();
+ return false;
+ }
+
+ var target = r.ec;
+ var offset = r.eo;
+ if (target.tagName && target.childNodes[offset]) {
+ target = target.childNodes[offset];
+ offset = 0;
+ }
+
+ var node = dom.node(target);
+ var data = dom.merge(node, target, offset, target, offset, null, true);
+ data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
+ r = range.create(data.sc, data.so);
+ r.select();
+ target = r.sc;
+ offset = r.so;
+
+ while (!dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
+
+ var contentAfter = target.textContent.slice(offset,Infinity).match(/\S|\u00A0/);
+ var content = target.textContent.replace(/[ \t\r\n]+$/, '');
+ var temp;
+ var temp2;
+ var next;
+
+ // media
+ if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
+ var parent;
+ var index;
+ if (!dom.isImg(node)) {
+ node = dom.hasContentAfter(node);
+ }
+ while (dom.isImg(node)) {
+ parent = node.parentNode;
+ index = dom.position(node);
+ if (index>0) {
+ next = node.previousSibling;
+ r = range.create(next, next.textContent.length);
+ } else {
+ r = range.create(parent, 0);
+ }
+ if (!dom.hasContentAfter(node) && !dom.hasContentBefore(node)) {
+ parent.appendChild($(' ')[0]);
+ }
+ parent.removeChild(node);
+ node = parent;
+ r.select();
+ }
+ }
+ // empty tag
+ else if (!content.length && target.tagName && dom.isRemovableEmptyNode(dom.isBR(target) ? target.parentNode : target)) {
+ if (node === $editable[0] || $.contains(node, $editable[0])) {
+ event.preventDefault();
+ return false;
+ }
+ var before = false;
+ next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
+ if (!dom.isContentEditable(next)) {
+ before = true;
+ next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
+ }
+ dom.removeSpace(next.parentNode, next, 0, next, 0); // clean before jump for not select invisible space between 2 tag
+ next = dom.firstChild(next);
+ node.parentNode.removeChild(node);
+ range.create(next, before ? next.textContent.length : 0).select();
+ }
+ // normal feature if same tag and not the end
+ else if (contentAfter) {
+ return true;
+ }
+ // merge with the next text node
+ else if (dom.isText(target) && (temp = dom.hasContentAfter(target)) && dom.isText(temp)) {
+ return true;
+ }
+ //merge with the next block
+ else if ((temp = dom.ancestorHaveNextSibling(target)) &&
+ !r.isOnCell() &&
+ dom.isMergable(temp) &&
+ dom.isMergable(temp2 = dom.hasContentAfter(temp)) &&
+ temp.tagName === temp2.tagName &&
+ (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
+ !dom.isNotBreakable(temp) &&
+ !dom.isNotBreakable(temp2)) {
+ dom.autoMerge(target, false);
+ next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
+ if (dom.isBR(next)) {
+ if (dom.position(next) === 0) {
+ range.create(next.parentNode, 0).select();
+ }
+ else {
+ range.create(next.previousSibling, next.previousSibling.textContent.length).select();
+ }
+ next.parentNode.removeChild(next);
+ } else {
+ range.create(next, 0).select();
+ }
+ }
+ // jump to next node for delete
+ else if ((temp = dom.ancestorHaveNextSibling(target)) && (temp2 = dom.hasContentAfter(temp)) && dom.isContentEditable(temp2)) {
+
+ dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
+ temp2 = dom.firstChild(temp2);
+
+ r = range.create(temp2, 0);
+ r.select();
+
+ if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
+ if (dom.isText(temp2)) {
+ temp2.textContent = temp2.textContent.replace(/^\s*\S/, '');
+ } else {
+ $.summernote.pluginEvents.delete(event, editor, layoutInfo);
+ }
+ }
+ }
+
+ $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ event.preventDefault();
+ return false;
+};
+$.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable, "backspace");
+
+ var r = range.create();
+ if (!r) return;
+ if (!r.isContentEditable()) {
+ event.preventDefault();
+ return false;
+ }
+ if (!r.isCollapsed()) {
+ if (dom.isCell(dom.node(r.sc)) || dom.isCell(dom.node(r.ec))) {
+ remove_table_content(r);
+ range.create(r.sc, dom.nodeLength(r.sc)).select();
+ } else {
+ r = r.deleteContents();
+ r.select();
+ }
+ event.preventDefault();
+ return false;
+ }
+
+ var target = r.sc;
+ var offset = r.so;
+ if (target.tagName && target.childNodes[offset]) {
+ target = target.childNodes[offset];
+ offset = 0;
+ }
+
+ var node = dom.node(target);
+ var data = dom.merge(node, target, offset, target, offset, null, true);
+ data = dom.removeSpace(node.parentNode, data.sc, data.so, data.ec, data.eo);
+ r = dom.isVoid(data.sc) ? range.createFromNode(data.sc) : range.create(data.sc, data.so);
+ r.select();
+ target = r.sc;
+ offset = r.so;
+ if (target.tagName && target.childNodes[offset]) {
+ target = target.childNodes[offset];
+ offset = 0;
+ node = dom.node(target);
+ }
+
+ while (node.parentNode && !dom.hasContentAfter(node) && !dom.hasContentBefore(node) && !dom.isImg(node)) {node = node.parentNode;}
+
+ var contentBefore = target.textContent.slice(0,offset).match(/\S|\u00A0/);
+ var content = target.textContent.replace(/[ \t\r\n]+$/, '');
+ var temp;
+ var temp2;
+ var prev;
+
+ // delete media
+ if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
+ if (!dom.isImg(node)) {
+ node = dom.hasContentBefore(node);
+ }
+ range.createFromNode(node).select();
+ $.summernote.pluginEvents.delete(event, editor, layoutInfo);
+ }
+ // table tr td
+ else if (r.isOnCell() && !offset && (target === (temp = dom.ancestor(target, dom.isCell)) || target === temp.firstChild || (dom.isText(temp.firstChild) && !dom.isVisibleText(temp.firstChild) && target === temp.firstChild.nextSibling))) {
+ if (dom.previousElementSibling(temp)) {
+ var td = dom.previousElementSibling(temp);
+ node = td.lastChild || td;
+ } else {
+ var tr = temp.parentNode;
+ var prevTr = dom.previousElementSibling(tr);
+ if (!$(temp.parentNode).text().match(/\S|\u00A0/)) {
+ if (prevTr) {
+ node = dom.lastChild(dom.lastElementChild(prevTr));
+ } else {
+ node = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(tr)) || $editable.get(0));
+ }
+ $(tr).empty();
+ if (!$(tr).closest('table').has('td, th').length) {
+ $(tr).closest('table').remove();
+ }
+ $(tr).remove();
+ range.create(node, node.textContent.length, node, node.textContent.length).select();
+ } else {
+ node = dom.lastElementChild(prevTr).lastChild || dom.lastElementChild(prevTr);
+ }
+ }
+ if (dom.isBR(node)) {
+ range.createFromNode(node).select();
+ } else {
+ range.create(node, dom.nodeLength(node)).select();
+ }
+ }
+ // empty tag
+ else if (!content.length && target.tagName && dom.isRemovableEmptyNode(target)) {
+ if (node === $editable[0] || $.contains(node, $editable[0])) {
+ event.preventDefault();
+ return false;
+ }
+ var before = true;
+ prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
+ if (!dom.isContentEditable(prev)) {
+ before = false;
+ prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
+ }
+ dom.removeSpace(prev.parentNode, prev, 0, prev, 0); // clean before jump for not select invisible space between 2 tag
+ prev = dom.lastChild(prev);
+ node.parentNode.removeChild(node);
+ range.createFromNode(prev).select();
+ range.create(prev, before ? prev.textContent.length : 0).select();
+ }
+ // normal feature if same tag and not the begin
+ else if (contentBefore) {
+ return true;
+ }
+ // merge with the previous text node
+ else if (dom.isText(target) && (temp = dom.hasContentBefore(target)) && (dom.isText(temp) || dom.isBR(temp))) {
+ return true;
+ }
+ //merge with the previous block
+ else if ((temp = dom.ancestorHavePreviousSibling(target)) &&
+ dom.isMergable(temp) &&
+ dom.isMergable(temp2 = dom.hasContentBefore(temp)) &&
+ temp.tagName === temp2.tagName &&
+ (temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
+ !dom.isNotBreakable(temp) &&
+ !dom.isNotBreakable(temp2)) {
+ prev = dom.firstChild(target);
+ dom.autoMerge(target, true);
+ range.create(prev, 0).select();
+ }
+ // jump to previous node for delete
+ else if ((temp = dom.ancestorHavePreviousSibling(target)) && (temp2 = dom.hasContentBefore(temp)) && dom.isContentEditable(temp2)) {
+
+ dom.removeSpace(temp2.parentNode, temp2, 0, temp, 0); // clean before jump for not select invisible space between 2 tag
+ temp2 = dom.lastChild(temp2);
+
+ r = range.create(temp2, temp2.textContent.length, temp2, temp2.textContent.length);
+ r.select();
+
+ if ((dom.isText(temp) || dom.getComputedStyle(temp).display === "inline") && (dom.isText(temp2) || dom.getComputedStyle(temp2).display === "inline")) {
+ if (dom.isText(temp2)) {
+ temp2.textContent = temp2.textContent.replace(/\S\s*$/, '');
+ } else {
+ $.summernote.pluginEvents.backspace(event, editor, layoutInfo);
+ }
+ }
+ }
+
+ r = range.create();
+ if (r) {
+ $(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
+ dom.scrollIntoViewIfNeeded(r.sc.parentNode.previousElementSibling || r.sc);
+ }
+
+ event.preventDefault();
+ return false;
+};
+
+function isFormatNode(node) {
+ return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
+}
+
+$.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutInfo, sorted) {
+ var $editable = layoutInfo.editable();
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var parent;
+ var r = range.create();
+ if (!r) return;
+ var node = r.sc;
+ while (node && node !== $editable[0]) {
+
+ parent = node.parentNode;
+ if (node.tagName === (sorted ? "UL" : "OL")) {
+
+ var ul = document.createElement(sorted ? "ol" : "ul");
+ ul.className = node.className;
+ parent.insertBefore(ul, node);
+ while (node.firstChild) {
+ ul.appendChild(node.firstChild);
+ }
+ parent.removeChild(node);
+ r.select();
+ return;
+
+ } else if (node.tagName === (sorted ? "OL" : "UL")) {
+
+ var lis = [];
+ for (var i=0; i= r.eo) {
+ r.ec = dom.lastChild(r.ec);
+ r.eo = dom.nodeLength(r.ec);
+ }
+ r = range.create(r.sc, r.so, r.ec, r.eo);
+ r.reRange().select();
+
+ if (sTagName === "blockquote" || sTagName === "pre") {
+ sTagName = $.summernote.core.agent.isMSIE ? '<' + sTagName + '>' : sTagName;
+ document.execCommand('FormatBlock', false, sTagName);
+ return;
+ }
+
+ // fix by odoo because if you select a style in a li with no p tag all the ul is wrapped by the style tag
+ var nodes = dom.listBetween(r.sc, r.ec, r.so, r.eo);
+ for (var i=0; i 0.05) {
+ font.style.fontSize = size + "px";
+ }
+ }
+ }
+ }
+
+ // remove empty values
+ // we must remove the value in 2 steps (applay inherit then remove) because some
+ // browser like chrome have some time an error for the rendering and/or keep inherit
+ for (i=0; i0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
+ className2 = font.getAttribute('class');
+ style2 = font.getAttribute('style');
+ if (node !== font && className === className2 && style === style2) {
+ remove(node, font);
+ nodes.splice(i,1);
+ i--;
+ continue;
+ }
+ }
+ }
+
+ range.create(startPoint.node, startPoint.offset, endPoint.node, endPoint.offset).select();
+};
+$.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value) {
+ var $editable = layoutInfo.editable();
+ event.preventDefault();
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, null, value);
+ editor.afterCommand($editable);
+};
+$.summernote.pluginEvents.color = function (event, editor, layoutInfo, sObjColor) {
+ var oColor = JSON.parse(sObjColor);
+ var foreColor = oColor.foreColor, backColor = oColor.backColor;
+
+ if (foreColor) { $.summernote.pluginEvents.foreColor(event, editor, layoutInfo, foreColor); }
+ if (backColor) { $.summernote.pluginEvents.backColor(event, editor, layoutInfo, backColor); }
+};
+$.summernote.pluginEvents.foreColor = function (event, editor, layoutInfo, foreColor) {
+ var $editable = layoutInfo.editable();
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, foreColor, null, null);
+ editor.afterCommand($editable);
+};
+$.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backColor) {
+ var $editable = layoutInfo.editable();
+ var r = range.create();
+ if (!r) return;
+ if (r.isCollapsed() && r.isOnCell()) {
+ var cell = dom.ancestor(r.sc, dom.isCell);
+ cell.className = cell.className.replace(new RegExp('(^|\\s+)bg-[^\\s]+(\\s+|$)', 'gi'), '');
+ cell.style.backgroundColor = "";
+ if (backColor.indexOf('bg-') !== -1) {
+ cell.className += ' ' + backColor;
+ } else if (backColor !== 'inherit') {
+ cell.style.backgroundColor = backColor;
+ }
+ return;
+ }
+ $.summernote.pluginEvents.applyFont(event, editor, layoutInfo, null, backColor, null);
+ editor.afterCommand($editable);
+};
+
+options.onCreateLink = function (sLinkUrl) {
+ if (sLinkUrl.indexOf('mailto:') === 0 || sLinkUrl.indexOf('tel:') === 0) {
+ sLinkUrl = sLinkUrl.replace(/^tel:([0-9]+)$/, 'tel://$1');
+ } else if (sLinkUrl.indexOf('@') !== -1 && sLinkUrl.indexOf(':') === -1) {
+ sLinkUrl = 'mailto:' + sLinkUrl;
+ } else if (sLinkUrl.indexOf('://') === -1 && sLinkUrl[0] !== '/'
+ && sLinkUrl[0] !== '#' && sLinkUrl.slice(0, 2) !== '${') {
+ sLinkUrl = 'http://' + sLinkUrl;
+ }
+ return sLinkUrl;
+};
+
+function summernote_table_scroll(event) {
+ var r = range.create();
+ if (r && r.isOnCell()) {
+ $('.o_table_handler').remove();
+ }
+}
+function summernote_table_update(oStyle) {
+ var r = range.create();
+ if (!oStyle.range || !r || !r.isOnCell() || !r.isOnCellFirst()) {
+ $('.o_table_handler').remove();
+ return;
+ }
+ var table = dom.ancestor(oStyle.range.sc, dom.isTable);
+ if (!table) { // if the editable tag is inside the table
+ return;
+ }
+ var $editable = $(table).closest('.o_editable');
+
+ $('.o_table_handler').remove();
+
+ var $dels = $();
+ var $adds = $();
+ var $tds = $('tr:first', table).children();
+ $tds.each(function () {
+ var $td = $(this);
+ var pos = $td.offset();
+
+ var $del = $('').appendTo('body');
+ $del.data('td', this);
+ $dels = $dels.add($del);
+ $del.css({
+ left: ((pos.left + $td.outerWidth()/2)-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+
+ var $add = $('').appendTo('body');
+ $add.data('td', this);
+ $adds = $adds.add($add);
+ $add.css({
+ left: (pos.left-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+ });
+
+ var $last = $tds.last();
+ var pos = $last.offset();
+ var $add = $('').appendTo('body');
+ $adds = $adds.add($add);
+ $add.css({
+ left: (pos.left+$last.outerWidth()-6) + "px",
+ top: (pos.top-6) + "px"
+ });
+
+ var $table = $(table);
+ $dels.data('table', table).on('mousedown', function (event) {
+ var td = $(this).data('td');
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var newTd;
+ if ($(td).siblings().length) {
+ var eq = $(td).index();
+ $table.find('tr').each(function () {
+ $('> td:eq('+eq+')', this).remove();
+ });
+ newTd = $table.find('tr:first > td:eq('+eq+'), tr:first > td:last').first();
+ } else {
+ var prev = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling($table[0])));
+ $table.remove();
+ $('.o_table_handler').remove();
+ r = range.create(prev, prev.textContent.length);
+ r.select();
+ $(r.sc).trigger('mouseup');
+ return;
+ }
+
+ $('.o_table_handler').remove();
+ range.create(newTd[0], 0, newTd[0], 0).select();
+ newTd.trigger('mouseup');
+ });
+ $adds.data('table', table).on('mousedown', function (event) {
+ var td = $(this).data('td');
+ $editable.data('NoteHistory').recordUndo($editable);
+
+ var newTd;
+ if (td) {
+ var eq = $(td).index();
+ $table.find('tr').each(function () {
+ $('td:eq('+eq+')', this).before('
');
+ });
+ newTd = $table.find('tr:first td:last');
+ }
+
+ $('.o_table_handler').remove();
+ range.create(newTd[0], 0, newTd[0], 0).select();
+ newTd.trigger('mouseup');
+ });
+
+ $dels.css({
+ 'position': 'absolute',
+ 'cursor': 'pointer',
+ 'background-color': '#fff',
+ 'color': '#ff0000'
+ });
+ $adds.css({
+ 'position': 'absolute',
+ 'cursor': 'pointer',
+ 'background-color': '#fff',
+ 'color': '#00ff00'
+ });
+}
+var fn_popover_update = eventHandler.modules.popover.update;
+eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
+ fn_popover_update.call(this, $popover, oStyle, isAirMode);
+ if ((isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
+ summernote_table_update(oStyle);
+ }
+};
+
+var fn_attach = eventHandler.attach;
+eventHandler.attach = function (oLayoutInfo, options) {
+ var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
+ fn_attach.call(this, oLayoutInfo, options);
+ $editable.on("scroll", summernote_table_scroll);
+};
+var fn_detach = eventHandler.detach;
+eventHandler.detach = function (oLayoutInfo, options) {
+ var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
+ fn_detach.call(this, oLayoutInfo, options);
+ $editable.off("scroll", summernote_table_scroll);
+ $('.o_table_handler').remove();
+};
+
+options.icons.image.image = "file-image-o";
+$.summernote.lang['en-US'].image.image = "File / Image";
+
+return $.summernote;
+});
diff --git a/addons/web_editor/static/src/js/backend/convert_inline.js b/addons/web_editor/static/src/js/editor/transcoder.js
similarity index 83%
rename from addons/web_editor/static/src/js/backend/convert_inline.js
rename to addons/web_editor/static/src/js/editor/transcoder.js
index b5c69102ff6..745e8492b11 100644
--- a/addons/web_editor/static/src/js/backend/convert_inline.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -1,7 +1,9 @@
-odoo.define('web_editor.convertInline', function (require) {
+odoo.define('web_editor.transcoder', function (require) {
'use strict';
-var FieldHtml = require('web_editor.field.html');
+var base = require('web_editor.base');
+
+var rulesCache = [];
/**
* Returns the css rules which applies on an element, tweaked so that they are
@@ -12,11 +14,8 @@ var FieldHtml = require('web_editor.field.html');
*/
function getMatchedCSSRules(a) {
var i, r, k;
- var doc = a.ownerDocument;
- var rulesCache = a.ownerDocument._rulesCache || (a.ownerDocument._rulesCache = []);
-
if (!rulesCache.length) {
- var sheets = doc.styleSheets;
+ var sheets = document.styleSheets;
for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
// try...catch because browser may not able to enumerate rules for cross-domain sheets
@@ -188,13 +187,11 @@ function getMatchedCSSRules(a) {
* converted to images
*/
function fontToImg($editable) {
- var fonts = odoo.__DEBUG__.services["wysiwyg.fonts"];
-
$editable.find('.fa').each(function () {
var $font = $(this);
var icon, content;
- _.find(fonts.fontIcons, function (font) {
- return _.find(fonts.getCssSelectors(font.parser), function (data) {
+ _.find(base.fontIcons, function (font) {
+ return _.find(base.getCssSelectors(font.parser), function (data) {
if ($font.is(data.selector.replace(/::?before/g, ''))) {
icon = data.names[0].split('-').shift();
content = data.css.match(/content:\s*['"]?(.)['"]?/)[1];
@@ -268,6 +265,9 @@ function applyOverDescendants(node, func) {
* @param {jQuery} $editable
*/
function classToStyle($editable) {
+ if (!rulesCache.length) {
+ getMatchedCSSRules($editable[0]);
+ }
applyOverDescendants($editable[0], function (node) {
var $target = $(node);
var css = getMatchedCSSRules(node);
@@ -328,7 +328,9 @@ function styleToClass($editable) {
$(this).after($('a,img', this));
}).remove();
- var $c = $('').appendTo($editable[0].ownerDocument.body);
+ getMatchedCSSRules($editable[0]);
+
+ var $c = $('').appendTo(document.body);
applyOverDescendants($editable[0], function (node) {
var $target = $(node);
@@ -340,7 +342,7 @@ function styleToClass($editable) {
}
});
css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
- style = ($target.attr('style') || '').replace(/\s*:\s*/, ':').replace(/\s*;\s*/, ';');
+ style = $target.attr('style') || '';
_.each(css, function (v) {
style = style.replace(v, '');
});
@@ -362,13 +364,13 @@ function styleToClass($editable) {
* @param {jQuery} $editable
*/
function attachmentThumbnailToLinkImg($editable) {
- $editable.find('a[href*="/web/content/"][data-mimetype]').filter(':empty, :containsExact( )').each(function () {
+ $editable.find('a[href*="/web/content/"][data-mimetype]:empty').each(function () {
var $link = $(this);
var $img = $('')
.attr('src', $link.css('background-image').replace(/(^url\(['"])|(['"]\)$)/g, ''))
.css('height', Math.max(1, $link.height()) + 'px')
.css('width', Math.max(1, $link.width()) + 'px');
- $link.prepend($img);
+ $link.append($img);
});
}
@@ -382,83 +384,6 @@ function linkImgToAttachmentThumbnail($editable) {
$editable.find('a[href*="/web/content/"][data-mimetype] > img').remove();
}
-
-//--------------------------------------------------------------------------
-//--------------------------------------------------------------------------
-
-
-FieldHtml.include({
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- commitChanges: function () {
- if (this.nodeOptions['style-inline'] && this.isRendered) {
- this._toInline();
- }
- return this._super();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Converts CSS dependencies to CSS-independent HTML.
- * - CSS display for attachment link -> real image
- * - Font icons -> images
- * - CSS styles -> inline styles
- *
- * @private
- */
- _toInline: function () {
- var $editable = this.wysiwyg.getEditable();
- var html = this.wysiwyg.getValue();
- $editable.html(html);
-
- attachmentThumbnailToLinkImg($editable);
- fontToImg($editable);
- classToStyle($editable);
- this.wysiwyg.setValue($editable.html(), {
- notifyChange: false,
- });
- },
- /**
- * Revert _toInline changes.
- *
- * @private
- */
- _fromInline: function () {
- var $editable = this.wysiwyg.getEditable();
- var html = this.wysiwyg.getValue();
- $editable.html(html);
-
- styleToClass($editable);
- imgToFont($editable);
- linkImgToAttachmentThumbnail($editable);
- this.wysiwyg.setValue($editable.html(), {
- notifyChange: false,
- });
- },
-
- //--------------------------------------------------------------------------
- // Handler
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _onLoadWysiwyg: function () {
- if (this.nodeOptions['style-inline']) {
- this._fromInline();
- }
- this._super();
- },
-});
-
return {
fontToImg: fontToImg,
imgToFont: imgToFont,
diff --git a/addons/web_editor/static/src/js/tours/rte.js b/addons/web_editor/static/src/js/tours/rte.js
new file mode 100644
index 00000000000..abdcd39f35b
--- /dev/null
+++ b/addons/web_editor/static/src/js/tours/rte.js
@@ -0,0 +1,325 @@
+odoo.define('web_editor.tour', function (require) {
+'use strict';
+
+var core = require('web.core');
+var tour = require('web_tour.tour');
+var base = require('web_editor.base');
+
+/**
+ * Simulates a click event of given type on the given element.
+ *
+ * @param {DOMElement} el
+ * @param {string} type - 'click', 'mouseup', ...
+ */
+function simulateClickEvent(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);
+}
+
+tour.register('rte', {
+ url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ test: true,
+ wait_for: base.ready(),
+}, [{
+ content: "Change html for this test",
+ trigger: "#editable_area",
+ run: function () {
+ var html = '\n'+
+ '\n'+
+ '
\n'+
+ '
\n'+
+ '
\n'+
+ '
Batnae municipium in Anthemusia
\n'+
+ '
Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.
\n'+
+ '
Quam quidem partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.
\n'+
+ '
Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.
';
+ this.$anchor.append(html);
+ var node = this.$anchor.find('ul li p:last')[0].firstChild;
+ $.summernote.core.range.create(node, 6).select();
+ simulateClickEvent(node, 'mouseup');
+ }
+}, {
+ content: "click on style dropdown",
+ trigger: '.note-air-popover .note-style button.dropdown-toggle',
+ extra_trigger: '#editable_area > section .row > div:first ul li p:first',
+}, {
+ content: "select h3",
+ trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
+}, {
+ content: "select h3",
+ trigger: '#editable_area > section .row > div:first > ul > li > h3',
+ run: function () {
+ var node = this.$anchor[0].firstChild;
+ $.summernote.core.range.create(node, 0).select();
+ simulateClickEvent(node, 'mouseup');
+ }
+}, {
+ content: "double tabulation",
+ trigger: '#editable_area > section .row > div:first > ul > li > h3',
+ run: "keydown 9,9", // tabulation
+}, {
+ content: "click on order list",
+ trigger: '.note-air-popover button[data-event="insertOrderedList"]',
+ extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
+}, {
+ content: "select for enter in ul",
+ trigger: '#editable_area > section .row > div:first ul li > p:last',
+ run: function () {
+ this.$anchor[0].firstChild.textContent += "";
+ $.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
+ simulateClickEvent(this.$anchor[0], 'mouseup');
+ }
+}, {
+ content: "enter in ul",
+ trigger: '#editable_area > section .row > div:first ul li > p:last',
+ run: "keydown 66,13", // enter
+}, {
+ trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
+ content: "backspace in list",
+ run: "keydown 8",
+}, {
+ content: "end",
+ trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
+}]);
+
+tour.register('rte_inline', {
+ url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
+ test: true,
+ wait_for: base.ready(),
+}, [{
+ content: "Change html for this test",
+ trigger: "#editable_area",
+ run: function () {
+ var html = '\n'+
+ '
\n'+
+ '
\n'+
+ ' \n'+
+ '
\n'+
+ '
\n'+
+ ' \n'+
+ '
\n'+
+ '
\n'+
+ ' \n'+
+ ' \n'+
+ '
\n'+
+ '
\n'+
+ ' \n'+
+ '
\n'+
+ '
';
+ this.$anchor.html(html);
+ }
+}, {
+ content: "call clean for save",
+ trigger: '#wrapwrap table',
+ run: function () {
+ core.bus.trigger('snippet_editor_clean_for_save');
+ }
+}, {
+ content: "check the image style",
+ trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
+}, {
+ content: "check the font image src",
+ trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(0,160,157)/28"]',
+}, {
+ content: "check the font class to css",
+ trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
+}, {
+ content: "check the second font class to css",
+ trigger: '#wrapwrap img:eq(2)[style*="float: right"],#wrapwrap img:eq(2)[style*="float:right"]',
+}]);
+});
diff --git a/addons/web_editor/static/src/js/wysiwyg/options.js b/addons/web_editor/static/src/js/wysiwyg/options.js
deleted file mode 100644
index aeaf8422b72..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/options.js
+++ /dev/null
@@ -1,6 +0,0 @@
-odoo.define('web_editor.wysiwyg.options', function (require) {
-'use strict';
-
-return $.summernote.options;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js b/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js
deleted file mode 100644
index de776020a42..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/abstract.js
+++ /dev/null
@@ -1,222 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.abstract', function (require) {
-'use strict';
-
-var Class = require('web.Class');
-var mixins = require('web.mixins');
-var ServicesMixin = require('web.ServicesMixin');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-//--------------------------------------------------------------------------
-// AbstractPlugin for summernote module API
-//--------------------------------------------------------------------------
-
-var AbstractPlugin = Class.extend(mixins.EventDispatcherMixin, ServicesMixin).extend({
- /**
- * Use this prop if you want to extend a summernote plugin.
- */
- init: function (context) {
- var self = this;
- this._super.apply(this, arguments);
- this.setParent(context.options.parent);
- this.context = context;
-
- if (!this.context.invoke) {
- // for use outside of wysiwyg/summernote
- this.context.invoke = function () {};
- }
-
- this.$editable = context.layoutInfo.editable;
- this.editable = this.$editable[0];
- this.document = this.editable.ownerDocument;
- this.window = this.document.defaultView;
- this.summernote = this.window._summernoteSlave || $.summernote; // if the target is in iframe
- this.ui = this.summernote.ui;
- this.$editingArea = context.layoutInfo.editingArea;
- this.options = _.defaults(context.options || {}, wysiwygOptions);
- this.lang = wysiwygTranslation;
- this._addButtons();
- if (this.events) {
- this.events = _.clone(this.events);
- _.each(_.keys(this.events), function (key) {
- var value = self.events[key];
- if (typeof value === 'string') {
- value = self[value].bind(self);
- }
- if (key.indexOf('summernote.') === 0) {
- self.events[key] = value;
- } else {
- delete self.events[key];
- key = key.split(' ');
- if (key.length > 1) {
- self.context.layoutInfo.editor.on(key[0], key.slice(1).join(' '), value);
- } else {
- self.context.layoutInfo.editor.on(key, value);
- }
- }
- });
- }
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- shouldInitialize: function () {
- return true;
- },
- initialize: function () {},
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Override to add buttons.
- */
- _addButtons: function () {},
- /**
- * Creates a dropdown button with its contents and behavior.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
- * @param {function} onclick
- */
- _createDropdownButton: function (optionName, buttonIcon, buttonTooltip, values, onclick) {
- var self = this;
-
- if (!onclick) {
- onclick = function (e) {
- var classNames = _.map(values, function (item) {
- return item.value;
- }).join(' ');
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.removeClass(classNames);
- if ($(e.target).data('value')) {
- $target.addClass($(e.target).data('value'));
- }
- };
- }
- if (optionName) {
- this.context.memo('button.' + optionName, function () {
- return self._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
- });
- } else {
- return this._renderDropdownButton(buttonIcon, buttonTooltip, values, onclick);
- }
- },
- /**
- * Creates a button to toggle a class.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {str} className
- */
- _createToggleButton: function (optionName, buttonIcon, buttonTooltip, className) {
- var self = this;
- return this._createButton(optionName, buttonIcon, buttonTooltip, function () {
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.toggleClass(className);
- });
- },
- /**
- * Creates a button.
- *
- * @param {str} optionName
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {function} onclick
- */
- _createButton: function (optionName, buttonIcon, buttonTooltip, onclick) {
- var self = this;
- if (optionName) {
- this.context.memo('button.' + optionName, function () {
- return self._renderButton(buttonIcon, buttonTooltip, onclick);
- });
- } else {
- return this._renderButton(buttonIcon, buttonTooltip, onclick);
- }
- },
- /**
- * Helper function to _createButton: renders the button.
- *
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {function} onclick
- * @returns {JQuery}
- */
- _renderButton: function (buttonIcon, buttonTooltip, onclick) {
- var self = this;
- return this.context.invoke('buttons.button', {
- contents: buttonIcon.indexOf('<') === -1 ? this.ui.icon(buttonIcon) : buttonIcon,
- tooltip: buttonTooltip,
- click: function (e) {
- e.preventDefault();
- self.context.invoke('editor.beforeCommand');
- onclick(e);
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- },
- }).render();
- },
- /**
- * Helper function to _createDropdownButton: renders the dropdown button.
- *
- * @param {str} buttonIcon (ex.: 'note-icon-align')
- * @param {str} buttonTooltip (ex.: 'Align Paragraph')
- * @param {Object[]} values (ex.: [{value: 'padding-small', string: 'S'}])
- * @param {function} onclick
- * @param {JQuery}
- */
- _renderDropdownButton: function (buttonIcon, buttonTooltip, values, onclick) {
- return this.ui.buttonGroup([
- this.context.invoke('buttons.button', {
- className: 'dropdown-toggle',
- contents: buttonIcon.indexOf('<') === -1 ?
- this.ui.dropdownButtonContents(this.ui.icon(buttonIcon), this.options) : buttonIcon,
- tooltip: buttonTooltip,
- data: {
- toggle: 'dropdown'
- }
- }),
- this.ui.dropdown({
- items: values,
- template: function (item) {
- return item.string;
- },
- click: this._wrapCommand(function (e) {
- e.preventDefault();
- onclick(e);
- }),
- })
- ]).render();
- },
- /**
- * Wraps a given function between common actions required
- * for history (undo/redo) and the maintenance of the DOM/range.
- *
- * @param {function} fn
- * @returns {any} the return of fn
- */
- _wrapCommand: function (fn) {
- var self = this;
- return function () {
- self.context.invoke('editor.restoreRange');
- self.context.invoke('editor.beforeCommand');
- var res = fn.apply(self, arguments);
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- return res;
- };
- },
-
-});
-
-return AbstractPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js b/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
deleted file mode 100644
index 1d5c2d47a33..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/bullet.js
+++ /dev/null
@@ -1,571 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.bullet', function (require) {
-'use strict';
-
-var core = require('web.core');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-
-wysiwygOptions.icons.checklist = 'fa fa-check-square';
-wysiwygOptions.keyMap.pc['CTRL+SHIFT+NUM9'] = 'insertCheckList';
-wysiwygOptions.keyMap.mac['CMD+SHIFT+NUM9'] = 'insertCheckList';
-wysiwygTranslation.lists.checklist = _t('Checklist');
-wysiwygTranslation.help.checklist = _t('Toggle checkbox list');
-
-
-var BulletPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.mousedown': '_onMouseDown',
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Insert an ordered list, an unordered list or a checklist.
- * If already in list, remove the list or convert it to the given type.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {false|Node[]} contents of the ul/ol or content of the converted/removed list
- */
- insertList: function (type) {
- var range = this.context.invoke('editor.createRange');
- if (!range) {
- return;
- }
- var res;
- var start = range.getStartPoint();
- var end = range.getEndPoint();
-
- var isInList = dom.ancestor(range.sc, dom.isList);
- if (isInList) {
- res = this._convertList(false, [], start, end, type);
- } else {
- var ul = this._createList(type);
- res = ul ? [].slice.call(ul.children) : [];
- }
-
- var startLeaf = this.context.invoke('HelperPlugin.firstLeaf', start.node);
- var endLeaf = this.context.invoke('HelperPlugin.firstLeaf', end.node);
- range = this.context.invoke('editor.setRange', startLeaf, start.offset, endLeaf, end.offset);
- range.select();
- this.context.invoke('editor.saveRange');
-
- return res;
- },
- /**
- * Indent a node (list or format node).
- *
- * @returns {false|Node[]} contents of list/indented item
- */
- indent: function () {
- return this._indent();
- },
- /**
- * Outdent a node (list or format node).
- *
- * @returns {false|Node[]} contents of list/outdented item
- */
- outdent: function () {
- return this._indent(true);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add checklist buttons.
- */
- _addButtons: function () {
- var self = this;
- this._super();
- this.context.memo('help.checklist', this.lang.help.checklist);
- this._createButton('checklist', this.options.icons.checklist, this.lang.lists.checklist, function (e) {
- e.preventDefault();
- self.context.invoke('editor.insertCheckList');
- });
- },
- /**
- * Convert ul<->ol or remove ul/ol.
- *
- * @param {boolean} isWithinElem true if selection already inside the LI
- * @param {DOM[]} nodes selected nodes
- * @param {Object} startPoint
- * @param {Object} endPoint
- * @param {boolean} sorted
- * @returns {boolean} isWithinElem
- */
- _convertList: function (isWithinElem, nodes, startPoint, endPoint, sorted) {
- var ol = dom.ancestor(startPoint.node, dom.isList);
- var parent = ol.parentNode;
-
- // get selected lis
-
- var lis = [];
- var lisBefore = [];
- var lisAfter = [];
- _.each(ol.children, function (li) {
- if (!isWithinElem && (li === startPoint.node || $.contains(li, startPoint.node))) {
- isWithinElem = true;
- }
- if (isWithinElem) {
- lis.push(li);
- } else if (lis.length) {
- lisAfter.push(li);
- } else {
- lisBefore.push(li);
- }
- if (isWithinElem && (li === endPoint.node || $.contains(li, endPoint.node))) {
- isWithinElem = false;
- }
- });
-
- var res = lis;
-
- if (lisBefore.length) {
- var ulBefore = this.document.createElement(ol.tagName);
- ulBefore.className = ol.className;
-
- if (dom.isLi(ol.parentNode)) {
- var li = this.document.createElement('li');
- li.className = ol.parentNode.className;
- $(li).insertBefore(ol.parentNode);
- li.appendChild(ulBefore);
- } else {
- $(ulBefore).insertBefore(ol);
- }
-
- $(ulBefore).append(lisBefore);
- }
- if (lisAfter.length) {
- var ulAfter = this.document.createElement(ol.tagName);
- ulAfter.className = ol.className;
-
- if (dom.isLi(ol.parentNode)) {
- var li = this.document.createElement('li');
- li.className = ol.parentNode.className;
- $(li).insertAfter(ol.parentNode);
- li.appendChild(ulAfter);
- } else {
- $(ulAfter).insertAfter(ol);
- }
-
- $(ulAfter).append(lisAfter);
- }
-
- // convert ul<->ol or remove list
- var current = ol.tagName === 'UL' && ol.className.indexOf('o_checklist') !== -1 ? 'checklist' : ol.tagName.toLowerCase();
- if (current !== sorted) {
- // convert ul <-> ol
-
- var ul;
- $(ol).removeClass('o_checklist');
- if (sorted === 'checklist' && current === "ul") {
- ul = ol;
- } else if (sorted === 'ul' && current === 'checklist') {
- ul = ol;
- } else {
- ul = this.document.createElement(sorted === "ol" ? "ol" : "ul");
- ul.className = ol.className;
- $(ul).removeClass('o_checklist');
- $(ul).insertBefore(ol).append(lis);
- parent.removeChild(ol);
- }
- if (sorted === 'checklist') {
- $(ul).addClass('o_checklist');
- }
-
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
-
- } else {
- // remove ol/ul
-
- if (dom.isLi(parent) || dom.isList(parent)) {
- if (dom.isLi(parent)) {
- ol = parent;
- parent = ol.parentNode;
- }
- $(lis).insertBefore(ol);
- } else {
- res = [];
- _.each(lis, function (li) {
- res.push.apply(res, li.childNodes);
- $(li.childNodes).insertBefore(ol);
- });
-
- // wrap in p
-
- var hasNode = _.find(res, function (node) {
- return node.tagName && node.tagName !== "BR" && !dom.isMedia(node);
- });
- if (!hasNode) {
- var p = this.document.createElement('p');
- $(p).insertBefore(ol).append(res);
- res = [p];
- }
- }
- parent.removeChild(ol);
-
- }
-
- nodes.push.apply(nodes, res);
-
- return isWithinElem;
- },
- /**
- * Create a list if allowed.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {false|Node} the list, if any
- */
- _createList: function (type) {
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- var formatNodes = this._filterEditableFormatNodes(nodes);
- if (!formatNodes.length) {
- return;
- }
-
- var ul = this._createListElement(type);
- $(formatNodes[0][0] || formatNodes[0]).before(ul);
- this._fillListElementWith(ul, formatNodes);
- this._deleteListElementEdges(ul);
-
- return ul;
- },
- /**
- * Create a list element of the given type and return it.
- *
- * @param {string('ol'|'ul'|'checklist')} type the type of list to insert
- * @returns {Node}
- */
- _createListElement: function (type) {
- var ul = this.document.createElement(type === "ol" ? "ol" : "ul");
- if (type === 'checklist') {
- ul.className = 'o_checklist';
- }
- return ul;
- },
- /**
- * Delete a list element's edges if necessary.
- *
- * @param {Node} ul
- */
- _deleteListElementEdges: function (ul) {
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'next');
- this.context.invoke('HelperPlugin.deleteEdge', ul, 'prev');
- this.editable.normalize();
- },
- /**
- * Fill a list element with the nodes passed, wrapped in LIs.
- *
- * @param {Node} ul
- * @param {Node[]} nodes
- */
- _fillListElementWith: function (ul, nodes) {
- var self = this;
- _.each(nodes, function (node) {
- var li = self.document.createElement('li');
- $(li).append(node);
- ul.appendChild(li);
- });
- },
- /**
- * Filter the editable format ancestors of the given nodes
- * and fill or wrap them if needed for range selection.
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- _filterEditableFormatNodes: function (nodes) {
- var self = this;
- var formatNodes = this.context.invoke('HelperPlugin.filterFormatAncestors', nodes);
- formatNodes = _.compact(_.map(formatNodes, function (node) {
- var ancestor = (!node.tagName || node.tagName === 'BR') && dom.ancestor(node, dom.isCell);
- if (ancestor && self.options.isEditableNode(ancestor)) {
- if (!ancestor.childNodes.length) {
- var br = self.document.createElement('br');
- ancestor.appendChild(br);
- }
- var p = self.document.createElement('p');
- $(p).append(ancestor.childNodes);
- ancestor.appendChild(p);
- return p;
- }
- return self.options.isEditableNode(node) && node || null;
- }));
- return formatNodes;
- },
- /**
- * Indent or outdent a format node.
- *
- * @param {bool} outdent true to outdent, false to indent
- * @returns {false|[]Node} indented nodes
- */
- _indent: function (outdent) {
- var range = this.context.invoke('editor.createRange');
- if (!range) {
- return;
- }
- // list groups shouldn't be outdented like regular lists
- if ($(dom.ancestor(range.sc, dom.isList)).hasClass('list-group')) {
- return;
- }
-
- var self = this;
- var nodes = [];
- var isWithinElem;
- var ancestor = range.commonAncestor() || this.editable;
- var $dom = $(ancestor);
-
- if (!dom.isList(ancestor)) {
- // to indent a selection, we indent the child nodes of the common
- // ancestor that contains this selection
- $dom = $(ancestor.tagName ? ancestor : ancestor.parentNode).children();
- }
-
- // if selection is inside indented contents and outdent is true, we can outdent this node
- var indentedContent = outdent && dom.ancestor(ancestor, function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- return node.tagName && !!parseFloat(node.style[style] || 0);
- });
-
- if (indentedContent) {
- $dom = $(indentedContent);
- } else {
- // if selection is inside a list, we indent its list items
- $dom = $(dom.ancestor(ancestor, dom.isList));
- if (!$dom.length) {
- // if the selection is contained in a single HTML node, we indent
- // the first ancestor 'content block' (P, H1, PRE, ...) or TD
- var nodes = this.context.invoke('HelperPlugin.getSelectedNodes');
- $dom = $(this._filterEditableFormatNodes(nodes));
- }
- }
-
- // if select tr, take the first td
- $dom = $dom.map(function () {
- return this.tagName === "TR" ? this.firstElementChild : this;
- });
-
- $dom.each(function () {
- if (isWithinElem || $.contains(this, range.sc)) {
- if (dom.isList(this)) {
- if (outdent) {
- var type = this.tagName === 'OL' ? 'ol' : (this.className && this.className.indexOf('o_checklist') !== -1 ? 'checklist' : 'ul');
- isWithinElem = self._convertList(isWithinElem, nodes, range.getStartPoint(), range.getEndPoint(), type);
- } else {
- isWithinElem = self._indentUL(isWithinElem, nodes, this, range.sc, range.ec);
- }
- } else if (self.context.invoke('HelperPlugin.isFormatNode', this) || dom.ancestor(this, dom.isCell)) {
- isWithinElem = self._indentFormatNode(outdent, isWithinElem, nodes, this, range.sc, range.ec);
- }
- }
- });
-
- if ($dom.parent().length) {
- var $parent = $dom.parent();
-
- // remove text nodes between lists
- var $ul = $parent.find('ul, ol');
- if (!$ul.length) {
- $ul = $(dom.ancestor(range.sc, dom.isList));
- }
- $ul.each(function () {
- var notWhitespace = /\S/;
- if (
- this.previousSibling &&
- this.previousSibling !== this.previousElementSibling &&
- !this.previousSibling.textContent.match(notWhitespace)
- ) {
- this.parentNode.removeChild(this.previousSibling);
- }
- if (
- this.nextSibling &&
- this.nextSibling !== this.nextElementSibling &&
- !this.nextSibling.textContent.match(notWhitespace)
- ) {
- this.parentNode.removeChild(this.nextSibling);
- }
- });
-
- // merge same ul or ol
- $ul.prev('ul, ol').each(function () {
- self.context.invoke('HelperPlugin.deleteEdge', this, 'next');
- });
-
- }
-
- range.normalize().select();
- this.context.invoke('editor.saveRange');
-
- return !!nodes.length && nodes;
- },
- /**
- * Indent several LIs in a list.
- *
- * @param {bool} isWithinElem true if selection already inside the LI
- * @param {Node[]} nodes
- * @param {Node} UL
- * @param {Node} start
- * @param {Node} end
- * @returns {bool} isWithinElem
- */
- _indentUL: function (isWithinElem, nodes, UL, start, end) {
- var next;
- var tagName = UL.tagName;
- var node = UL.firstChild;
- var ul = document.createElement(tagName);
- ul.className = UL.className;
- var flag;
-
- if (isWithinElem) {
- flag = true;
- }
-
- // create and fill ul into a li
- while (node) {
- if (flag || node === start || $.contains(node, start)) {
- isWithinElem = true;
- node.parentNode.insertBefore(ul, node);
- }
- next = node.nextElementSibling;
- if (isWithinElem) {
- ul.appendChild(node);
- nodes.push(node);
- }
- if (node === end || $.contains(node, end)) {
- isWithinElem = false;
- break;
- }
- node = next;
- }
-
- var temp;
- var prev = ul.previousElementSibling;
- if (
- prev && prev.tagName === "LI" &&
- (temp = prev.firstElementChild) && temp.tagName === tagName &&
- ((prev.firstElementChild || prev.firstChild) !== ul)
- ) {
- $(prev.firstElementChild || prev.firstChild).append($(ul).contents());
- $(ul).remove();
- ul = prev;
- ul.parentNode.removeChild(ul.nextElementSibling);
- }
- next = ul.nextElementSibling;
- if (
- next && next.tagName === "LI" &&
- (temp = next.firstElementChild) && temp.tagName === tagName &&
- (ul.firstElementChild !== next.firstElementChild)
- ) {
- $(ul.firstElementChild).append($(next.firstElementChild).contents());
- $(next.firstElementChild).remove();
- ul.parentNode.removeChild(ul.nextElementSibling);
- }
-
- // wrap in li
- var li = this.document.createElement('li');
- li.className = 'o_indent';
- $(ul).before(li);
- li.appendChild(ul);
-
- return isWithinElem;
- },
- /**
- * Outdent a container node.
- *
- * @param {Node} node
- * @returns {float} margin
- */
- _outdentContainer: function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- var margin = parseFloat(node.style[style] || 0) - 1.5;
- node.style[style] = margin > 0 ? margin + "em" : "";
- return margin;
- },
- /**
- * Indent a container node.
- *
- * @param {Node} node
- * @returns {float} margin
- */
- _indentContainer: function (node) {
- var style = dom.isCell(node) ? 'paddingLeft' : 'marginLeft';
- var margin = parseFloat(node.style[style] || 0) + 1.5;
- node.style[style] = margin + "em";
- return margin;
- },
- /**
- * Indent/outdent a format node.
- *
- * @param {bool} outdent true to outdent, false to indent
- * @param {bool} isWithinElem true if selection already inside the element
- * @param {DOM[]} nodes
- * @param {DOM} p
- * @param {DOM} start
- * @param {DOM} end
- * @returns {bool} isWithinElem
- */
- _indentFormatNode: function (outdent, isWithinElem, nodes, p, start, end) {
- if (p === start || $.contains(p, start) || $.contains(start, p)) {
- isWithinElem = true;
- }
- if (isWithinElem) {
- nodes.push(p);
- if (outdent) {
- this._outdentContainer(p);
- } else {
- this._indentContainer(p);
- }
- }
- if (p === end || $.contains(p, end) || $.contains(end, p)) {
- isWithinElem = false;
- }
- return isWithinElem;
- },
-
- //--------------------------------------------------------------------------
- // Handle
- //--------------------------------------------------------------------------
-
- /**
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onMouseDown: function (se, e) {
- if (!dom.isLi(e.target) || !$(e.target).parent('ul.o_checklist').length || e.offsetX > 0) {
- return;
- }
- e.preventDefault();
- var checked = $(e.target).hasClass('o_checked');
- $(e.target).toggleClass('o_checked', !checked);
- var $sublevel = $(e.target).next('ul.o_checklist, li:has(> ul.o_checklist)').find('> li, ul.o_checklist > li');
- var $parents = $(e.target).parents('ul.o_checklist').map(function () {
- return this.parentNode.tagName === 'LI' ? this.parentNode : this;
- });
- if (checked) {
- $sublevel.removeClass('o_checked');
- $parents.prev('ul.o_checklist li').removeClass('o_checked');
- } else {
- $sublevel.addClass('o_checked');
- var $lis;
- do {
- $lis = $parents.not(':has(li:not(.o_checked))').prev('ul.o_checklist li:not(.o_checked)');
- $lis.addClass('o_checked');
- } while ($lis.length);
- }
- },
-});
-
-registry.add('BulletPlugin', BulletPlugin);
-
-return BulletPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js b/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js
deleted file mode 100644
index 7059c448d81..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/buttons.js
+++ /dev/null
@@ -1,88 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.buttons', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-
-var dom = $.summernote.dom;
-
-Plugins.buttons.include({
- /**
- * Fix tooltip for 'option' buttons (summernote 0.8.9 bug)
- * Remove this once the library is updated to 0.8.10.
- *
- * @override
- */
- addToolbarButtons: function () {
- this.options.codeview = this.lang.options.codeview;
- this.options.help = this.lang.options.help;
- this.options.fullscreen = this.lang.options.fullscreen;
- this._super();
- },
- /**
- * Show current style (of start of selection) in magic wand dropdown.
- *
- * @param {JQuery Object} $container
- */
- updateActiveStyleDropdown: function ($container) {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- var el = dom.ancestor(range.sc, function (n) {
- return n.tagName && self.options.styleTags.indexOf(n.tagName.toLowerCase()) !== -1;
- });
- if (el) {
- var tagName = el.tagName.toLowerCase();
- $container.find('.dropdown-style a').each(function (idx, item) {
- var $item = $(item);
- // always compare string to avoid creating another func.
- var isChecked = ($item.data('value') + '') === (tagName + '');
- $item.toggleClass('active', isChecked);
- });
- } else {
- var $item = $container.find('.dropdown-style a.active');
- $item.removeClass('active');
- }
- },
- /**
- * @override
- */
- updateCurrentStyle: function ($container) {
- this._super.apply(this, arguments);
-
- this.updateParaAlignIcon($container || this.$toolbar);
- this.updateActiveStyleDropdown($container || this.$toolbar);
- },
- /**
- * Show current paragraph alignment (of start of selection) on paragraph alignment dropdown.
- *
- * @param {JQuery Object} $container
- */
- updateParaAlignIcon: function ($container) {
- var range = this.context.invoke('editor.createRange');
- var $paraIcon = $container.find('.note-para .dropdown-toggle i');
- var el = dom.isText(range.sc) ? range.sc.parentNode : range.sc;
- if (el) {
- $paraIcon.removeClass();
- switch ($(el).css('text-align')) {
- case 'left':
- $paraIcon.addClass('note-icon-align-left');
- break;
- case 'center':
- $paraIcon.addClass('note-icon-align-center');
- break;
- case 'right':
- $paraIcon.addClass('note-icon-align-right');
- break;
- case 'justify':
- $paraIcon.addClass('note-icon-align-justify');
- break;
- default:
- $paraIcon.addClass('note-icon-align-left');
- break;
- }
- }
- },
-});
-
-return Plugins.buttons;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js b/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js
deleted file mode 100644
index 01ffc52b205..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/codeview.js
+++ /dev/null
@@ -1,60 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.codeview', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var _t = core._t;
-
-
-var CodeviewPlugin = Plugins.codeview.extend({
- /**
- * @override
- */
- activate: function () {
- this._super();
- if (this.$codable.height() === 0) {
- this.$codable.height(180);
- }
- this.context.invoke('editor.hidePopover');
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @override
- */
- deactivate: function () {
- if (
- this.context.invoke('HelperPlugin.hasJinja', this.context.invoke('code')) &&
- !this.isBeingDestroyed
- ) {
- var message = _t("Your code contains JINJA conditions.\nRemove them to return to WYSIWYG HTML edition.");
- this.do_warn(_t("Cannot edit HTML"), message);
- this.$codable.focus();
- return;
- }
- this._super();
- this.$editable.css('height', '');
- },
- /**
- * @override
- */
- destroy: function () {
- this.isBeingDestroyed = true;
- this._super();
- },
- /**
- * Force activation of the code view.
- */
- forceActivate: function () {
- if (!this.isActivated()) {
- this.activate();
- }
- },
-});
-
-registry.add('codeview', CodeviewPlugin);
-
-return CodeviewPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js b/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
deleted file mode 100644
index a88fc50fc7f..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/dropzone.js
+++ /dev/null
@@ -1,229 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.dropzone', function (require) {
-'use strict';
-
-var core = require('web.core');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var utils = require('web.utils');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-
-var DropzonePlugin = Plugins.dropzone.extend({
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * Disable Summernote's handling of drop events.
- */
- attachDragAndDropEvent: function () {
- this._super.apply(this, arguments);
- this.$dropzone.off('drop');
- this.$dropzone.on('dragenter', this._onDragenter.bind(this));
- this.$dropzone.on('drop', this._onDrop.bind(this));
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Clean up then drops HTML or plain text into the editor.
- *
- * @private
- * @param {String} html
- * @param {Boolean} textOnly true to allow only dropping plain text
- */
- _dropHTML: function (html, textOnly) {
- this.context.invoke('editor.beforeCommand');
-
- // Clean up
- var nodes = this.context.invoke('TextPlugin.prepareClipboardData', html);
-
- // Delete selection
- this.context.invoke('HelperPlugin.deleteSelection');
-
- // Insert the nodes
- this.context.invoke('TextPlugin.pasteNodes', nodes, textOnly);
- this.context.invoke('HelperPlugin.normalize');
- this.context.invoke('editor.saveRange');
-
- this.context.invoke('editor.afterCommand');
- },
- /**
- * Drop images into the editor: save them as attachments.
- *
- * @private
- * @param {File[]]} files (images only)
- */
- _dropImages: function (files) {
- var self = this;
- this.context.invoke('editor.beforeCommand');
- var range = this.context.invoke('editor.createRange');
-
- var spinners = [];
- var images = [];
- var defs = [];
- _.each(files, function (file) {
- // Add spinner
- var spinner = $('').attr('data-filename', file.name)[0];
- self.context.invoke('editor.hidePopover');
- if (range.sc.tagName) {
- if (range.so >= dom.nodeLength(range.sc)) {
- $(range.sc).append(spinner);
- } else {
- $(range.sc.childNodes[range.so]).before(spinner);
- }
- } else {
- range.sc.splitText(range.so);
- $(range.sc).after(spinner);
- }
- spinners.push(spinner);
-
- // save images as attachments
- var def = new Promise(function (resolve) {
- // Get image's Base64 string
- utils.getDataURLFromFile(file).then(function (result) {
- self._uploadImage(result, file.name).then(function (attachment) {
- // Make the HTML
- var image = self.document.createElement('img');
- image.setAttribute('style', 'width: 100%;');
- image.src = '/web/content/' + attachment.id + '/' + attachment.name;
- image.alt = attachment.name;
- $(spinner).replaceWith(image);
- images.push(image);
- resolve(image);
- $(image).trigger('dropped');
- });
- });
- });
- defs.push(def);
- });
-
- this.trigger_up('drop_images', {
- spinners: spinners,
- promises: defs,
- });
-
- Promise.all(defs).then(function () {
- var defs = [];
- $(images).each(function () {
- if (!this.height) {
- var def = new Promise(function (resolve) {
- $(this).one('load error abort', resolve);
- });
- defs.push(def);
- }
- });
- Promise.all(defs).then(function () {
- if (images.length === 1) {
- range = self.context.invoke('editor.setRange', _.last(images), 0);
- range.select();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- self.context.invoke('MediaPlugin.updatePopoverAfterEdit', images[0]);
- } else {
- self.context.invoke('editor.afterCommand');
- }
- });
- });
- },
- /**
- * Upload an image from its Base64 representation.
- *
- * @private
- * @param {String} imageBase64
- * @param {String} fileName
- * @returns {Promise}
- */
- _uploadImage: function (imageBase64, fileName) {
- var options = {};
- this.trigger_up('getRecordInfo', {
- recordInfo: options,
- type: 'media',
- callback: function (recordInfo) {
- _.defaults(options, recordInfo);
- },
- });
-
- return this._rpc({
- route: '/web_editor/attachment/add_data',
- params: {
- 'data': imageBase64.split(';base64,').pop(),
- 'name': fileName,
- 'res_id': options.res_id || 0,
- 'res_model': options.res_model,
- },
- });
- },
- /**
- * @private
- * @param {JQueryEvent} e
- */
- _onDragenter: function (e) {
- var range = this.context.invoke('editor.createRange');
- if (!this.editable.contains(range.sc)) {
- var node = this.editable;
- if (node && node.firstChild) {
- node = node.firstChild;
- }
- range = this.context.invoke('editor.setRange', node, 0, node, 0);
- range.select();
- }
- },
- /**
- * @private
- * @param {JQueryEvent} e
- */
- _onDrop: function (e) {
- e.preventDefault();
-
- this.$editor.removeClass('dragover');
-
- if (this.options.disableDragAndDrop) {
- return;
- }
- var dataTransfer = e.originalEvent.dataTransfer;
-
- if (!this._canDropHere()) {
- this.context.invoke('HelperPlugin.notify', _t("Not a dropzone"), _t("Dropping is prohibited in this area."));
- return;
- }
-
- if (dataTransfer.getData('text/html')) {
- this._dropHTML(dataTransfer.getData('text/html'));
- return;
- }
- if (dataTransfer.files.length) {
- var images = [];
- _.each(dataTransfer.files, function (file) {
- if (file.type.indexOf('image') !== -1) {
- images.push(file);
- }
- });
- if (!images.length || images.length < dataTransfer.files.length) {
- this.context.invoke('HelperPlugin.notify', _t("Unsupported file type"), _t("Images are the only file types that can be dropped."));
- }
- if (images.length) {
- this._dropImages(images);
- }
- }
- },
- /**
- * Return true if dropping is allowed at the current range.
- *
- * @private
- * @returns {Boolean}
- */
- _canDropHere: function () {
- var range = this.context.invoke('editor.createRange');
- return this.options.isEditableNode(range.sc);
- },
-});
-
-registry.add('dropzone', DropzonePlugin);
-
-return DropzonePlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js b/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js
deleted file mode 100644
index 350a9158188..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/editor.js
+++ /dev/null
@@ -1,189 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.editor', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var TablePlugin = require('web_editor.wysiwyg.plugin.table');
-
-
-var NewSummernoteEditor = Plugins.editor.extend({
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- initialize: function () {
- var self = this;
- this.history = this.context.modules.HistoryPlugin;
- this.dropzone = this.context.modules.DropzonePlugin;
-
- this.insertTable = this.wrapCommand(this._insertTable.bind(this));
- this.insertOrderedList = this.wrapCommand(this._insertOrderedList.bind(this));
- this.insertUnorderedList = this.wrapCommand(this._insertUnorderedList.bind(this));
- this.insertCheckList = this.wrapCommand(this._insertCheckList.bind(this));
- this.indent = this.wrapCommand(this._indent.bind(this));
- this.outdent = this.wrapCommand(this._outdent.bind(this));
- this.table = new TablePlugin(this.context);
- this.hasFocus = this._hasFocus.bind(this);
-
- this._makeTextCommand('formatBlock');
- this._makeTextCommand('removeFormat');
- _.each('bold,italic,underline,strikethrough,superscript,subscript'.split(','), function (sCmd) {
- self._makeTextCommand('formatText', sCmd);
- });
- _.each('justifyLeft,justifyCenter,justifyRight,justifyFull'.split(','), function (sCmd) {
- self._makeTextCommand('formatBlockStyle', sCmd);
- });
-
- this._super();
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide all popovers.
- */
- hidePopover: function () {
- this.context.invoke('MediaPlugin.hidePopovers');
- this.context.invoke('LinkPopover.hide');
- },
- /*
- * Focus the editor.
- */
- focus: function () {
- // [workaround] Screen will move when page is scolled in IE.
- // - do focus when not focused
- if (!this.hasFocus()) {
- var range = $.summernote.range.create();
- if (range) {
- $(range.sc).closest('[contenteditable]').focus();
- range.select();
- } else {
- this.$editable.focus();
- }
- }
- },
- /**
- * Fix double-undo (CTRL-Z) issue with Odoo integration.
- *
- * @override
- */
- undo: function () {
- this.createRange();
- this._super();
- },
- /**
- * Set the range at the given nodes and offsets.
- * If no `ec` is specified, the range is collapsed on start.
- * Note: Does NOT select the range.
- *
- * @param {Node} sc
- * @param {Number} so
- * @param {Node} [ec]
- * @param {Number} [eo]
- * @returns {Object} range
- */
- setRange: function (sc, so, ec, eo) {
- var range = this.createRange();
- range.sc = sc;
- range.so = so;
- range.ec = ec || sc;
- range.eo = eo || so;
- return range;
- },
- /**
- * Remove a link and preserve its text contents.
- */
- unlink: function () {
- this.beforeCommand();
- this.context.invoke('LinkPlugin.unlink');
- this.afterCommand();
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Returns true if editable area has focus.
- *
- * @private
- * @returns {Boolean}
- */
- _hasFocus: function () {
- return this.$editable.is(':focus') || !!this.$editable.find('[contenteditable]:focus').length;
- },
- /**
- * Indent a list or a format node.
- *
- * @private
- * @returns {false|Node[]} contents of list/indented item
- */
- _indent: function () {
- return this.context.invoke('BulletPlugin.indent');
- },
- /**
- * Insert a checklist.
- *
- * @private
- */
- _insertCheckList: function () {
- this.context.invoke('BulletPlugin.insertList', 'checklist');
- },
- /**
- * Insert an ordered list.
- *
- * @private
- */
- _insertOrderedList: function () {
- this.context.invoke('BulletPlugin.insertList', 'ol');
- },
- /**
- * Insert table (respecting unbreakable node rules).
- *
- * @private
- * @param {string} dim (eg: 3x3)
- */
- _insertTable: function (dim) {
- this.context.invoke('TablePlugin.insertTable', dim);
- },
- /**
- * Insert an ordered list.
- *
- * @private
- */
- _insertUnorderedList: function () {
- this.context.invoke('BulletPlugin.insertList', 'ul');
- },
- /**
- * Adds a TextPlugin command to the editor object.
- * The command will exist in the editor object under the name
- * arg if specified, or commandName otherwise.
- *
- * @param {String} commandName
- * @param {String} arg
- */
- _makeTextCommand: function (commandName, arg) {
- var self = this;
- this[arg || commandName] = self.wrapCommand(function (value) {
- self.context.invoke('TextPlugin.' + commandName, arg || value, value);
- });
- },
- /**
- * Outdent a list or a format node.
- *
- * @private
- * @returns {false|Node[]} contents of list/outdented item
- */
- _outdent: function () {
- return this.context.invoke('BulletPlugin.outdent');
- },
-});
-
-// Override Summernote default editor
-registry.add('editor', NewSummernoteEditor);
-
-return NewSummernoteEditor;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/font.js b/addons/web_editor/static/src/js/wysiwyg/plugin/font.js
deleted file mode 100644
index 0956b031747..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/font.js
+++ /dev/null
@@ -1,567 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.font', function (require) {
-'use strict';
-
-var core = require('web.core');
-var ColorpickerDialog = require('web.ColorpickerDialog');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-var QWeb = core.qweb;
-var _t = core._t;
-
-var dom = $.summernote.dom;
-
-//--------------------------------------------------------------------------
-// Font (colorpicker & font-size)
-//--------------------------------------------------------------------------
-
-dom.isFont = function (node) {
- return node && node.tagName === "FONT" || dom.isIcon(node);
-};
-
-var FontPlugin = AbstractPlugin.extend({
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Creates both ColorPicker buttons (fore- and backcolors) containing their palettes.
- *
- * @returns {jQuery} the buttons' div.btn-group container
- */
- createColorPickerButtons: function () {
- var self = this;
- var $container = $('');
-
- // create and append the buttons
-
- this._createForeColorButton().appendTo($container);
- this._createBackColorButton().appendTo($container);
-
- // add event
-
- $container.on('mousedown', function () {
- self.context.invoke('editor.saveRange');
- });
- return $container;
- },
- /**
- * Takes the font size button created by summernote and override its
- * events so as to customize its behavior.
- *
- * @param {jQuery} $button the button, as created by summernote
- * @returns {jQuery} the overridden button
- */
- overrideFontSizeButton: function ($button) {
- var self = this;
- $button.click(function (e) {
- e.preventDefault();
- });
- $button.find('.dropdown-menu').off('click').on('mousedown', function (e) {
- e.preventDefault();
- self.context.invoke('editor.createRange');
- self.context.invoke('editor.beforeCommand');
- var $target = $(e.target);
- self.context.invoke('FontPlugin.changeFontSize', $target.closest('[data-value]').data('value'), $target);
- self.context.invoke('buttons.updateCurrentStyle');
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- });
- return $button;
- },
- /**
- * Creates a (fore- or back-) color palette.
- *
- * @param {string('foreColor'|'backColor')} eventName
- * @returns {jQuery}
- */
- createPalette: function (eventName) {
- var self = this;
- var colors = _.clone(this.options.colors);
- colors.splice(0, 1); // Ignore the summernote gray palette and use ours
- var $palette = $(QWeb.render('wysiwyg.plugin.font.colorPalette', {
- colors: colors,
- eventName: eventName,
- lang: this.lang,
- }));
- if (this.options.tooltip) {
- $palette.find('.note-color-btn').tooltip({
- container: this.options.container,
- trigger: 'hover',
- placement: 'bottom'
- });
- }
-
- // custom structure for the color picker and custom colorsin XML template
- var $clpicker = $(QWeb.render('web_editor.colorpicker'));
-
- var $buttons = $(_.map($clpicker.children(), function (group) {
- var $contents = $(group).contents();
- if (!$contents.length) {
- return '';
- }
- var $row = $("", {
- "class": "note-color-row mb8 clearfix",
- 'data-group': $(group).data('name'),
- }).append($contents);
- 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(""));
-
- $buttons.find('button').each(function () {
- var color = $(this).data('color');
- $(this).addClass('note-color-btn bg-' + color).attr('data-value', (eventName === 'backColor' ? 'bg-' : 'text-') + color);
- });
-
- $palette.find('.o_theme_color_placeholder').prepend($buttons.filter('[data-group="theme"]'));
- $palette.find('.o_transparent_color_placeholder').prepend($buttons.filter('[data-group="transparent_grayscale"]'));
- $palette.find('.o_common_color_placeholder').prepend($buttons.filter('[data-group="common"]'));
-
- $palette.off('click').on('mousedown', '.note-color-btn', function (e) {
- e.preventDefault();
- self.context.invoke('editor.createRange');
- self.context.invoke('editor.beforeCommand');
- var method = eventName === 'backColor' ? 'changeBgColor' : 'changeForeColor';
- var $target = $(e.target);
- self.context.invoke('FontPlugin.' + method, $target.closest('[data-value]').data('value'), $target);
- self.context.invoke('buttons.updateCurrentStyle');
- self.editable.normalize();
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.afterCommand');
- });
- $palette.on('mousedown', '.note-custom-color', this._onCustomColor.bind(this, eventName));
-
- return $palette;
- },
- /**
- * Change the selection's fore color.
- *
- * @param {string} color (hexadecimal or class name)
- */
- changeForeColor: function (color) {
- this._applyFont(color || 'text-undefined', null, null);
- },
- /**
- * Change the selection's background color.
- *
- * @param {string} color (hexadecimal or class name)
- */
- changeBgColor: function (color) {
- this._applyFont(null, color || 'bg-undefined', null);
- },
- /**
- * Change the selection's font size.
- *
- * @param {integer} fontsize
- */
- changeFontSize: function (fontsize) {
- this._applyFont(null, null, fontsize || 'inherit');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Applies the given styles (fore- or backcolor, font size)
- * to a given node.
- *
- * @private
- * @param {Node} node
- * @param {string} color (hexadecimal or class name)
- * @param {string} bgcolor (hexadecimal or class name)
- * @param {integer} size
- * @returns {Node} the node
- */
- _applyStylesToFontNode: function (node, color, bgcolor, size) {
- var className = node.className.split(this.context.invoke('HelperPlugin.getRegex', 'space'));
- var k;
- if (color) {
- for (k = 0; k < className.length; k++) {
- if (className[k].length && className[k].slice(0, 5) === "text-") {
- className.splice(k, 1);
- k--;
- }
- }
- if (color === 'text-undefined') {
- node.className = className.join(" ");
- node.style.color = "inherit";
- } else if (color.indexOf('text-') !== -1) {
- node.className = className.join(" ") + " " + color;
- node.style.color = "inherit";
- } else {
- node.className = className.join(" ");
- node.style.color = color;
- }
- }
- if (bgcolor) {
- for (k = 0; k < className.length; k++) {
- if (className[k].length && className[k].slice(0, 3) === "bg-") {
- className.splice(k, 1);
- k--;
- }
- }
-
- if (bgcolor === 'bg-undefined') {
- node.className = className.join(" ");
- node.style.backgroundColor = "inherit";
- } else if (bgcolor.indexOf('bg-') !== -1) {
- node.className = className.join(" ") + " " + bgcolor;
- node.style.backgroundColor = "inherit";
- } else {
- node.className = className.join(" ");
- node.style.backgroundColor = bgcolor;
- }
- }
- if (size) {
- node.style.fontSize = "inherit";
- if (!isNaN(size) && Math.abs(parseInt(this.window.getComputedStyle(node).fontSize, 10) - size) / size > 0.05) {
- node.style.fontSize = size + "px";
- }
- }
- return node;
- },
- /**
- * Applies the given styles (fore- or backcolor, font size) to the selection.
- * If no text is selected, apply to the current text node, if any.
- *
- * @private
- * @param {string} color (hexadecimal or class name)
- * @param {string} bgcolor (hexadecimal or class name)
- * @param {integer} fontsize
- */
- _applyFont: function (color, bgcolor, size) {
- var self = this;
- var r = this.context.invoke('editor.createRange');
- if (!r || !this.$editable.has(r.sc).length || !this.$editable.has(r.ec).length) {
- return;
- }
- var target;
- var font;
- if (r.isCollapsed()) {
- if (dom.isIcon(r.sc)) {
- target = dom.lastAncestor(r.sc, dom.isIcon);
- } else {
- target = this.context.invoke('editor.restoreTarget');
- if (target && dom.isIcon(target)) {
- r = this.context.invoke('editor.setRange', target, 0);
- } else if (dom.isText(r.sc)) {
- font = dom.create("font");
- font.appendChild(this.document.createTextNode('\u200B'));
-
- var fontParent = dom.ancestor(r.sc, function (n) {
- return n.tagName === 'FONT';
- });
- var right;
- if (fontParent) {
- right = this.context.invoke('HelperPlugin.splitTree', fontParent, r.getStartPoint());
- } else {
- right = r.sc.splitText(r.so);
- }
- $(right).before(font);
- font = this._applyStylesToFontNode(font, color, bgcolor, size);
- r = this.context.invoke('editor.setRange', font, 1);
- r.select();
- return;
- }
- }
- }
-
- var startPoint = r.getStartPoint();
- var endPoint = r.getEndPoint();
- if (startPoint.node.tagName && startPoint.node.childNodes[startPoint.offset]) {
- startPoint.node = startPoint.node.childNodes[startPoint.offset];
- startPoint.offset = 0;
- }
- if (endPoint.node.tagName && endPoint.node.childNodes[endPoint.offset]) {
- endPoint.node = endPoint.node.childNodes[endPoint.offset];
- endPoint.offset = 0;
- }
- // get first and last point
- var ancestor;
- var node;
- if (!r.isCollapsed()) {
- if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
- ancestor = dom.lastAncestor(endPoint.node, dom.isFont) || endPoint.node;
- dom.splitTree(ancestor, endPoint);
- }
- if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
- ancestor = dom.lastAncestor(startPoint.node, dom.isFont) || startPoint.node;
- node = dom.splitTree(ancestor, startPoint);
- if (endPoint.node === startPoint.node) {
- endPoint.node = node;
- endPoint.offset = dom.nodeLength(node);
- }
- startPoint.node = node;
- startPoint.offset = 0;
- }
- }
- // get list of nodes to change
- var nodes = [];
- dom.walkPoint(startPoint, endPoint, function (point) {
- var node = point.node;
- if (((dom.isText(node) && self.context.invoke('HelperPlugin.isVisibleText', node)) || dom.isIcon(node)) &&
- (node !== endPoint.node || endPoint.offset)) {
- nodes.push(point.node);
- }
- });
- nodes = _.unique(nodes);
- // if fontawesome
- if (r.isCollapsed()) {
- nodes.push(startPoint.node);
- }
-
- // apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
- var $font;
- var fonts = [];
- var style;
- var className;
- var i;
- if (color || bgcolor || size) {
- for (i = 0; i < nodes.length; i++) {
- node = nodes[i];
- font = dom.lastAncestor(node, dom.isFont);
- if (!font) {
- if (node.textContent.match(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace'))) {
- node.textContent = node.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startAndEndSpace', 'g'), '\u00A0');
- }
- font = dom.create("font");
- node.parentNode.insertBefore(font, node);
- font.appendChild(node);
- }
- fonts.push(font);
- this._applyStylesToFontNode(font, color, bgcolor, size);
- }
- }
- // remove empty values
- // we must remove the value in 2 steps (applay inherit then remove) because some
- // browser like chrome have some time an error for the rendering and/or keep inherit
- for (i = 0; i < fonts.length; i++) {
- font = fonts[i];
- if (font.style.color === "inherit") {
- font.style.color = "";
- }
- if (font.style.backgroundColor === "inherit") {
- font.style.backgroundColor = "";
- }
- if (font.style.fontSize === "inherit") {
- font.style.fontSize = "";
- }
- $font = $(font);
- if (font.style.color === '' && font.style.backgroundColor === '' && font.style.fontSize === '') {
- $font.removeAttr("style");
- }
- if (!font.className.length) {
- $font.removeAttr("class");
- }
- }
-
- // target the deepest node
- if (startPoint.node.tagName && !startPoint.offset) {
- startPoint.node = this.context.invoke('HelperPlugin.firstLeaf', startPoint.node.childNodes[startPoint.offset] || startPoint.node);
- startPoint.offset = 0;
- }
- if (endPoint.node.tagName && !endPoint.offset) {
- endPoint.node = this.context.invoke('HelperPlugin.firstLeaf', endPoint.node.childNodes[endPoint.offset] || endPoint.node);
- endPoint.offset = 0;
- }
-
- // select nodes to clean (to remove empty font and merge same nodes)
- nodes = [];
- dom.walkPoint(startPoint, endPoint, function (point) {
- nodes.push(point.node);
- });
- nodes = _.unique(nodes);
- // remove node without attributes (move content), and merge the same nodes
- for (i = 0; i < nodes.length; i++) {
- node = nodes[i];
- if (dom.isText(node) && !this.context.invoke('HelperPlugin.isVisibleText', node)) {
- continue;
- }
- font = dom.lastAncestor(node, dom.isFont);
- node = font || dom.ancestor(node, dom.isSpan);
- if (!node) {
- continue;
- }
- $font = $(node);
- className = this.context.invoke('HelperPlugin.orderClass', node);
- style = this.context.invoke('HelperPlugin.orderStyle', node);
- if (!className && !style) {
- $(node).before($(node).contents());
- if (endPoint.node === node) {
- endPoint = dom.prevPointUntil(endPoint, function (point) {
- return point.node !== node;
- });
- }
- $(node).remove();
-
- nodes.splice(i, 1);
- i--;
- continue;
- }
- var prev = font && font.previousSibling;
- while (prev && !font.tagName && !this.context.invoke('HelperPlugin.isVisibleText', prev)) {
- prev = prev.previousSibling;
- }
- if (prev &&
- font.tagName === prev.tagName &&
- className === this.context.invoke('HelperPlugin.orderClass', prev) && style === this.context.invoke('HelperPlugin.orderStyle', prev)) {
- $(prev).append($(font).contents());
- if (endPoint.node === font) {
- endPoint = dom.prevPointUntil(endPoint, function (point) {
- return point.node !== font;
- });
- }
- $(font).remove();
-
- nodes.splice(i, 1);
- i--;
- continue;
- }
- }
-
- // restore selection
- r = this.context.invoke('editor.setRange', startPoint.node, startPoint.offset, endPoint.node, endPoint.offset);
- r.normalize().select();
-
- if (target) {
- this.context.invoke('MediaPlugin.updatePopoverAfterEdit', target);
- }
- },
- /**
- * Creates the backcolor button containing its palette.
- *
- * @private
- * @returns {jQuery} the backcolor button
- */
- _createBackColorButton: function () {
- var $bgPalette = this.createPalette('backColor');
- $bgPalette.find("button:not(.note-color-btn)")
- .addClass("note-color-btn")
- .attr('data-event', 'backColor')
- .each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'bg-' + $el.data('color');
- $el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : className);
- });
-
- var $bgContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
- className: 'note-bg-color',
- icon: this.options.icons.bg,
- }));
- $bgContainer.find('.dropdown-menu').append($bgPalette);
- return $bgContainer;
- },
- /**
- * Creates the forecolor button containing its palette.
- *
- * @private
- * @returns {jQuery} the forecolor button
- */
- _createForeColorButton: function () {
- var $forePalette = this.createPalette('foreColor');
- $forePalette.find("button:not(.note-color-btn)")
- .addClass("note-color-btn")
- .attr('data-event', 'foreColor')
- .each(function () {
- var $el = $(this);
- var className = $el.hasClass('o_custom_color') ? $el.data('color') : 'text-' + $el.data('color');
- $el.attr('data-value', className).addClass($el.hasClass('o_custom_color') ? '' : 'bg-' + $el.data('color'));
- });
-
- var $foreContainer = $(QWeb.render('wysiwyg.plugin.font.paletteButton', {
- className: 'note-fore-color',
- icon: this.options.icons.fore,
- }));
- $foreContainer.find('.dropdown-menu').append($forePalette);
- return $foreContainer;
- },
- /**
- * Method called on custom color button click :
- * opens the color picker dialog and saves the chosen color on save.
- *
- * @private
- * @param {string} targetColor
- * @param {jQuery Event} ev
- */
- _onCustomColor: function (targetColor, ev) {
- ev.preventDefault();
- ev.stopPropagation();
-
- var self = this;
- var $button = $(ev.target).next('button');
- var target = this.context.invoke('editor.restoreTarget');
- var colorPickerDialog = new ColorpickerDialog(this, {});
-
- this.context.invoke('editor.saveRange');
- colorPickerDialog.on('colorpicker:saved', this, this._wrapCommand(function (ev) {
- $button = $button.clone().appendTo($button.parent());
- $button.show();
- $button.css('background-color', ev.data.cssColor);
- $button.attr('data-value', ev.data.cssColor);
- $button.data('value', ev.data.cssColor);
- $button.attr('title', ev.data.cssColor);
- self.context.invoke('editor.saveTarget', target);
- self.context.invoke('editor.restoreRange');
- $button.mousedown();
- }));
- colorPickerDialog.open();
- this.context.invoke('MediaPlugin.hidePopovers');
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- fore: 'fa fa-font',
- bg: 'fa fa-paint-brush',
-});
-_.extend(wysiwygTranslation.color, {
- customColor: _t('Custom color'),
- fore: _t('Color'),
- bg: _t('Background color'),
-});
-
-registry.add('FontPlugin', FontPlugin);
-
-registry.addXmlDependency('/web_editor/static/src/xml/wysiwyg_colorpicker.xml');
-registry.addJob(function (wysiwyg) {
- if ('web_editor.colorpicker' in QWeb.templates) {
- return;
- }
-
- if (wysiwyg.isDestroyed()) {
- throw new Error('The Wysiwyg are destroyed before this loading');
- }
-
- var options = {};
- wysiwyg.trigger_up('getRecordInfo', {
- recordInfo: options,
- callback: function (recordInfo) {
- _.defaults(options, recordInfo);
- },
- });
- return wysiwyg._rpc({
- model: 'ir.ui.view',
- method: 'read_template',
- args: ['web_editor.colorpicker'],
- kwargs: {
- context: options.context,
- },
- }).then(function (template) {
- if (!/^/.test(template)) {
- template = _.str.sprintf('%s', template);
- }
- QWeb.add_template(template);
- });
-});
-
-
-return FontPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js b/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js
deleted file mode 100644
index f30b16b8bfc..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/font_buttons.js
+++ /dev/null
@@ -1,31 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.font.buttons', function (require) {
-'use strict';
-
-var ButtonsPlugin = require('web_editor.wysiwyg.plugin.buttons');
-
-//--------------------------------------------------------------------------
-// override the ColorPicker button into the Toolbar and the font size button
-//--------------------------------------------------------------------------
-
-ButtonsPlugin.include({
- /**
- * @override
- */
- addToolbarButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('button.colorpicker', function () {
- return self.context.invoke('FontPlugin.createColorPickerButtons');
- });
-
- var fontsizeFunction = this.context.memo('button.fontsize');
- this.context.memo('button.fontsize', function () {
- return self.context.invoke('FontPlugin.overrideFontSizeButton', fontsizeFunction());
- });
- },
-});
-
-return ButtonsPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js b/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js
deleted file mode 100644
index 2a28cbb221d..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/help_dialog.js
+++ /dev/null
@@ -1,27 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.HelpDialog', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-/**
- * Allows to customize link content and style.
- */
-var HelpDialog = Plugins.helpDialog.extend({
- /**
- * Restore the hidden close button.
- */
- showHelpDialog: function () {
- var self = this;
- return this._super().then(function () {
- self.$dialog.find('button.close span').attr('aria-hidden', 'false');
- });
- },
-});
-
-registry.add('helpDialog', HelpDialog);
-
-return {
- HelpDialog: HelpDialog,
-};
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js b/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
deleted file mode 100644
index d65c91aa682..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/helper.js
+++ /dev/null
@@ -1,1948 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.helper', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var Dialog = require('web.Dialog');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var HelperPlugin = AbstractPlugin.extend({
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * This dictionary contains oft-used regular expressions,
- * for performance and readability purposes. It can be
- * accessed and extended by the getRegex() method.
- */
- regex: {
- char: {
- noflag: /\S|\u00A0|\u200B/,
- },
- emptyElemWithBR: {
- noflag: /^\s* \s*$/,
- },
- endInvisible: {
- noflag: /\u200B$/,
- },
- endNotChar: {
- noflag: /[^\S\u00A0\u200B]+$/,
- },
- endSingleSpace: {
- noflag: /[\S\u00A0\u200B]\s$/,
- },
- endSpace: {
- noflag: /\s+$/,
- },
- invisible: {
- noflag: /\u200B/,
- },
- jinja: {
- noflag: /(^|\n)\s*%\send|%\sset/,
- },
- notWhitespace: {
- noflag: /\S/,
- },
- onlyEmptySpace: {
- noflag: /^[\s\u00A0\u200B]*( )?[\s\u00A0\u200B]*$/,
- },
- semicolon: {
- noflag: / ?; ?/,
- },
- space: {
- noflag: /\s+/,
- g: /\s+/g,
- },
- spaceOrNewline: {
- noflag: /[\s\n\r]+/,
- g: /[\s\n\r]+/g,
- },
- startAndEndInvisible: {
- noflag: /^\u200B|\u200B$/,
- g: /^\u200B|\u200B$/g,
- },
- startAndEndSpace: {
- noflag: /^\s+|\s+$/,
- g: /^\s+|\s+$/g,
- },
- startAndEndSemicolon: {
- noflag: /^ ?;? ?| ?;? ?$/,
- },
- startInvisible: {
- noflag: /^\u200B/,
- },
- startNotChar: {
- noflag: /^[^\S\u00A0\u200B]+/,
- },
- startSingleSpace: {
- noflag: /^\s[\S\u00A0\u200B]/,
- },
- startSpace: {
- noflag: /^\s+/,
- },
- },
-
- /**
- * Compares two nodes to see if they are similar.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {Node} otherNode
- * @returns {Boolean} true if the nodes are similar
- */
- compareNodes: function (node, otherNode) {
- if (!otherNode || !node) {
- return false;
- }
- if (node.tagName !== otherNode.tagName) {
- return false;
- }
- if (dom.isText(node)) {
- return true;
- }
- this.removeBlankAttrs(node);
- this.removeBlankAttrs(otherNode);
- this.orderClass(node);
- this.orderStyle(node);
- this.orderClass(otherNode);
- this.orderStyle(otherNode);
- var nodeAttributes = this._getAttrsNoID(node);
- var otherNodeAttributes = this._getAttrsNoID(otherNode);
- if (nodeAttributes.length !== otherNodeAttributes.length) {
- return false;
- }
- for (var i = 0; i < nodeAttributes.length; i++) {
- var attr = nodeAttributes[i];
- var otherAttr = otherNodeAttributes[i];
- if (attr.name !== otherAttr.name || attr.value !== otherAttr.value) {
- return false;
- }
- }
- return true;
- },
- /**
- * Returns the number of leading breakable space in the textNode.
- * Note: returns 0 if the node is not a textNode.
- *
- * @param {Node} node
- */
- countLeadingBreakableSpace: function (node) {
- if (!dom.isText(node)) {
- return 0;
- }
- var clone = $(node).clone()[0];
- var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).start;
- return breakableSpace === 1 ? 0 : breakableSpace;
- },
- /**
- * Returns the number of trailing breakable space in the textNode.
- * Note: returns 0 if the node is not a textNode.
- *
- * @param {Node} node
- */
- countTrailingBreakableSpace: function (node) {
- if (!dom.isText(node)) {
- return 0;
- }
- var clone = $(node).clone()[0];
- var breakableSpace = this.removeExtremeBreakableSpace(clone, 0).end;
- return breakableSpace === 1 ? 0 : breakableSpace;
- },
- /**
- * Remove the dom between 2 points (respecting unbreakable rules).
- * Returns an object::
- *
- * {
- * node: pointA.node (updated if necessary),
- * offset: pointA.offset (updated if necessary),
- * changed: bool (whether changes were applied)
- * }
- *
- * @param {Object} pointA
- * @param {Node} pointA.node
- * @param {number} pointA.offset
- * @param {Object} pointB
- * @param {Node} pointB.node
- * @param {number} pointB.offset
- * @returns {Object} {node, offset, changed}
- */
- deleteBetween: function (pointA, pointB) {
- var self = this;
- if (pointB.node.childNodes[pointB.offset]) {
- var firstLeaf = this.firstLeaf(pointB.node.childNodes[pointB.offset]);
- pointB = this.makePoint(firstLeaf, 0);
- }
- if (pointB.node.tagName && pointB.node.tagName !== 'BR' && pointB.offset >= dom.nodeLength(pointB.node)) {
- pointB = dom.nextPoint(pointB);
- }
- var changed;
- var commonAncestor = dom.commonAncestor(pointA.node, pointB.node);
-
- var ecAncestor = dom.ancestor(pointB.node, function (node) {
- return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
- });
- var next = this.splitTree(ecAncestor, pointB, {
- nextText: true,
- });
-
- var scAncestor = dom.ancestor(pointA.node, function (node) {
- return node === commonAncestor || self.options.isUnbreakableNode(node.parentNode);
- });
- if (dom.isIcon(pointA.node)) {
- pointA = dom.prevPoint(pointA);
- }
- this.splitTree(scAncestor, pointA, {
- nextText: true,
- });
- pointA.offset = dom.nodeLength(pointA.node);
-
- var nodes = [];
- dom.nextPointUntil(pointA, function (point) {
- if (point.node === next || !point.node) {
- return true;
- }
- if (dom.isText(point.node) && point.offset) {
- return;
- }
- var target = point.node.childNodes[point.offset] || point.node;
- if (target === pointA.node || $.contains(target, pointA.node) || target === next || $.contains(target, next)) {
- return;
- }
- if (
- nodes.indexOf(target) === -1 && !dom.ancestor(target, function (target) {
- return nodes.indexOf(target) !== -1;
- })
- ) {
- nodes.push(target);
- }
- });
- $(nodes).remove();
-
- changed = !!nodes.length;
- var toMerge = changed && pointA.node.parentNode !== next.parentNode;
-
-
- var point = this.makePoint(this.firstLeaf(next), 0);
- if (nodes.length > 1 || nodes.length && !dom.isText(nodes[0])) {
- point = this.removeEmptyInlineNodes(point);
- }
-
- // Remove whole li/ul/ol if deleted all contents of li/ul/ol
- var ul = dom.ancestor(next, function (n) {
- return n.tagName === 'UL' || n.tagName === 'OL';
- });
- if (ul && next[dom.isText(next) ? 'textContent' : 'innerHTML'] === '' && pointA.node !== next.previousSibling) {
- var toRemove = next;
- while (
- toRemove !== ul && toRemove.parentNode &&
- !this.options.isUnbreakableNode(toRemove.parentNode) &&
- this.isBlankNode(toRemove.parentNode)
- ) {
- toRemove = toRemove.parentNode;
- }
- $(toRemove).remove();
- }
- if (!$.contains(this.editable, pointA.node)) {
- pointA = point;
- }
-
- if (toMerge) {
- pointA = this.deleteEdge(pointA.node, 'next') || pointA;
- }
-
- return {
- node: pointA.node,
- offset: pointA.offset,
- changed: changed,
- };
- },
- /**
- * Remove the edge between a node and its sibling
- * (= merge the nodes, respecting unbreakable rules).
- *
- * @param {Node} node
- * @param {String('next'|'prev')} direction
- * @param {Boolean} doNotTryNonSim true to not try merging non-similar nodes
- * @returns {Object} {node, offset}
- */
- deleteEdge: function (node, direction, doNotTryNonSim) {
- var prevOrNext = direction === 'prev' ? 'previousSibling' : 'nextSibling';
- var result = false;
- var startN = node;
-
- if (node.tagName === 'BR' && node.nextSibling && !(dom.isText(node.nextSibling) && !this.isVisibleText(node.nextSibling))) {
- node = node.nextSibling;
- node = this.firstLeaf(node);
- }
-
- var nodes = [];
- var next;
- while (node && node !== this.editable && !this.options.isUnbreakableNode(node)) {
- nodes.push(node);
-
- next = node[prevOrNext];
- while (next && !next.tagName) {
- if (!this.getRegex('char').test(next.textContent)) {
- next = next[prevOrNext];
- continue;
- }
- break;
- }
-
- if (next) {
- break;
- }
- node = node.parentNode;
- }
-
- if (next && next.tagName === 'TABLE') {
- return this.makePoint(node, 0);
- }
-
- var ifBrRemovedAndMerge = !_.filter(nodes, this.isNodeBlockType.bind(this)).length;
- var brRemoved = false;
-
- var range = this.context.invoke('editor.createRange');
-
- var spaceToRemove = [];
- var offset;
- while ((node = nodes.pop())) {
- next = node[prevOrNext];
- while (next && !next.tagName) {
- if (!this.getRegex('char').test(next.textContent)) {
- spaceToRemove.push(next);
- next = next[prevOrNext];
- continue;
- }
- break;
- }
-
- // handle merge of text with p (useful in case of
text
with
text
)
- if (next && dom.isText(node) && this.isNodeBlockType(next)) {
- node = [next, next = node][0]; // swap node and next
- direction = direction === 'next' ? 'prev' : 'next'; // switch direction
- }
- if (next && dom.isText(next) && this.isNodeBlockType(node)) {
- if (this.isBlankNode(node, true)) {
- $(node).empty();
- }
- $(node)[direction === 'next' ? 'append' : 'prepend'](next);
- offset = direction === 'next' ? 0 : dom.nodeLength(next);
- if (dom.isText(next[prevOrNext])) {
- // merge text if needed
- var sib = next[prevOrNext];
- var firstText = (direction === 'next' ? sib : next).textContent;
- var secondText = (direction === 'next' ? next : sib).textContent;
- next.textContent = firstText + secondText;
- $(sib).remove();
- offset = firstText.length;
- }
- result = this.makePoint(next, offset);
- break;
- }
-
- if (
- !next ||
- !(node.tagName || next.tagName === 'BR') ||
- !next.tagName
- ) {
- continue;
- }
-
- if (!brRemoved && next.tagName === 'BR' && (!next[prevOrNext] || this.compareNodes(node, next[prevOrNext]))) {
- var newNext = next[prevOrNext];
- $(next).remove();
- next = newNext;
- offset = (next ? direction === 'prev' : direction === 'next') ? dom.nodeLength(next) : 0;
- result = this.makePoint(next || node, offset);
- if (!ifBrRemovedAndMerge) {
- continue;
- }
- brRemoved = true;
- ifBrRemovedAndMerge = false;
- }
-
- if (!this.compareNodes(node, next)) {
- continue;
- }
- _.each(spaceToRemove, function (space) {
- $(space).remove();
- });
- spaceToRemove = [];
- next = node[prevOrNext];
- var $next = $(next);
- if (next.tagName) {
- var textNode;
- var nextTextNode;
- var deep;
- if (direction === 'prev') {
- textNode = this.firstLeaf(node);
- if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(textNode);
- range.so = range.eo = 0;
- nextTextNode = this.lastLeaf(next);
- if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(nextTextNode);
- }
- }
- deep = this.lastLeaf(next);
- result = this.makePoint(deep, dom.nodeLength(deep));
- if (
- this.getRegex('char').test(node.textContent) || node.childElementCount > 1 ||
- node.firstElementChild && node.firstElementChild.tagName !== "BR"
- ) {
- $next.append($(node).contents());
- }
- $(node).remove();
- } else {
- nextTextNode = this.firstLeaf(next);
- if (!nextTextNode.tagName && !dom.ancestor(nextTextNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(nextTextNode);
- textNode = this.lastLeaf(node);
- if (!textNode.tagName && !dom.ancestor(textNode, dom.isPre)) {
- this.removeExtremeBreakableSpace(textNode);
- range.so = range.eo = dom.nodeLength(node);
- }
- }
- if (node.innerHTML.trim() === ' ') {
- $(node).contents().remove();
- }
- deep = this.lastLeaf(node);
- result = this.makePoint(deep, dom.nodeLength(deep));
- $(node).append($next.contents());
- $next.remove();
- }
- continue;
- } else if (!this.getRegex('char').test(next.textContent)) {
- result = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
- $next.remove();
- continue;
- }
-
- break;
- }
-
- if (!result && startN && !doNotTryNonSim) {
- result = this.deleteNonSimilarEdge(startN, direction);
- }
-
- return result;
- },
- /**
- * Find and delete the previous/next non-similar edge if possible.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {String} direction 'prev' or 'next'
- * @returns {false|Object} {node, offset}
- */
- deleteNonSimilarEdge: function (node, direction) {
- var next = node[direction === 'next' ? 'nextSibling' : 'previousSibling'];
- while (
- next && dom.isText(next) &&
- this.getRegexBlank({
- space: true,
- invisible: true,
- }).test(next.textContent)
- ) {
- next = next[direction === 'next' ? 'nextSibling' : 'previousSibling'];
- }
-
- if (next) {
- return;
- }
-
- node = this.firstBlockAncestor(node);
-
- if (this.options.isUnbreakableNode(node)) {
- return;
- }
-
- var point = this.makePoint(node, direction === 'prev' ? 0 : dom.nodeLength(node));
- var otherBlock = this.findNextBlockToMerge(point.node, direction);
-
- if (!otherBlock) {
- return;
- }
-
- var blockToMergeFrom = direction === 'next' ? otherBlock : point.node;
- var blockToMergeInto = direction === 'next' ? point.node : otherBlock;
-
- // empty tag are removed
- if (
- this.getRegexBlank({
- space: true,
- newline: true,
- }).test(blockToMergeInto.textContent) &&
- !$(blockToMergeInto).find('.fa').length && $(blockToMergeInto).find('br').length <= 1
- ) {
- $(blockToMergeInto).remove();
- return this.makePoint(this.firstLeaf(blockToMergeFrom), 0);
- }
-
- // UL/OL can only have LI, UL or OR as direct children
- if (dom.isList(blockToMergeInto) && !dom.isLi(blockToMergeFrom) && !dom.isList(blockToMergeFrom)) {
- return;
- }
-
- return this.mergeNonSimilarBlocks(blockToMergeFrom, blockToMergeInto);
- },
- /**
- * Deletes the contents of the selected DOM.
- *
- * @returns {Boolean} true if there was a selection to delete
- */
- deleteSelection: function () {
- var range = this.context.invoke('editor.createRange');
- if (range.isCollapsed()) {
- return false;
- }
- var point = this.deleteBetween(range.getStartPoint(), range.getEndPoint());
- point = this.fillEmptyNode(point);
-
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range = range.select();
-
- this.editable.normalize();
- this.context.invoke('editor.saveRange');
-
- // remove tooltip when remove DOM nodes
- $('body > .tooltip').tooltip('hide');
-
- return true;
- },
- /**
- * Fill up an empty node so as to allow the carret to go inside it.
- * A block node will be filled with a , with the offset before it.
- * An inline node will be filled with two zero-width spaces, with the offset in between the two.
- * Returns the given point (with the completed node and the updated offset).
- *
- * @param {Object} point {node, offset}
- * @returns {Object} {node, offset}
- */
- fillEmptyNode: function (point) {
- if (
- !point.node.tagName && this.getRegexBlank({
- space: true,
- invisible: true,
- nbsp: true,
- }).test(point.node.parentNode.innerHTML)
- ) {
- point.node = point.node.parentNode;
- point.offset = 0;
- }
- if (
- point.node.tagName && point.node.tagName !== 'BR' &&
- this.getRegexBlank({
- space: true,
- invisible: true,
- nbsp: true,
- }).test(point.node.innerHTML)
- ) {
- var text = this.document.createTextNode('');
- point.node.innerHTML = '';
- point.node.appendChild(text);
- point.node = text;
- point.offset = 0;
- }
- if (point.node.parentNode.innerHTML === '') {
- if (this.isNodeBlockType(point.node.parentNode)) {
- var node = point.node.parentNode;
- node.innerHTML = ' ';
- point.node = node.firstChild;
- point.offset = 0;
- } else {
- point.node.textContent = '\u200B\u200B';
- point.offset = 1;
- }
- }
- return point;
- },
- /**
- * Get the "format" ancestors list of nodes.
- * In this context, a "format" node is understood as
- * an editable block or an editable element expecting text
- * (eg.: p, h1, span).
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- filterFormatAncestors: function (nodes) {
- var self = this;
- var selectedNodes = [];
- _.each(this.filterLeafChildren(nodes), function (node) {
- var ancestor = dom.ancestor(node, function (node) {
- return dom.isCell(node) || (
- !self.options.isUnbreakableNode(node) &&
- (self.isFormatNode(node) || self.isNodeBlockType(node))
- ) && self.editable !== node;
- });
- if (!ancestor) {
- ancestor = node;
- }
- if (dom.isCell(ancestor)) {
- ancestor = node;
- }
- if (ancestor && selectedNodes.indexOf(ancestor) === -1) {
- selectedNodes.push(ancestor);
- }
- });
- return selectedNodes;
- },
- /**
- * Get the "leaf" children of a list of nodes.
- * In this context, a "leaf" is understood as
- * either a text node or a node that doesn't expect text contents.
- *
- * @param {Node[]} nodes
- * @returns {Node[]}
- */
- filterLeafChildren: function (nodes) {
- var self = this;
- return _.compact(_.map(nodes, function (node) {
- if (node.firstChild) {
- node = node.firstChild;
- }
- if (
- node.tagName === "BR" ||
- self.isVisibleText(node) ||
- dom.isFont(node) ||
- dom.isImg(node) ||
- dom.isDocument(node)
- ) {
- return node;
- }
- }));
- },
- /**
- * Find the previous/next non-similar block to merge with.
- * "Similar" means that they have the same tag, styles, classes and attributes.
- *
- * @param {Node} node
- * @param {String} direction 'prev' or 'next
- * @returns {false|Node}
- */
- findNextBlockToMerge: function (node, direction) {
- var self = this;
- var startNode = node;
- var mergeableTags = this.options.styleTags.join(', ') + ', li';
- var blockToMerge = false;
-
- var li = dom.ancestor(node, function (n) {
- return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
- });
- var nextElementSibling = direction === 'next' ? 'nextElementSibling' : 'previousElementSibling';
- li = li && dom.isLi(li) ? li : undefined;
- if (li) {
- if (li[nextElementSibling]) {
- node = li;
- } else {
- node = dom.ancestor(node, function (n) {
- return ((n.tagName === 'UL' || n.tagName === 'OL') && n[nextElementSibling] &&
- (direction === 'next' || dom.isList(n[nextElementSibling])));
- });
- }
- }
-
- if (!node || !node[nextElementSibling]) {
- return false;
- }
-
- node = node[nextElementSibling];
-
- var ulFoldedSnippetNode = dom.ancestor(node, function (n) {
- return $(n).hasClass('o_ul_folded');
- });
- var ulFoldedSnippetStartNode = dom.ancestor(startNode, function (n) {
- return $(n).hasClass('o_ul_folded');
- });
- if (
- (this.options.isUnbreakableNode(node) && (!ulFoldedSnippetNode || ulFoldedSnippetNode === this.editable)) &&
- this.options.isUnbreakableNode(startNode) && (!ulFoldedSnippetStartNode || ulFoldedSnippetStartNode === this.editable)
- ) {
- return false;
- }
-
- node = this.firstBlockAncestor(node);
-
- li = dom.isList(node) ? undefined : dom.ancestor(node, function (n) {
- return n !== node && self.isNodeBlockType(n) || dom.isLi(n);
- });
- li = li && dom.isLi(li) ? li : undefined;
- node = li || node;
-
- if (node.tagName === 'UL' || node.tagName === 'OL') {
- node = node[direction === 'next' ? 'firstElementChild' : 'lastElementChild'];
- }
-
- if (this.options.isUnbreakableNode(node)) {
- return false;
- }
-
- if (node === startNode || $(node).has(startNode).length || $(startNode).has(node).length) {
- return false;
- }
-
- var $mergeable = $(node).find('*').addBack()
- .filter(mergeableTags)
- .filter(function (i, n) {
- if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
- return n;
- }
- });
- if ($mergeable.length) {
- blockToMerge = $mergeable[direction === 'next' ? 'first' : 'last']()[0] || false;
- }
-
- return blockToMerge;
- },
- /**
- * Get the first ancestor of a node, that is of block type (or itself).
- *
- * @param {Node} node
- * @returns {Node}
- */
- firstBlockAncestor: function (node) {
- var self = this;
- return dom.ancestor(node, function (n) {
- return self.isNodeBlockType(n);
- });
- },
- /**
- * Get the first leaf of a node, that is editable and not a media.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- firstLeaf: function (node) {
- while (node.firstChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
- node = node.firstChild;
- }
- return node;
- },
- /**
- * Get the first leaf of a node, that is an element and not a BR.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node} node
- */
- firstNonBRElementLeaf: function (node) {
- while (node.firstElementChild && node.firstElementChild.tagName !== 'BR') {
- node = node.firstElementChild;
- }
- return node;
- },
- /**
- * Returns the node targeted by a path
- *
- * @param {Object[]} path list of object (tagName, offset)
- * @returns {Node}
- */
- fromPath: function (path) {
- var node = this.editable;
- var to;
- path = path.slice();
- while ((to = path.shift())) {
- node = _.filter(node.childNodes, function (node) {
- return !to.tagName && node.tagName === 'BR' || node.tagName === to.tagName;
- })[to.offset];
- }
- return node;
- },
- /**
- * Returns (and creates if necessary) a regular expression.
- * If a regular expression with the given name exists, simply returns it.
- * Otherwise, creates a new one with the given name, exp and flag.
- *
- * @param {String} name
- * @param {String} [flag] optional
- * @param {String} [exp] optional
- * @returns {RegExp}
- */
- getRegex: function (name, flag, exp) {
- var flagName = flag || 'noflag';
- flag = flag || '';
- // If the regular expression exists, but not with this flag:
- // retrieve whichever version of it and apply the new flag to it,
- // then save that new version in the `regex` object.
- if (this.regex[name] && !this.regex[name][flagName]) {
- if (exp) {
- console.warn("A regular expression already exists with the name: " + name + ". The expression passed will be ignored.");
- }
- var firstVal = this.regex[name][Object.keys(this.regex[name])[0]];
- this.regex[name][flagName] = new RegExp(firstVal, flag);
- } else if (!this.regex[name]) {
- // If the regular expression does not exist:
- // save it into the `regex` object, with the name, expression
- // and flag passed as arguments (if any).
- if (!exp) {
- throw new Error("Cannot find a regular expression with the name " + name + ". Pass an expression to create it.");
- }
- this.regex[name] = {};
- this.regex[name][flagName] = new RegExp(exp, flag);
- }
- return this.regex[name][flagName];
- },
- /**
- * Returns (and creates if necessary) a regular expression
- * targetting a string made ONLY of some combination of the
- * characters enabled with options.
- * If a regular expression with the given options exists, simply returns it.
- * eg: getRegexBlank({space: true, nbsp: true}) => /^[\s\u00A0]*$/
- *
- * @param {Object} [options] optional
- * @param {Boolean} options.not ^ (not all that follows)
- * @param {Boolean} options.space \s (a whitespace)
- * @param {Boolean} options.notspace \S (not a whitespace)
- * @param {Boolean} options.nbsp \u00A0 (a non-breakable space)
- * @param {Boolean} options.invisible \u200B (a zero-width character)
- * @param {Boolean} options.newline \n|\r (a new line or a carriage return)
- * @param {Boolean} options.atLeastOne + (do not target blank strings)
- * @returns {RegExp}
- */
- getRegexBlank: function (options) {
- options = options || {};
- var charMap = {
- notspace: {
- name: 'NotSpace',
- exp: '\\S',
- },
- space: {
- name: 'Space',
- exp: '\\s',
- },
- nbsp: {
- name: 'Nbsp',
- exp: '\\u00A0',
- },
- invisible: {
- name: 'Invisible',
- exp: '\\u200B',
- },
- newline: {
- name: 'Newline',
- exp: '\\n\\r',
- },
- };
- var name = 'only';
- var exp = '';
- var atLeastOne = options.atLeastOne;
- options.atLeastOne = false;
-
- // Build the expression and its name
- if (options.not) {
- name += 'Not';
- exp += '^';
- options.not = false;
- }
- _.each(options, function (value, key) {
- if (value && charMap[key]) {
- name += charMap[key].name;
- exp += charMap[key].exp;
- }
- });
-
- exp = '^[' + exp + ']' + (atLeastOne ? '+' : '*') + '$';
- name += atLeastOne ? 'One' : '';
- return this.getRegex(name, undefined, exp);
- },
- /**
- * Returns a list of all selected nodes in the range.
- *
- * @returns {Node []}
- */
- getSelectedNodes: function () {
- var range = this.context.invoke('editor.createRange');
- if (!range.isCollapsed()) {
- if (range.so && !range.sc.tagName) {
- if (range.sc === range.ec) {
- range.sc = range.ec = range.sc.splitText(range.so);
- range.eo -= range.so;
- } else {
- range.sc = range.sc.splitText(range.so);
- }
- range.so = 0;
- }
- if (!range.ec.tagName && range.eo !== dom.nodeLength(range.ec) && !range.ec.tagName) {
- range.ec.splitText(range.eo);
- }
- range.select();
- }
- var res = [range.sc];
- var point = range.getStartPoint();
- var prevNode = range.sc;
- dom.nextPointUntil(point, function (pt) {
- if (pt.node !== prevNode) {
- var ok = true;
- // Return only the smallest traversed children
- _.each(res, function (n, i) {
- // If pt.node is a child of res[i], replace it
- if (dom.listAncestor(pt.node).indexOf(n) !== -1) {
- res[i] = pt.node;
- ok = false;
- } else if (dom.listAncestor(n).indexOf(pt.node) !== -1) {
- // Inversely, skip parents of res[i]
- ok = false;
- }
- });
- if (ok) {
- res.push(pt.node);
- }
- }
- prevNode = pt.node;
- return pt.node === range.ec;
- });
- return _.uniq(res);
- },
- /**
- * Returns a list of all selected text nodes in the range.
- *
- * @returns {Node []}
- */
- getSelectedText: function () {
- var self = this;
- var selectedText = _.filter(this.getSelectedNodes(), function (node) {
- node = self.firstLeaf(node);
- return self.isVisibleText(node);
- });
- return _.uniq(selectedText);
- },
- /**
- * Returns true if the value contains jinja logic.
- *
- * @returns {Boolean}
- */
- hasJinja: function (value) {
- return this.getRegex('jinja').test(value);
- },
- /**
- * Inserts a block node (respecting the rules of unbreakable nodes).
- * In order to insert the node, the DOM tree is split at the carret position.
- * If there is a selection, it is deleted first.
- *
- * @param {Node} node
- */
- insertBlockNode: function (node) {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- range = range.deleteContents();
- var point = range.getStartPoint();
- var unbreakable = point.node;
- if (!this.options.isUnbreakableNode(point.node)) {
- unbreakable = dom.ancestor(point.node, function (node) {
- return self.options.isUnbreakableNode(node.parentNode) || node === self.editable;
- }) || point.node;
- }
-
- if (unbreakable === point.node && !point.offset && point.node.tagName !== 'P') {
- if (point.node.innerHTML === ' ') {
- $(point.node.firstElementChild).remove();
- }
- if (point.node.tagName === "BR") {
- $(point.node).replaceWith(node);
- } else {
- point.node.append(node);
- }
- return;
- }
- if (!this.options.isUnbreakableNode(point.node)) {
- var tree = dom.splitTree(unbreakable, point, {
- isSkipPaddingBlankHTML: true,
- isNotSplitEdgePoint: true,
- });
- if ((!tree || $.contains(tree, range.sc)) && (point.offset || point.node.tagName)) {
- tree = tree || dom.ancestor(point.node, function (node) {
- return self.options.isUnbreakableNode(node.parentNode);
- });
- $(tree).after(node);
- } else {
- $(tree).before(node);
- }
- } else {
- // prevent unwrapped text in unbreakable
- if (dom.isText(unbreakable)) {
- $(unbreakable).wrap(this.document.createElement('p'));
- unbreakable.splitText(point.offset);
- unbreakable = unbreakable.parentNode;
- point.offset = 1;
- }
- $(unbreakable.childNodes[point.offset]).before(node);
- }
- if (range.sc.innerHTML === ' ') {
- var clone = range.sc.cloneNode(true);
- if (node.previousSibling === range.sc) {
- $(node).after(clone);
- } else if (node.nextSibling === range.sc) {
- $(node).before(clone);
- }
- }
- },
- /**
- * Inserts a string as a text node in the DOM.
- * If the range is on a text node, splits the text node first.
- * Otherwise just inserts the text node.
- * Wraps it in a P if needed.
- *
- * @param {String} text
- */
- insertTextInline: function (text) {
- if (text === " ") {
- text = "\u00A0";
- }
- this.editable.normalize();
- this.deleteSelection();
-
- var range = this.context.invoke('editor.createRange');
-
-
- if (!range.sc.tagName && range.sc.textContent.match(/\S/)) {
- var before = range.sc.textContent.slice(0, range.so);
- var after = range.sc.textContent.slice(range.so);
-
- if (
- (before.length || after.length) &&
- (!before.length || before[before.length - 1] === ' ') &&
- (!after.length || after[0] === ' ')
- ) {
- var startSpace = this.getRegex('startSpace');
- var endSpace = this.getRegex('endSpace');
- before = before.replace(endSpace, '\u00A0');
- after = after.replace(startSpace, '\u00A0');
- range.sc.textContent = before + after;
- if (range.so > before.length) {
- range.so = range.eo = before.length;
- }
- range.select();
- }
- }
-
-
- range = range.deleteContents();
- range = this._standardizeRangeOnEdge(range);
- var textNode = this._insertTextNodeInEditableArea(range, text);
-
- // if the text node can't be inserted in the dom (not editable area) do nothing
- if (!textNode) {
- return;
- }
-
- this.secureExtremeSingleSpace(range.sc);
-
- this._wrapTextWithP(textNode);
-
- range = this.context.invoke('editor.setRange', textNode, text.length);
- range.select();
- this.context.invoke('editor.saveRange');
-
- // Clean up and make leading/trailing/multiple space visible
- this.normalize();
- range = this.context.invoke('editor.createRange');
- var reStartBlanks = this.getRegex('startBlanks', '', '^([\\s\\u00A0\\u200B]*)');
- var reEndBlanks = this.getRegex('endBlanks', '', '([\\s\\u00A0\\u200B]*)$');
- var reAllNBSP = /\u00A0/g;
- var reMultipleSpace = /(\s){2,}/;
-
- var textContent = range.sc.textContent;
- var startBlanks = range.sc.textContent.match(reStartBlanks)[0] || '';
- var endBlanks = range.sc.textContent.match(reEndBlanks)[0] || '';
- var trimmed = range.sc.textContent.replace(reStartBlanks, '').replace(reEndBlanks, '');
- // Remove the single inner nbsp's and replace the multiple inner spaces with nbsp's
- var cleanContents = trimmed.replace(reAllNBSP, ' ')
- .replace(reMultipleSpace, function (space) {
- return Array(space.length + 1).join('\u00A0');
- });
- // Keep the leading/trailing whitespace, nbsp's and zero-width chars
- range.sc.textContent = startBlanks + cleanContents + endBlanks;
-
- this._removeInvisibleChar(range);
-
- try {
- range.select();
- } catch (e) {
- /* Under certain unknown conditions, the range cannot be
- applied and triggered an error. In order to allow the user to
- keep editing, we need to prevent the dialog from showing, hence
- the use of `console.error`.
- If this error appears, the carret is moved to the beginning
- of the focused node.*/
- console.error(e);
- var log = [
- ['textContent', textContent],
- ['trimmed', trimmed],
- ['cleanContents', cleanContents],
- ];
- if (range) {
- log.push(
- ['start', range.sc.outerHTML || range.sc.textContent, range.so],
- ['end', range.ec.outerHTML || range.ec.textContent, range.eo],
- ['parent', range.sc.parentNode && range.sc.parentNode.outerHTML]);
- }
- console.error('Informations for debugging', log);
- }
- this.context.invoke('editor.saveRange');
- },
- /**
- * Returns true if the node is a text node containing nothing
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isBlankText: function (node) {
- return dom.isText(node) &&
- this.getRegexBlank({
- not: true,
- notspace: true,
- nbsp: true,
- invisible: true,
- })
- .test(node.textContent);
- },
- /**
- * Returns true if the node is blank.
- * In this context, a blank node is understood as
- * a node expecting text contents (or with children expecting text contents)
- * but without any.
- *
- * @param {Node} node
- * @param {Boolean} brIsBlank true to consider nodes with BR element
- * as ultimate only child to be blank
- * @returns {Boolean}
- */
- isBlankNode: function (node, brIsBlank) {
- var self = this;
- if (brIsBlank && dom.isBR(node)) {
- return true;
- }
- if (dom.isVoid(node) || dom.isIcon(node)) {
- return false;
- }
- if (this.getRegexBlank({
- space: true,
- }).test(node[dom.isText(node) ? 'textContent' : 'innerHTML'])) {
- return true;
- }
- var areAllChildrenBlank = _.all(node.childNodes, function (node) {
- return self.isBlankNode(node, brIsBlank);
- });
- if (node.childNodes.length && areAllChildrenBlank) {
- return true;
- }
- return false;
- },
- /**
- * Returns true if the point is on the left/right edge of the first
- * previous/next point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @param {String('left'|'right')} side
- * @returns {Boolean}
- */
- isEdgeOfTag: function (point, tagName, side) {
- var method = side === 'left' ? 'isLeftEdgePoint' : 'isRightEdgePoint';
- var prevOrNext = side === 'left' ? 'prev' : 'next';
- var newPt;
- var first = true;
- while (point && point.node.tagName !== tagName) {
- newPt = this.skipNodes(point, prevOrNext, function (pt) {
- return pt.node.tagName === tagName && dom[method](pt);
- });
- if (newPt.node.tagName === tagName || newPt.node.tagName === 'BR') {
- point = newPt;
- break;
- }
- if (newPt === point && (!first || dom.isText(point.node) && !dom[method](point))) {
- break;
- }
- point = dom[prevOrNext + 'Point'](newPt);
- first = false;
- }
- if (!point) {
- return false;
- }
- var ancestor = dom.ancestor(point.node, function (n) {
- return n.tagName === tagName;
- });
- return !!(ancestor && dom[method + 'Of'](point, ancestor));
- },
- /**
- * Returns true if the node is a "format" node.
- * In this context, a "format" node is understood as
- * an editable block or an editable element expecting text
- * (eg.: p, h1, span).
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isFormatNode: function (node) {
- return node.tagName && this.options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
- },
- /**
- * Returns true if the node is within a table.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isInTable: function (node) {
- return !!dom.ancestor(node, function (n) {
- return n.tagName === 'TABLE';
- });
- },
- /**
- * Returns true if the point is on the left edge of a block node
- * (skips insignificant nodes).
- *
- * @param {Object} point
- * @returns {Boolean}
- */
- isLeftEdgeOfBlock: function (point) {
- point = this.skipNodes(point, 'prev');
- return dom.isLeftEdgePointOf(point, this.firstBlockAncestor(point.node));
- },
- /**
- * Returns true if the point is on the left edge of the first
- * previous point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @returns {Boolean}
- */
- isLeftEdgeOfTag: function (point, tagName) {
- return this.isEdgeOfTag(point, tagName, 'left');
- },
- /**
- * Returns true if the node is a block.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isNodeBlockType: function (node) {
- if (dom.isText(node)) {
- return false;
- }
- var display = this.window.getComputedStyle(node).display;
- // All inline elements have the word 'inline' in their display value, except 'contents'
- return display.indexOf('inline') === -1 && display !== 'contents';
- },
- /**
- * Returns true if the point is on the right edge of the first
- * next point with the given tag name (skips insignificant nodes).
- *
- * @param {Object} point
- * @param {String} tagName
- * @returns {Boolean}
- */
- isRightEdgeOfTag: function (point, tagName) {
- return this.isEdgeOfTag(point, tagName, 'right');
- },
- /**
- * Returns true if point should be ignored.
- * This is generally used for trying to figure out if the point is an edge point.
- *
- * @param {Object} point
- * @param {String} direction ('prev' or 'next')
- * @param {Object} options
- * @param {Boolean} options.noSkipBlankText true to not skip blank text
- * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
- * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
- * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
- * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
- * @returns {Boolean}
- */
- isSkippable: function (point, direction, options) {
- options = options || {};
- var isEdge = direction === 'prev' ? dom.isLeftEdgePoint(point) : dom.isRightEdgePoint(point);
-
- // skip blank text nodes
- if (
- !options.noSkipBlankText &&
- this.isBlankText(point.node)
- ) {
- return true;
- }
- // skip single BRs
- if (
- !options.noSkipSingleBRs &&
- point.node.tagName === 'BR' &&
- (!point.node.previousSibling || this.isBlankText(point.node.previousSibling)) &&
- (!point.node.nextSibling || this.isBlankText(point.node.nextSibling))
- ) {
- return true;
- }
- // skip leading/trailing breakable space
- if (
- !options.noSkipExtremeBreakableSpace &&
- (direction === 'prev' && !isEdge && point.offset <= this.countLeadingBreakableSpace(point.node) ||
- direction === 'next' && point.offset > dom.nodeLength(point.node) - this.countTrailingBreakableSpace(point.node))
- ) {
- return true;
- }
- // skip to leaf node or edge
- var node = direction === 'prev' ? point.node.childNodes[0] : point.node.childNodes[point.node.childNodes.length - 1];
- var offset = direction === 'prev' ? 0 : dom.nodeLength(node);
- if (
- !options.noSkipParent &&
- !isEdge && point.node.childNodes.length &&
- this.isSkippable(this.makePoint(node, offset), direction, options)
- ) {
- return true;
- }
- // skip if on edge and sibling is skippable
- var sibling = direction === 'prev' ? point.node.previousSibling : point.node.nextSibling;
- offset = direction === 'prev' ? 0 : dom.nodeLength(sibling);
- if (
- !options.noSkipSibling &&
- isEdge && sibling &&
- this.isSkippable(this.makePoint(sibling, offset), direction, _.defaults({
- noSkipSibling: true,
- }, options))
- ) {
- return true;
- }
- return false;
- },
- /**
- * Returns true if the node is a text node with visible text.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- isVisibleText: function (node) {
- return !node.tagName && this.getRegex('char').test(node.textContent);
- },
- /**
- * Get the last leaf of a node, that is editable and not a media.
- * In this context, a leaf node is understood as a childless node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- lastLeaf: function (node) {
- while (node.lastChild && !dom.isMedia(node) && this.options.isEditableNode(node)) {
- node = node.lastChild;
- }
- return node;
- },
- /**
- * Returns a Point object from a node and an offset.
- *
- * @param {Node} node
- * @param {Number} offset
- */
- makePoint: function (node, offset) {
- return {
- node: node,
- offset: offset,
- };
- },
- /**
- * Merges mergeFromBlock into mergeIntoBlock, respecting the rules of unbreakable.
- *
- * @param {Node} mergeFromBlock block to merge from
- * @param {Node} mergeIntoBlock block to merge into
- * @returns {Object} {node, offset}
- */
- mergeNonSimilarBlocks: function (mergeFromBlock, mergeIntoBlock) {
- var point;
- var mergeableTags = this.options.styleTags.join(', ') + ', li';
- var $contents = $(mergeFromBlock).find('*').addBack()
- .filter(mergeableTags)
- .filter(function (i, n) {
- if (!(n.tagName === 'LI' && $(n).find(mergeableTags).length)) {
- return n;
- }
- }).contents();
- var containsUnbreakables = !!$contents.filter(this.options.isUnbreakable).length;
-
- if ($contents.length && !containsUnbreakables) {
- if (dom.isText($contents[0])) {
- this.removeExtremeBreakableSpace($contents[0]);
- }
- var $lastContents = $(mergeIntoBlock).contents().last();
- if (!($contents.length === 1 && $contents[0].tagName === 'BR')) {
- if (mergeIntoBlock.innerHTML.trim() === ' ') {
- $(mergeIntoBlock).contents().remove();
- $(mergeIntoBlock).append($contents);
- $lastContents = false;
- } else {
- $lastContents.after($contents);
- }
- }
- while (mergeFromBlock.parentNode && this.isBlankNode(mergeFromBlock.parentNode, true)) {
- mergeFromBlock = mergeFromBlock.parentNode;
- }
- $(mergeFromBlock).remove();
-
- point = {};
- if ($lastContents && $lastContents.length) {
- point = this.makePoint($lastContents[0], dom.nodeLength($lastContents[0]));
- } else {
- point = this.makePoint($contents[0], 0);
- }
-
- point = this.deleteEdge(point.node, 'next', true) || point;
- }
- return point;
- },
- /**
- * Normalize the DOM and range.
- */
- normalize: function () {
- this.editable.normalize();
- var range = this.context.invoke('editor.createRange');
- var rangeN = range.normalize();
-
- // summernote's normalize function fails when br in text,
- // and targets the br instead of the point just after br.
- var point = rangeN.getStartPoint();
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName && point.node.childNodes[point.offset]) {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node !== range.sc || point.offset !== range.so) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range.select();
- }
- },
- /**
- * Simulate a do_notify by notifying the user through a dialog.
- *
- * @param {String} title
- * @param {String} content
- */
- notify: function (title, content) {
- var $notif = $('
' + content + '
');
- new Dialog(this, {
- title: title,
- size: 'medium',
- $content: $notif,
- }).open();
- },
- /**
- * Return true if the `container` contains the `contained` and only
- * the `contained` (blank text nodes are ignored).
- *
- * @param {Node} container
- * @param {Node} contained
- * @returns {Boolean}
- */
- onlyContains: function (container, contained) {
- var self = this;
- if (!$.contains(container, contained)) {
- return false;
- }
- var $contents = $(container).contents();
- var otherContents = $contents.filter(function (index, node) {
- if (node === contained || dom.isText(node) && !self.isVisibleText(node)) {
- return false;
- }
- return true;
- });
- return !otherContents.length;
- },
- /**
- * Reorders the classes in the node's class attribute and returns it.
- *
- * @param {Node} node
- * @returns {String}
- */
- orderClass: function (node) {
- var className = node.getAttribute && node.getAttribute('class');
- if (!className) {
- return null;
- }
- className = className.replace(this.getRegex('spaceOrNewline', 'g'), ' ')
- .replace(this.getRegex('startAndEndSpace', 'g'), '')
- .replace(this.getRegex('space', 'g'), ' ');
- className = className.replace('o_default_snippet_text', '')
- .replace('o_checked', '');
- if (!className.length) {
- node.removeAttribute("class");
- return null;
- }
- className = className.split(" ");
- className.sort();
- className = className.join(" ");
- node.setAttribute('class', className);
- return className;
- },
- /**
- * Reorders the styles in the node's style attributes and returns it.
- *
- * @param {Node} node
- * @returns {String}
- */
- orderStyle: function (node) {
- var style = node.getAttribute('style');
- if (!style) {
- return null;
- }
- style = style.replace(this.getRegex('spaceOrNewline'), ' ')
- .replace(this.getRegex('startAndEndSemicolon', 'g'), '')
- .replace(this.getRegex('semicolon', 'g'), ';');
- if (!style.length) {
- node.removeAttribute("style");
- return null;
- }
- style = style.split(";");
- style.sort();
- style = style.join("; ") + ";";
- node.setAttribute('style', style);
- return style;
- },
- /**
- * Returns the path from the editable node to the given node.
- *
- * @param {Node} node
- * @returns {Object[]} list of objects (tagName, offset)
- */
- path: function (node) {
- var path = [];
- while (node && node !== this.editable) {
- var tagName = node.tagName;
- path.unshift({
- tagName: tagName,
- offset: _.filter(node.parentNode.childNodes, function (node) {
- return node.tagName === tagName;
- }).indexOf(node),
- });
- node = node.parentNode;
- }
- return path;
- },
- /**
- * Removes all attributes without a value from the given node.
- *
- * @param {Node} node
- * @returns {Node}
- */
- removeBlankAttrs: function (node) {
- _.each([].slice.call(node.attributes), function (attr) {
- if (!attr.value) {
- node.removeAttribute(attr.name);
- }
- });
- return node;
- },
- /**
- * Remove a node's direct blank siblings, if any.
- * Eg: TextNodeText => TextNodeText
- *
- * @param {Node} node
- */
- removeBlankSiblings: function (node) {
- var isAfterBlank = node.previousSibling && this.isBlankNode(node.previousSibling);
- if (isAfterBlank) {
- $(node.previousSibling).remove();
- }
- var isBeforeBlank = node.nextSibling && this.isBlankNode(node.nextSibling);
- if (isBeforeBlank) {
- $(node.nextSibling).remove();
- }
- },
- /**
- * Removes the block target and joins its siblings.
- *
- * @param {Node} target
- * @param {Boolean} doNotInsertP true to NOT fill an empty unbreakable with a p element.
- * @returns {Object} {node, offset}
- */
- removeBlockNode: function (target, doNotInsertP) {
- var self = this;
- var check = function (point) {
- if (point.node === target) {
- return false;
- }
- return !point.node || self.options.isEditableNode(point.node) &&
- (point.node.tagName === "BR" || self.isVisibleText(point.node));
- };
- var parent = target.parentNode;
- var offset = [].indexOf.call(parent.childNodes, target);
- var deleteEdge = 'next';
- var point = dom.prevPointUntil(this.makePoint(target, 0), check);
- if (!point || !point.node) {
- deleteEdge = 'prev';
- point = dom.nextPointUntil(this.makePoint(target, 0), check);
- }
-
- $(target).remove();
-
- if (
- point && (deleteEdge === 'prev' && point.offset) ||
- deleteEdge === 'next' && point.offset === dom.nodeLength(point.node)
- ) {
- point = this.deleteEdge(point.node, deleteEdge) || point;
- }
-
- $(parent).contents().filter(function () {
- return dom.isText(this) && self.getRegexBlank({
- atLeastOne: true,
- invisible: true,
- }).test(this.textContent);
- }).remove();
-
- var br;
- if (parent.innerHTML === '') {
- br = this.document.createElement('br');
- $(parent).append(br);
- point = this.makePoint(parent, 0);
- }
-
- if (!doNotInsertP && this.getRegexBlank({
- space: true,
- invisible: true,
- }).test(parent.innerHTML)) {
- br = this.document.createElement('br');
- if (this.options.isUnbreakableNode(parent) && parent.tagName !== "TD") {
- var p = this.document.createElement('p');
- $(p).append(br);
- $(parent).append(p);
- } else {
- $(parent).append(br);
- }
- point = this.makePoint(br.parentNode, 0);
- }
-
- if (point && point.node.tagName === "BR" && point.node.parentNode) {
- point = {
- node: point.node.parentNode,
- offset: [].indexOf.call(point.node.parentNode.childNodes, point.node),
- };
- }
-
- return point || this.makePoint(parent, offset);
- },
- /**
- * Removes the empty inline nodes around the point, and joins its siblings.
- *
- * @param {Object} point {node, offset}
- * @returns {Object} {node, offset}
- */
- removeEmptyInlineNodes: function (point) {
- var node = point.node;
- if (!point.node.tagName && !point.node.textContent.length) {
- node = node.parentNode;
- if ($(node).hasClass('o_default_snippet_text')) {
- // for default snippet value
- return point;
- }
- }
- var prev;
- var next;
- while (
- node.tagName !== 'BR' &&
- (node.tagName ? node.innerHTML : node.textContent) === '' &&
- !this.isNodeBlockType(node) &&
- this.options.isEditableNode(node.parentNode) &&
- (!node.attributes || !node.attributes.contenteditable) &&
- !dom.isMedia(node)
- ) {
- prev = node.previousSibling;
- next = node.nextSibling;
- point = {
- node: node.parentNode,
- offset: [].indexOf.call(node.parentNode.childNodes, node)
- };
- $(node).remove();
- node = point.node;
- }
- if (next && !next.tagName) {
- if (/^\s+[^\s<]/.test(next.textContent)) {
- next.textContent = next.textContent.replace(this.getRegex('startSpace'), '\u00A0');
- }
- }
- if (prev) {
- if (!prev.tagName) {
- if (/[^\s>]\s+$/.test(prev.textContent)) {
- prev.textContent = prev.textContent.replace(this.getRegex('endSpace'), ' ');
- }
- }
- point = this.makePoint(prev, dom.nodeLength(prev));
- }
- return point;
- },
- /**
- * Removes any amount of leading/trailing breakable space from a text node.
- * Returns how many characters were removed at the start
- * and at the end of the text node.
- *
- * @param {Node} textNode
- * @param {Boolean} secureExtremeties (defaults to true)
- * @returns {Object} removed {start, end}
- */
- removeExtremeBreakableSpace: function (textNode, secureExtremeties) {
- if (arguments.length === 1) {
- secureExtremeties = true;
- }
- if (secureExtremeties) {
- this.secureExtremeSingleSpace(textNode);
- }
- var removed = {
- start: 0,
- end: 0,
- };
- textNode.textContent = textNode.textContent.replace(this.getRegex('startNotChar'), function (toRemove) {
- removed.start = toRemove.length;
- return '';
- });
- textNode.textContent = textNode.textContent.replace(this.getRegex('endNotChar'), function (toRemove) {
- removed.end = toRemove.length;
- return '';
- });
- return removed;
- },
- /**
- * Makes the leading/trailing single space of a node non breakable (nbsp).
- *
- * @param {Node} node
- */
- secureExtremeSingleSpace: function (node) {
- if (this.getRegex('endSingleSpace').test(node.textContent)) {
- // if the text ends with a single space, make it insecable
- node.textContent = node.textContent.substr(0, node.textContent.length - 1) + '\u00A0';
- }
- if (this.getRegex('startSingleSpace').test(node.textContent)) {
- // if the text starts with a single space, make it insecable
- node.textContent = '\u00A0' + node.textContent.substr(1, node.textContent.length);
- }
- },
- /**
- * Skips points to ignore (generally for trying to figure out if edge point).
- * Returns the resulting point.
- *
- * @param {Object} point
- * @param {String} direction ('prev' or 'next')
- * @param {function} pred (extra condition to stop at)
- * @param {Object} options
- * @param {Boolean} options.noSkipBlankText true to not skip blank text
- * @param {Boolean} options.noSkipSingleBRs true to not skip single BRs
- * @param {Boolean} options.noSkipExtremeBreakableSpace true to not skip leading/trailing breakable space
- * @param {Boolean} options.noSkipParent true to not skip to leaf nodes or offset 0
- * @param {Boolean} options.noSkipSibling true to not skip if on edge and sibling is skippable
- * @returns {Object} {node, offset}
- */
- skipNodes: function (point, direction, pred, options) {
- var self = this;
- if (arguments.length === 3 && !_.isFunction(arguments[2])) {
- // allow for passing options and no pred function
- options = _.clone(pred);
- pred = null;
- }
- options = options || {};
- return dom[direction + 'PointUntil'](point, function (pt) {
- return !self.isSkippable(pt, direction, options) || pred && pred(pt);
- });
- },
- /**
- * Split the DOM tree at the node's start and end points.
- *
- * @param {Node} node
- */
- splitAtNodeEnds: function (node) {
- if (!node.parentNode) {
- return;
- }
- var startPoint = this.makePoint(node, 0);
- var endPoint = this.makePoint(node, dom.nodeLength(node));
- var splitOptions = {
- isSkipPaddingBlankHTML: true,
- };
- this.splitTree(node.parentNode, startPoint, splitOptions);
- this.splitTree(node.parentNode, endPoint, splitOptions);
- // Splitting at ends may create blank nodes (because of dom.splitTree) so let's clean it up:
- this.removeBlankSiblings(node.parentNode);
- },
- /**
- * Split the DOM tree at the point
- *
- * @param {Node} root - split root
- * @param {BoundaryPoint} point {node, offset}
- * @param {Object} [options]
- * @param {Boolean} [options.nextText] - default: false
- * @param {Boolean} [options.isSkipPaddingBlankHTML] - default: false
- * @param {Boolean} [options.isNotSplitEdgePoint] - default: false
- * @returns {Node} right node of boundary point
- */
- splitTree: function (root, point, options) {
- var nextText;
- if (options && options.nextText && !point.node.tagName) {
- nextText = point.node.splitText(point.offset);
- }
- var emptyText = false;
- if (!point.node.tagName && point.node.textContent === "") {
- emptyText = true;
- point.node.textContent = '\u200B';
- point.offset = 1;
- }
- var next = dom.splitTree(root, point, options);
- if (emptyText) {
- point.node.textContent = '';
- }
- var result = nextText || next || point.node;
- var att = nextText ? 'textContent' : 'innerHTML';
- if (/^\s+([^\s<])/.test(result[att])) {
- result[att] = result[att].replace(this.getRegex('startSpace'), '\u00A0');
- }
- return result;
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- _getAttrsNoID: function (node) {
- var nodeAttributes = [];
- for (var i = 0; i < node.attributes.length; i++) {
- if (node.attributes[i].name !== 'id') {
- nodeAttributes.push(node.attributes[i]);
- }
- }
- return nodeAttributes;
- },
- _insertTextNodeInEditableArea: function (range, text) {
- // try to insert the text node in editable area
- var textNode = this.document.createTextNode(text);
- if (this.options.isEditableNode(range.sc) && $(range.sc).closest('[contenteditable]').attr('contenteditable') === 'true') {
- if (dom.isText(range.sc) && this.isVisibleText(range.sc)) {
- // If range is on visible text: split the text at offset and insert the text node
- range.sc.splitText(range.so);
- range.so = range.eo = dom.nodeLength(range.sc);
- $(range.sc).after(textNode);
- } else if (dom.isBR(range.sc)) {
- if (range.sc.nextSibling && this.isVisibleText(range.sc.nextSibling)) {
- $(range.sc).before(textNode);
- } else {
- $(range.sc).replaceWith(textNode);
- }
- } else if (dom.isVoid(range.sc)) {
- $(range.sc).before(textNode);
- } else if (range.sc.childNodes[range.so]) {
- var node = range.sc.childNodes[range.so];
- if (dom.isBR(node)) {
- $(node).replaceWith(textNode);
- } else {
- $(node).before(textNode);
- }
- } else if (dom.isText(range.sc)) {
- $(range.sc).after(textNode);
- } else if (this.options.isUnbreakableNode(range.sc)) {
- $(range.sc).append(textNode);
- }
- }
- if (!textNode.parentNode && this.options.isEditableNode(range.sc.parentNode)) {
- $(range.sc).before(textNode);
- }
-
- return textNode.parentNode && textNode;
- },
- /**
- * Perform operations that are necessary after the insertion of a visible character:
- * adapt range for the presence of zero-width characters, move out of media, rerange.
- *
- * @private
- */
- _removeInvisibleChar: function (range) {
- if (range.sc.tagName || dom.ancestor(range.sc, dom.isAnchor)) {
- return true;
- }
- var needReselect = false;
- var fake = range.sc.parentNode;
- if ((fake.className || '').indexOf('o_fake_editable') !== -1 && dom.isMedia(fake)) {
- var $media = $(fake.parentNode);
- $media[fake.previousElementSibling ? 'after' : 'before'](fake.firstChild);
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so - 2, range.so - 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so - 2) + range.sc.textContent.slice(range.so - 1);
- range.so = range.eo = range.so - 1;
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so, range.so + 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so) + range.sc.textContent.slice(range.so + 1);
- needReselect = true;
- }
- if (needReselect) {
- range = range.normalize();
- }
- return range;
- },
- _standardizeRangeOnEdge: function (range) {
- var self = this;
-
- var invisible = this.document.createTextNode('\u200B');
-
- if (dom.isText(range.sc) && !this.isVisibleText(range.sc) && range.sc.nextSibling) {
- var firstLeafOfNext = this.firstLeaf(range.sc.nextSibling);
- range = this.context.invoke('editor.setRange', firstLeafOfNext, 0);
- }
-
- // Create empty text node to have a range into the node
- if (range.sc.tagName && !dom.isVoid(range.sc) && !range.sc.childNodes[range.so]) {
- $(range.sc).append(invisible);
- if (invisible.previousSibling && invisible.previousSibling.tagName === "BR") {
- invisible.previousSibling.remove();
- }
- range = this.context.invoke('editor.setRange', invisible, 0);
- }
-
- // On left edge of non-empty element: move before
- var siblings = range.sc.parentNode && range.sc.parentNode.childNodes;
- var isInEmptyElem = !siblings || !siblings.length || _.all(siblings, this.isBlankNode.bind(this));
- if (
- !range.so && !isInEmptyElem &&
- !(range.sc.previousSibling && range.sc.previousSibling.tagName === "BR") &&
- !this.options.isEditableNode(range.sc)
- ) {
- var point = range.getStartPoint();
- var newPoint = dom.prevPointUntil(point, function (pt) {
- return pt.node !== range.sc && !pt.node.tagName && !self.isBlankText(pt.node);
- });
- if (!newPoint || this.firstBlockAncestor(newPoint.node) !== this.firstBlockAncestor(point.node)) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- } else {
- range = this.context.invoke('editor.setRange', newPoint.node, newPoint.offset);
- }
- }
-
- return range;
- },
- _wrapTextWithP: function (textNode) {
- if (dom.ancestor(textNode, dom.isAnchor)) {
- return;
- }
- var self = this;
- var isFormatNode = dom.ancestor(textNode, this.isFormatNode.bind(this));
- if (!isFormatNode) {
- var hasInlineParent = dom.ancestor(textNode.parentNode, function (node) {
- return !self.isNodeBlockType(node);
- });
- if (
- !hasInlineParent &&
- (textNode.tagName ||
- !(textNode.previousSibling && this.isVisibleText(textNode.previousSibling) ||
- textNode.nextSibling && this.isVisibleText(textNode.nextSibling))
- )
- ) {
- var blankP = this.document.createElement('p');
- $(textNode).after(blankP);
- $(blankP).prepend(textNode);
- }
- }
- },
-});
-
-registry.add('HelperPlugin', HelperPlugin);
-
-return HelperPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js b/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js
deleted file mode 100644
index 708ac89333f..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/hint.js
+++ /dev/null
@@ -1,139 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.hint', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-
-
-var HintPlugin = Plugins.hintPopover.extend({
- init: function (context) {
- context.options.hint = (context.options.hint || []).concat(this._hints());
- this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * Replace the current hint.
- */
- replace: function () {
- var self = this;
- var $item = this.$content.find('.note-hint-item.active');
- if ($item.length) {
- this.lastWordRange.select();
- this.context.invoke('HelperPlugin.deleteSelection');
- var range = this.context.invoke('editor.createRange');
-
- this.nodeFromItem($item).each(function () {
- $(range.sc).after(this);
- range = self.context.invoke('editor.setRange', this, dom.nodeLength(this));
- });
- range.select();
- this.context.invoke('editor.saveRange');
- this.lastWordRange = null;
- this.hide();
- this.context.triggerEvent('change', this.$editable.html(), this.$editable[0]);
- this.context.invoke('editor.focus');
- }
- },
- /**
- * @param {JQueryEvent} e
- */
- handleKeyup: function (e) {
- var self = this;
- if ([13, 38, 40].indexOf(e.keyCode) === -1) { // enter, up, down
- var wordRange = this.context.invoke('editor.createRange');
- var keyword_1 = wordRange.sc.textContent.slice(0, wordRange.so);
- if (this.hints.length && keyword_1) {
- this.$content.empty();
- this.$popover.hide();
- this.lastWordRange = wordRange;
- var hasMatch = false;
-
- // test all hints
- this.hints.forEach(function (hint, idx) {
- var match = keyword_1.match(hint.match);
- if (match) {
- hasMatch = true;
- wordRange.so = wordRange.eo - match[0].length;
- self.createGroup(idx, match[0]).appendTo(self.$content);
- }
- });
- if (!hasMatch) {
- return;
- }
-
- // select first .note-hint-item
- this.$content.find('.note-hint-item:first').addClass('active');
-
- // set position for popover after group is created
- var rect = wordRange.getClientRects()[0];
- var bnd = {
- top: rect.top + $(this.document).scrollTop(),
- left: rect.left + $(this.document).scrollLeft(),
- };
- this.$popover.css({
- left: bnd.left,
- top: bnd.top + (this.direction === 'top' ? -this.$popover.outerHeight() : (rect.bottom - rect.top)) - 5,
- });
- } else {
- this.hide();
- }
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Get hint objects.
- *
- * @private
- * @returns {Object[]} hints
- */
- _hints: function () {
- var self = this;
- return [{
- className: 'o_hint_partner',
- match: /\B@(\w+(\s\w*)?)$/,
- search: function (keyword, callback) {
- self._rpc({
- model: 'res.partner',
- method: "search_read",
- fields: ['id', 'name', 'email'],
- domain: [
- ['name', 'ilike', keyword],
- ],
- limit: 10,
- }).then(callback);
- },
- template: function (partner) {
- return partner.name + (partner.email ? ' (' + partner.email + ')' : '');
- },
- content: function (item) {
- return $(self.document.createTextNode('@' + item.name + '\u00A0'));
- },
- },
- {
- className: 'fa',
- match: /:([\-+\w]+)$/,
- search: function () {},
- template: function () {
- return '\u200B';
- },
- content: function () {}
- },
- ];
- },
-});
-
-registry.add('hintPopover', null);
-
-return HintPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/history.js b/addons/web_editor/static/src/js/wysiwyg/plugin/history.js
deleted file mode 100644
index addf6383783..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/history.js
+++ /dev/null
@@ -1,87 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.history', function (require) {
-'use strict';
-
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-
-var HistoryPlugin = Plugins.history.extend({
- /**
- * Apply a snapshot.
- *
- * @override
- */
- applySnapshot: function () {
- this.context.invoke('MediaPlugin.hidePopovers');
- try {
- this._super.apply(this, arguments);
- } catch (e) {
- console.error(e);
- }
- this.context.invoke('editor.focus');
- var newRange = this.context.invoke('editor.createRange');
- if (!this.options.isEditableNode(newRange.sc)) {
- var firstEditableSel = '[contenteditable="true"]:not(":hidden")';
- var sc = this.$editable.find(firstEditableSel)[0];
- this.context.invoke('editor.setRange', sc, 0, sc, 0).select();
- }
- },
- /**
- * Clear the history.
- *
- * @override
- */
- clear: function () {
- this.stack = [];
- this.stackOffset = -1;
- this.recordUndo();
- },
- /**
- * Get the current history stack and the current offset.
- *
- * @returns {Object} {stack: Object, stackOffset: Integer}
- */
- getHistoryStep: function () {
- return {
- stack: this.stack,
- stackOffset: this.stackOffset,
- };
- },
- /**
- * Prevent errors with first snapshot.
- *
- * @override
- */
- makeSnapshot: function () {
- var rng = $.summernote.range.create(this.editable);
- var snapshot = this._super();
- if (rng.sc === this.editable || $(rng.sc).has(this.editable).length) {
- snapshot.bookmark.s.path = snapshot.bookmark.e.path = [0];
- snapshot.bookmark.s.offset = snapshot.bookmark.e.offset = 0;
- }
- return snapshot;
- },
- /**
- * @override
- */
- recordUndo: function () {
- if (!this.stack[this.stackOffset] || this.$editable.html() !== this.stack[this.stackOffset].contents) {
- this._super();
- }
- },
- /**
- * @override
- */
- undo: function () {
- if (this.stackOffset > 0) {
- this.stackOffset--;
- }
- this.applySnapshot(this.stack[this.stackOffset]);
- },
-});
-
-registry.add('HistoryPlugin', HistoryPlugin);
-
-return HistoryPlugin;
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js b/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
deleted file mode 100644
index 9ac05220e45..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/keyboard.js
+++ /dev/null
@@ -1,1237 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.keyboard', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var dom = $.summernote.dom;
-dom.isAnchor = function (node) {
- return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
- !$(node).hasClass('fa') && !$(node).hasClass('o_image');
-};
-var keycode = ({
- '8': 'BACKSPACE',
- '9': 'TAB',
- '13': 'ENTER',
- '46': 'DELETE',
-});
-
-var isSafari = /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
-var iPhone = /iPad|iPhone|iPod/.test(navigator.userAgent) && !window.MSStream;
-
-var KeyboardPlugin = AbstractPlugin.extend({
- events: iPhone ?
- {
- 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
- } :
- {
- 'summernote.keydown': '_onKeydown',
- 'DOMNodeInserted .note-editable': '_removeGarbageSpans',
- 'keypress .note-editable': '_onKeyPress',
- 'textInput .note-editable': '_onTextInput',
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Perform various DOM and range manipulations after a deletion:
- * - Rerange out of BR elements
- * - Clean the DOM at current range position
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @returns {Object} range
- */
- _afterDeletion: function (range, direction) {
- range = direction === 'prev' ? this._insertInvisibleCharAfterSingleBR(range) : range;
- range = this._rerangeOutOfBR(range, direction);
- range = this._cleanRangeAfterDeletion(range);
- return range;
- },
- /**
- * Perform operations that are necessary after the insertion of a visible character:
- * - Adapt range for the presence of zero-width characters
- * - Move out of media
- * - Rerange
- *
- * @private
- */
- _afterVisibleChar: function () {
- var range = this.context.invoke('editor.createRange');
- if (range.sc.tagName || dom.ancestor(range.sc, dom.isAnchor)) {
- return true;
- }
- var needReselect = false;
- var fake = range.sc.parentNode;
- if ((fake.className || '').indexOf('o_fake_editable') !== -1 && dom.isMedia(fake)) {
- var $media = $(fake.parentNode);
- $media[fake.previousElementSibling ? 'after' : 'before'](fake.firstChild);
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so - 2, range.so - 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so - 2) + range.sc.textContent.slice(range.so - 1);
- range.so = range.eo = range.so - 1;
- needReselect = true;
- }
- if (range.sc.textContent.slice(range.so, range.so + 1) === '\u200B') {
- range.sc.textContent = range.sc.textContent.slice(0, range.so) + range.sc.textContent.slice(range.so + 1);
- needReselect = true;
- }
- if (needReselect) {
- range.normalize().select();
- }
- },
- /**
- * Perform various DOM and range manipulations to prepare a deletion:
- * - Rerange within the element targeted by the range
- * - Slice the text content if necessary
- * - Move before an invisible BR if necessary
- * - Replace a media with an empty SPAN if necessary
- * - Change the direction of deletion if necessary
- * - Clean the DOM at range position if necessary
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @param {Boolean} didDeleteNodes true if nodes were already deleted prior to this call
- * @returns {Object} {didDeleteNodes: Boolean, range: Object, direction: String('prev'|next')}
- */
- _beforeDeletion: function (range, direction, didDeleteNodes) {
- var res = {
- range: range,
- direction: direction,
- didDeleteNodes: didDeleteNodes,
- };
-
- res.range = this._rerangeToOffsetChild(res.range, direction);
- res.range = this._sliceAndRerangeBeforeDeletion(res.range);
- res.range = direction === 'prev' ? this._moveBeforeInvisibleBR(res.range) : res.range;
-
- if (dom.isMedia(res.range.sc)) {
- var span = this._replaceMediaWithEmptySpan(res.range.sc);
- res.range = this.context.invoke('editor.setRange', span, 0);
- res.didDeleteNodes = true;
- return res;
- }
-
- if (res.didDeleteNodes) {
- res.direction = 'next';
- return res;
- }
-
- res.range = this._cleanRangeBeforeDeletion(res.range, direction);
-
- return res;
- },
- /**
- * Clean the DOM at range position after a deletion:
- * - Remove empty inline nodes
- * - Fill the current node if it's empty
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _cleanRangeAfterDeletion: function (range) {
- var point = range.getStartPoint();
-
- point = this.context.invoke('HelperPlugin.removeEmptyInlineNodes', point);
- point = this.context.invoke('HelperPlugin.fillEmptyNode', point);
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- return range;
- },
- /**
- * Clean the DOM at range position:
- * - Remove all previous zero-width characters
- * - Remove leading/trailing breakable space
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @returns {Object} range
- */
- _cleanRangeBeforeDeletion: function (range, direction) {
- if (direction === 'prev') {
- this._removeAllPreviousInvisibleChars(range);
- }
- range = this._removeExtremeBreakableSpaceAndRerange(range);
- return range;
- },
- /**
- * Get information on the range in order to perform a deletion:
- * - The point at which to delete, if any
- * - Whether the node contains a block
- * - The block to remove, if any
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @param {Boolean} wasOnStartOfBR true if the requested deletion started at
- * the beginning of a BR element
- * @returns {Object} {
- * point: {false|Object},
- * hasBlock: {Boolean},
- * blockToRemove: {false|Node},
- * }
- */
- _getDeleteInfo: function (range, direction, wasOnStartOfBR) {
- var self = this;
- var hasBlock = false;
- var blockToRemove = false;
- var method = direction === 'prev' ? 'prevPointUntil' : 'nextPointUntil';
-
- var pt = range.getStartPoint();
- pt = dom[method](pt, function (point) {
- var isAtStartOfMedia = !point.offset && dom.isMedia(point.node);
- var isBRorHR = point.node.tagName === 'BR' || point.node.tagName === 'HR';
- var isRootBR = wasOnStartOfBR && point.node === range.sc;
- var isOnRange = range.ec === point.node && range.eo === point.offset;
-
- if (!point.offset && self.context.invoke('HelperPlugin.isNodeBlockType', point.node)) {
- hasBlock = true;
- if (blockToRemove) {
- return true;
- }
- }
-
- if (!blockToRemove && (isAtStartOfMedia || isBRorHR && !isRootBR)) {
- blockToRemove = point.node;
- return false;
- }
-
- if (isOnRange) {
- return false;
- }
-
- return self._isDeletableNode(point.node);
- });
-
- return {
- point: pt || false,
- hasBlock: hasBlock,
- blockToRemove: blockToRemove,
- };
- },
- /**
- * Handle deletion (BACKSPACE / DELETE).
- *
- * @private
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @returns {Boolean} true if case handled
- */
- _handleDeletion: function (direction) {
- var range = this.context.invoke('editor.createRange');
- range = direction === 'prev' && this._replaceEmptyParentWithEmptyP(range);
- if (range) {
- range = this._afterDeletion(range, direction);
- range.select();
- return true;
- }
- var didDeleteNodes = this.context.invoke('HelperPlugin.deleteSelection');
- range = this.context.invoke('editor.createRange');
- var wasOnStartOfBR = direction === 'prev' && !range.so && range.sc.tagName === 'BR';
-
- this._removeNextEmptyUnbreakable(range.sc);
- var temp = this._beforeDeletion(range, direction, didDeleteNodes);
- didDeleteNodes = temp.didDeleteNodes;
- range = temp.range;
- direction = temp.direction;
-
- if (!didDeleteNodes) {
- var newRange = this._performDeletion(range, direction, wasOnStartOfBR);
- didDeleteNodes = newRange.so !== range.so || newRange.sc !== range.sc;
- range = newRange;
- }
-
- range = this._afterDeletion(range, direction);
-
- range = range.collapse(direction === 'prev').select();
- this.editable.normalize();
- return didDeleteNodes;
- },
- /**
- * Handle ENTER.
- *
- * @private
- * @returns {Boolean} true if case handled
- */
- _handleEnter: function () {
- var self = this;
- var range = this.context.invoke('editor.createRange');
-
- var ancestor = dom.ancestor(range.sc, function (node) {
- return dom.isLi(node) || self.options.isUnbreakableNode(node.parentNode) && node.parentNode !== self.editable ||
- self.context.invoke('HelperPlugin.isNodeBlockType', node) && !dom.ancestor(node, dom.isLi);
- });
-
- if (
- dom.isLi(ancestor) && !$(ancestor.parentNode).hasClass('list-group') &&
- this.context.invoke('HelperPlugin.getRegexBlank', {
- space: true,
- newline: true,
- }).test(ancestor.textContent) &&
- $(ancestor).find('br').length <= 1 &&
- !$(ancestor).find('.fa, img').length
- ) {
- // double enter in a list make oudent
- this.context.invoke('BulletPlugin.outdent');
- return true;
- }
-
- var btn = dom.ancestor(range.sc, function (n) {
- return $(n).hasClass('btn');
- });
-
- var point = range.getStartPoint();
-
- if (!point.node.tagName && this.options.isUnbreakableNode(point.node.parentNode)) {
- return this._handleShiftEnter();
- }
-
- if (point.node.tagName && point.node.childNodes[point.offset] && point.node.childNodes[point.offset].tagName === "BR") {
- point = dom.nextPoint(point);
- }
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
-
- var next = this.context.invoke('HelperPlugin.splitTree', ancestor, point, {
- isSkipPaddingBlankHTML: !this.context.invoke('HelperPlugin.isNodeBlockType', point.node.parentNode) && !!point.node.parentNode.nextSibling
- });
- $(next).removeClass('o_checked');
- while (next.firstChild) {
- next = next.firstChild;
- }
-
- // if there is no block in the split parents, then we add a br between the two node
- var hasSplitBlock = false;
- var node = next;
- var lastChecked = node;
- while (node && node !== ancestor && node !== this.editable) {
- if (this.context.invoke('HelperPlugin.isNodeBlockType', node)) {
- hasSplitBlock = true;
- break;
- }
- lastChecked = node;
- node = node.parentNode;
- }
- if (!hasSplitBlock && lastChecked.tagName) {
- $(lastChecked).before(this.document.createElement('br'));
- }
-
- if (!next.tagName) {
- this.context.invoke('HelperPlugin.secureExtremeSingleSpace', next);
- }
- if (next.tagName !== "BR" && next.innerHTML === "") {
- next.innerHTML = '\u200B';
- }
- if (ancestor) {
- var firstChild = this.context.invoke('HelperPlugin.firstLeaf', ancestor);
- var lastChild = this.context.invoke('HelperPlugin.lastLeaf', ancestor);
- if (this.context.invoke('HelperPlugin.isBlankNode', ancestor)) {
- firstChild = dom.isText(firstChild) ? firstChild.parentNode : firstChild;
- $(firstChild).contents().remove();
- $(firstChild).append(this.document.createElement('br'));
- }
- if (lastChild.tagName === 'BR' && lastChild.previousSibling) {
- $(lastChild).after(this.document.createTextNode('\u200B'));
- }
- }
-
- // move to next editable area
- point = this.context.invoke('HelperPlugin.makePoint', next, 0);
- if (
- (point.node.tagName && point.node.tagName !== 'BR') ||
- !this.context.invoke('HelperPlugin.isVisibleText', point.node.textContent)
- ) {
- point = dom.nextPointUntil(point, function (pt) {
- if (pt.node === point.node) {
- return;
- }
- return (
- pt.node.tagName === "BR" ||
- self.context.invoke('HelperPlugin.isVisibleText', pt.node)
- ) &&
- self.options.isEditableNode(pt.node);
- });
- point = point || this.context.invoke('HelperPlugin.makePoint', next, 0);
- if (point.node.tagName === "BR") {
- point = dom.nextPoint(point);
- }
- }
-
- if (!hasSplitBlock && !point.node.tagName) {
- point.node.textContent = '\u200B' + point.node.textContent;
- point.offset = 1;
- }
-
- // if the left part of the split node ends with a space, replace that space with nbsp
- if (range.sc.textContent) {
- var endSpace = this.context.invoke('HelperPlugin.getRegex', 'endSpace');
- range.sc.textContent = range.sc.textContent.replace(endSpace,
- function (trailingSpaces) {
- return Array(trailingSpaces.length + 1).join('\u00A0');
- }
- );
- }
-
- // On buttons, we want to split the button and move to the beginning of it
- if (btn) {
- next = dom.ancestor(point.node, function (n) {
- return $(n).hasClass('btn');
- });
-
- // Force content in empty buttons, the carret can be moved there
- this.context.invoke('LinkPopover.hide');
- this.context.invoke('LinkPopover.fillEmptyLink', next, true);
- this.context.invoke('LinkPopover.fillEmptyLink', btn, true);
-
- // Move carret to the new button
- range = this.context.invoke('editor.setRange', next.firstChild, 0);
- range.select();
- } else {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- range.normalize().select();
- }
-
- return true;
- },
- /**
- * Handle SHIFT+ENTER.
- *
- * @private
- * @returns {Boolean} true if case handled
- */
- _handleShiftEnter: function () {
- var range = this.context.invoke('editor.createRange');
- var target = range.sc.childNodes[range.so] || range.sc;
- var before;
- if (target.tagName) {
- if (target.tagName === "BR") {
- before = target;
- } else if (target === range.sc) {
- if (range.so) {
- before = range.sc.childNodes[range.so - 1];
- } else {
- before = this.document.createTextNode('');
- $(range.sc).append(before);
- }
- }
- } else {
- before = target;
- var after = target.splitText(target === range.sc ? range.so : 0);
- if (
- !after.nextSibling && after.textContent === '' &&
- this.context.invoke('HelperPlugin.isNodeBlockType', after.parentNode)
- ) {
- after.textContent = '\u200B';
- }
- if (!after.tagName && (!after.previousSibling || after.previousSibling.tagName === "BR")) {
- after.textContent = after.textContent.replace(startSpace, '\u00A0');
- }
- }
-
- if (!before) {
- return true;
- }
-
- var br = this.document.createElement('br');
- $(before).after(br);
- var next = this.context.invoke('HelperPlugin.makePoint', br, 0);
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
-
- if (!before.tagName) {
- next = dom.nextPoint(next);
- var nextNode = this.context.invoke('HelperPlugin.firstLeaf', next.node.childNodes[next.offset] || next.node);
- if (!nextNode.tagName) {
- next.node = nextNode;
- next.offset = 0;
- }
- }
-
- if (
- next.node.tagName === "BR" && next.node.nextSibling &&
- !next.node.nextSibling.tagName && !dom.ancestor(next.node, dom.isPre)
- ) {
- next.node.nextSibling.textContent = next.node.nextSibling.textContent.replace(startSpace, '\u00A0');
- }
- if (
- !next.node.tagName &&
- (!next.node.previousSibling || next.node.previousSibling.tagName === "BR") &&
- !dom.ancestor(next.node, dom.isPre)
- ) {
- next.node.textContent = next.node.textContent.replace(startSpace, '\u00A0');
- }
-
- range = this.context.invoke('editor.setRange', next.node, next.offset);
- range.select();
-
- return true;
- },
- /**
- * Insert a zero-width character after a BR if the range is
- * at the beginning of an invisible text node
- * and after said single BR element.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _insertInvisibleCharAfterSingleBR: function (range) {
- if (this._isAtStartOfInvisibleText(range) && this._isAfterSingleBR(range.sc)) {
- var invisibleChar = this.document.createTextNode('\u200B');
- $(range.sc.previousSibling).after(invisibleChar);
- range = this.context.invoke('editor.setRange', invisibleChar, 1);
- }
- return range;
- },
- /**
- * Return true if the node comes after a BR element.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterBR: function (node) {
- return node.previousSibling && node.previousSibling.tagName === 'BR';
- },
- /**
- * Return true if the range if positioned after a BR element that doesn't visually
- * show a new line in the DOM: a BR in an element that has only a BR, or text then a BR.
- * eg:
or
text
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterInvisibleBR: function (range) {
- return this._isAfterOnlyBR(range) || this._isAfterOnlyTextThenBR(range);
- },
- /**
- * Return true if the range is positioned on a text node, after an zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterInvisibleChar: function (range) {
- return !range.sc.tagName && range.so && range.sc.textContent[range.so - 1] === '\u200B';
- },
- /**
- * Return true if the range is positioned on a text node, after an leading zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterLeadingInvisibleChar: function (range) {
- return !range.sc.tagName && range.so === 1 && range.sc.textContent[0] === '\u200B';
- },
- /**
- * Return true if the range if positioned after a BR element in a node that has only a BR.
- * eg:
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterOnlyBR: function (range) {
- return this._hasOnlyBR(range.sc) && range.so === 1;
- },
- /**
- * Return true if the node has for only element child a BR element.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _hasOnlyBR: function (node) {
- return node.childElementCount === 1 && node.firstChild.tagName === 'BR';
- },
- /**
- * Return true if the range if positioned after a BR element in a node that has only text
- * and ends with a BR.
- * eg:
text
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAfterOnlyTextThenBR: function (range) {
- var hasTrailingBR = range.sc.lastChild && range.sc.lastChild.tagName === 'BR';
- if (!hasTrailingBR) {
- return false;
- }
- var hasOnlyTextThenBR = _.all(range.sc.childNodes, function (n) {
- return dom.isText(n) || n === range.sc.lastChild;
- });
- var isAfterTrailingBR = range.so === dom.nodeLength(range.sc);
- return hasOnlyTextThenBR && isAfterTrailingBR;
- },
- /**
- * Return true if the node is after a single BR.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterSingleBR: function (node) {
- var isPreviousAfterBR = node.previousSibling && this._isAfterBR(node.previousSibling);
- return this._isAfterBR(node) && !isPreviousAfterBR;
- },
- /**
- * Return true if the node comes after two BR elements.
- *
- * @private
- * @param {Node} node
- * @returns {Boolean}
- */
- _isAfterTwoBRs: function (node) {
- var isAfterBR = this._isAfterBR(node);
- var isPreviousSiblingAfterBR = node.previousSibling && this._isAfterBR(node.previousSibling);
- return isAfterBR && isPreviousSiblingAfterBR;
- },
- /**
- * Return true if the range is positioned at the start of an invisible text node.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isAtStartOfInvisibleText: function (range) {
- return !range.so && dom.isText(range.sc) && !this.context.invoke('HelperPlugin.isVisibleText', range.sc);
- },
- /**
- * Return true if the range is positioned on a text node, before a trailing zero-width character.
- *
- * @private
- * @param {Object} range
- * @returns {Boolean}
- */
- _isBeforeTrailingInvisibleChar: function (range) {
- var isBeforeLastCharOfText = !range.sc.tagName && range.so === dom.nodeLength(range.sc) - 1;
- var isLastCharInvisible = range.sc.textContent.slice(range.so) === '\u200B';
- return isBeforeLastCharOfText && isLastCharInvisible;
- },
- /**
- * Return true if the node is deletable.
- *
- * @private
- * @param {Node} node
- * @return {Boolean}
- */
- _isDeletableNode: function (node) {
- var isVisibleText = this.context.invoke('HelperPlugin.isVisibleText', node);
- var isMedia = dom.isMedia(node);
- var isBR = node.tagName === 'BR';
- var isEditable = this.options.isEditableNode(node);
- return isEditable && (isVisibleText || isMedia || isBR);
- },
- /**
- * Return true if the range is positioned on an edge to delete, depending on the given direction.
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- */
- _isOnEdgeToDelete: function (range, direction) {
- var isOnBR = range.sc.tagName === 'BR';
- var parentHasOnlyBR = range.sc.parentNode && range.sc.parentNode.innerHTML.trim() === " ";
- var isOnDirEdge;
- if (direction === 'next') {
- isOnDirEdge = range.so === dom.nodeLength(range.sc);
- } else {
- isOnDirEdge = range.so === 0;
- }
- return (!isOnBR || parentHasOnlyBR) && isOnDirEdge;
- },
- /**
- * Move the range before a BR if that BR doesn't visually show a new line in the DOM.
- * Return the new range.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _moveBeforeInvisibleBR: function (range) {
- if (this._isAfterInvisibleBR(range)) {
- range.so -= 1;
- }
- return range;
- },
- /**
- * Perform a deletion in the given direction.
- * Note: This is where the actual deletion takes place.
- * It should be preceded by _beforeDeletion and
- * followed by _afterDeletion.
- *
- * @see _handleDeletion
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction 'prev' to delete BEFORE the carret
- * @param {Boolean} wasOnStartOfBR true if the requested deletion started at
- * the beginning of a BR element
- * @returns {Object} range
- */
- _performDeletion: function (range, direction, wasOnStartOfBR) {
- var didDeleteNodes = false;
- if (this._isOnEdgeToDelete(range, direction)) {
- var rest = this.context.invoke('HelperPlugin.deleteEdge', range.sc, direction);
- didDeleteNodes = !!rest;
- if (didDeleteNodes) {
- range = this.context.invoke('editor.setRange', rest.node, rest.offset);
- return range;
- }
- }
-
- var deleteInfo = this._getDeleteInfo(range, direction, wasOnStartOfBR);
-
- if (!deleteInfo.point) {
- return range;
- }
-
- var point = deleteInfo.point;
- var blockToRemove = deleteInfo.blockToRemove;
- var hasBlock = deleteInfo.hasBlock;
-
- var isLonelyBR = blockToRemove && blockToRemove.tagName === 'BR' && this._hasOnlyBR(blockToRemove.parentNode);
- var isHR = blockToRemove && blockToRemove.tagName === "HR";
-
- if (blockToRemove && !isLonelyBR) {
- $(blockToRemove).remove();
- point = isHR ? this.context.invoke('HelperPlugin.deleteEdge', range.sc, direction) : point;
- didDeleteNodes = true;
- } else if (!hasBlock) {
- var isAtEndOfNode = point.offset === dom.nodeLength(point.node);
- var shouldMove = isAtEndOfNode || direction === 'next' && point.offset;
-
- point.offset = shouldMove ? point.offset - 1 : point.offset;
- point.node = this._removeCharAtOffset(point);
- didDeleteNodes = true;
-
- var isInPre = !!dom.ancestor(range.sc, dom.isPre);
- if (!isInPre) {
- this.context.invoke('HelperPlugin.secureExtremeSingleSpace', point.node);
- }
-
- if (direction === 'prev' && !point.offset && !this._isAfterBR(point.node)) {
- point.node = this._replaceLeadingSpaceWithSingleNBSP(point.node);
- }
- }
-
- if (didDeleteNodes) {
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- }
- return range;
- },
- /**
- * Prevent the appearance of a text node with the editable DIV as direct parent:
- * wrap it in a p element.
- *
- * @private
- */
- _preventTextInEditableDiv: function () {
- var range = this.context.invoke('editor.createRange');
- while (
- dom.isText(this.editable.firstChild) &&
- !this.context.invoke('HelperPlugin.isVisibleText', this.editable.firstChild)
- ) {
- var node = this.editable.firstChild;
- if (node && node.parentNode) {
- node.parentNode.removeChild(node);
- }
- }
- var editableIsEmpty = !this.editable.childNodes.length;
- if (editableIsEmpty) {
- var p = this.document.createElement('p');
- p.innerHTML = ' ';
- this.editable.appendChild(p);
- range = this.context.invoke('editor.setRange', p, 0);
- } else if (this.context.invoke('HelperPlugin.isBlankNode', this.editable.firstChild) && !range.sc.parentNode) {
- this.editable.firstChild.innerHTML = ' ';
- range = this.context.invoke('editor.setRange', this.editable.firstChild, 0);
- }
-
- range.select();
- },
- /**
- * Remove all invisible chars before the current range, that are adjacent to it,
- * then rerange.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _removeAllPreviousInvisibleChars: function (range) {
- while (this._isAfterInvisibleChar(range)) {
- var text = range.sc.textContent;
- range.sc.textContent = text.slice(0, range.so - 1) + text.slice(range.so, text.length);
- range.so -= 1;
- }
- return range;
- },
- /**
- * Remove a char from a point's text node, at the point's offset.
- *
- * @private
- * @param {Object} point
- * @returns {Node}
- */
- _removeCharAtOffset: function (point) {
- var text = point.node.textContent;
- var startToOffset = text.slice(0, point.offset);
- var offsetToEnd = text.slice(point.offset + 1);
- point.node.textContent = startToOffset + offsetToEnd;
- return point.node;
- },
- /**
- * Remove any amount of leading/trailing breakable space at range position.
- * Then move the range and return it.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _removeExtremeBreakableSpaceAndRerange: function (range) {
- var isInPre = !!dom.ancestor(range.sc, dom.isPre);
- if (!range.sc.tagName && !isInPre) {
- var changed = this.context.invoke('HelperPlugin.removeExtremeBreakableSpace', range.sc);
- range.so = range.eo = range.so > changed.start ? range.so - changed.start : 0;
- range.so = range.eo = range.so > dom.nodeLength(range.sc) ? dom.nodeLength(range.sc) : range.so;
- range.select();
- this.context.invoke('editor.saveRange');
- }
- return range;
- },
- /**
- * Patch for Google Chrome's contenteditable SPAN bug.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _removeGarbageSpans: function (e) {
- if (e.target.className === "" && e.target.tagName == "SPAN" &&
- e.target.style.fontStyle === "inherit" &&
- e.target.style.fontVariantLigatures === "inherit" &&
- e.target.style.fontVariantCaps === "inherit") {
- var $span = $(e.target);
- $span.after($span.contents()).remove();
- }
- },
- /**
- * Remove the first unbreakable ancestor's next sibling if not a block and empty.
- *
- * @private
- * @param {Node} node
- */
- _removeNextEmptyUnbreakable: function (node) {
- var self = this;
- var unbreakable = dom.ancestor(node, this.options.isUnbreakableNode);
- if (unbreakable === this.editable) {
- return;
- }
- var nextUnbreakable = unbreakable && unbreakable.nextElementSibling;
- var isNextEmpty = nextUnbreakable && dom.isEmpty(nextUnbreakable) && !dom.isVoid(nextUnbreakable);
- var isNextContainsOnlyInvisibleText = nextUnbreakable && _.all($(nextUnbreakable).contents(), function (n) {
- return dom.isText(n) && !self.context.invoke('HelperPlugin.isVisibleText', n);
- });
- if (!dom.isBlock(nextUnbreakable) && (isNextEmpty || isNextContainsOnlyInvisibleText)) {
- $(nextUnbreakable).remove();
- }
- },
- /**
- * If the range's start container is empty and constitutes the only contents of its parent,
- * replace it with an empty p, then rerange.
- *
- * @private
- * @param {Object} range
- * @returns {Object|undefined} range
- */
- _replaceEmptyParentWithEmptyP: function (range) {
- var node = range.sc.childElementCount === 1 && range.sc.firstChild.tagName === 'BR' ? range.sc.firstChild : range.sc;
- if (node === this.editable || !node.parentNode || node.parentNode === this.editable) {
- return;
- }
- if (
- dom.isEmpty(node) &&
- this.context.invoke('HelperPlugin.onlyContains', node.parentNode, node) &&
- ['LI', 'P'].indexOf(node.parentNode.tagName) === -1
- ) {
- var emptyP = this.document.createElement('p');
- var br = this.document.createElement('br');
- $(emptyP).append(br);
- $(node.parentNode).before(emptyP).remove();
- range.sc = range.ec = br;
- range.so = range.eo = 0;
- return range.collapse(true);
- }
- return;
- },
- /**
- * Replace all leading space from a text node with one non-breakable space.
- *
- * @param {Node} node
- * @returns {Node} node
- */
- _replaceLeadingSpaceWithSingleNBSP: function (node) {
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
- node.textContent = node.textContent.replace(startSpace, '\u00A0');
- return node;
- },
- /**
- * Replace a media node with an empty SPAN and return that SPAN.
- *
- * @param {Node} media
- * @returns {Node} span
- */
- _replaceMediaWithEmptySpan: function (media) {
- var span = this.document.createElement('span');
- media = dom.ancestor(media, function (n) {
- return !n.parentNode || !dom.isMedia(n.parentNode);
- });
- $(media).replaceWith(span);
- return span;
- },
- /**
- * Move the (collapsed) range to get out of BR elements.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _rerangeOutOfBR: function (range, direction) {
- range = this._rerangeToFirstNonBRElementLeaf(range);
- range = this._rerangeToNextNonBR(range, direction === 'next');
- return range;
- },
- /**
- * Move the (collapsed) range to the first leaf that is not a BR element.
- *
- * @private
- * @param {Object} range
- * @returns {Object} range
- */
- _rerangeToFirstNonBRElementLeaf: function (range) {
- var leaf = this.context.invoke('HelperPlugin.firstNonBRElementLeaf', range.sc);
- if (leaf !== range.sc) {
- range = this.context.invoke('editor.setRange', leaf, 0);
- }
- return range;
- },
- /**
- * Move the (collapsed) range to the next (or previous) node that is not a BR element.
- *
- * @private
- * @param {Object} range
- * @param {Boolean} previous true to move to the previous node
- * @returns {Object} range
- */
- _rerangeToNextNonBR: function (range, previous) {
- var point = range.getStartPoint();
- var method = previous ? 'prevPointUntil' : 'nextPointUntil';
- point = dom[method](point, function (pt) {
- return pt.node.tagName !== 'BR';
- });
- range = this.context.invoke('editor.setRange', point.node, point.offset);
- return range;
- },
- /**
- * Move the (collapsed) range to the child of the node at the current offset if possible.
- *
- * @private
- * @param {Object} range
- * @param {String('prev'|'next')} direction
- * @returns {Object} range
- */
- _rerangeToOffsetChild: function (range, direction) {
- if (range.sc.childNodes[range.so]) {
- var node;
- var offset;
- if (direction === 'prev' && range.so > 0) {
- node = range.sc.childNodes[range.so - 1];
- offset = dom.nodeLength(node);
- range = this.context.invoke('editor.setRange', node, offset);
- } else {
- node = range.sc.childNodes[range.so];
- offset = 0;
- range = this.context.invoke('editor.setRange', node, offset);
- }
- }
- return range;
- },
- /**
- * Select all the contents of the current unbreakable ancestor.
- */
- _selectAll: function () {
- var self = this;
- var range = this.context.invoke('editor.createRange');
- var unbreakable = dom.ancestor(range.sc, this.options.isUnbreakableNode);
- var $contents = $(unbreakable).contents();
- var startNode = $contents.length ? $contents[0] : unbreakable;
- var pointA = this.context.invoke('HelperPlugin.makePoint', startNode, 0);
- pointA = dom.nextPointUntil(pointA, function (point) {
- return self.context.invoke('HelperPlugin.isVisibleText', point.node);
- }) || pointA;
- var endNode = $contents.length ? $contents[$contents.length - 1] : unbreakable;
- var endOffset = $contents.length ? dom.nodeLength($contents[$contents.length - 1]) : 1;
- var pointB = this.context.invoke('HelperPlugin.makePoint', endNode, endOffset);
- pointB = dom.prevPointUntil(pointB, function (point) {
- return self.context.invoke('HelperPlugin.isVisibleText', point.node);
- }) || pointB;
- range.sc = pointA.node;
- range.so = pointA.offset;
- range.ec = pointB.node;
- range.eo = pointB.offset;
- range.select().normalize();
- },
- /**
- * Before a deletion, if necessary, slice the text content at range, then rerange.
- *
- * @param {Object} range
- * @returns {Object} range
- */
- _sliceAndRerangeBeforeDeletion: function (range) {
- if (this._isAfterLeadingInvisibleChar(range) && !this._isAfterTwoBRs(range.sc)) {
- range.sc.textContent = range.sc.textContent.slice(1);
- range.so = 0;
- }
- if (this._isBeforeTrailingInvisibleChar(range) && !this._isAfterBR(range.sc)) {
- range.sc.textContent = range.sc.textContent.slice(0, range.so);
- }
- return range;
- },
-
-
- //--------------------------------------------------------------------------
- // Handle
- //--------------------------------------------------------------------------
-
- /**
- * Customize handling of certain keydown events.
- *
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case handled
- */
- _onKeydown: function (se, e) {
- if (e.ctrlKey && e.key === 'a') {
- e.preventDefault();
- this._selectAll();
- return;
- }
- if (!keycode[e.keyCode]) {
- return;
- }
-
- this.context.invoke('editor.clearTarget');
- this.context.invoke('MediaPlugin.hidePopovers');
- this.context.invoke('editor.beforeCommand');
- var handled = false;
- switch (keycode[e.keyCode]) {
- case 'BACKSPACE':
- handled = this._onBackspace(e);
- break;
- case 'TAB':
- handled = this._onTab(e);
- break;
- case 'ENTER':
- handled = this._onEnter(e);
- break;
- case 'DELETE':
- handled = this._onDelete(e);
- break;
- }
- if (handled) {
- e.preventDefault();
- this._preventTextInEditableDiv();
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.afterCommand');
- }
- return handled;
- },
- /**
- * Handle BACKSPACE keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onBackspace: function (e) {
- var range = this.context.invoke('editor.createRange');
- var needOutdent = false;
-
- // Special cases
- if (range.isCollapsed()) {
-
- // Do nothing if on left edge of a table cell
- var point = range.getStartPoint();
- if (point.node.childNodes[point.offset]) {
- point.node = point.node.childNodes[point.offset];
- point.offset = dom.nodeLength(point.node);
- }
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfTag', point, 'TD')) {
- return true;
- }
-
- // Outdent if on left edge of an indented block
- point = range.getStartPoint();
- var isIndented = !!dom.ancestor(point.node, function (n) {
- var style = dom.isCell(n) ? 'paddingLeft' : 'marginLeft';
- return n.tagName && !!parseFloat(n.style[style] || 0);
- });
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfBlock', point)) {
- if (isIndented) {
- this.context.invoke('BulletPlugin.outdent');
- return true;
- }
- if (dom.ancestor(range.sc, dom.isLi)) {
- needOutdent = true;
- }
- }
- }
-
- var flag = this._handleDeletion('prev');
-
- if (!flag && needOutdent) {
- range.select();
- this.context.invoke('BulletPlugin.outdent');
- }
-
- return true;
- },
- /**
- * Handle DELETE keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onDelete: function (e) {
- var range = this.context.invoke('editor.createRange');
-
- // Special case
- if (range.isCollapsed()) {
- // Do nothing if on left edge of a table cell
- if (this.context.invoke('HelperPlugin.isRightEdgeOfTag', range.getStartPoint(), 'TD')) {
- return true;
- }
- }
-
- this._handleDeletion('next');
- return true;
- },
- /**
- * Handle ENTER keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onEnter: function (e) {
- this.context.invoke('HelperPlugin.deleteSelection');
- if (e.shiftKey) {
- this._handleShiftEnter();
- } else if (e.ctrlKey) {
- this.context.invoke('TextPlugin.insertHR');
- } else {
- this._handleEnter();
- }
- return true;
- },
- /**
- * Handle TAB keydown event.
- *
- * @private
- * @param {jQueryEvent} e
- * @returns {Boolean} true if case is handled and event default must be prevented
- */
- _onTab: function (e) {
- // If TAB not handled, prevent default and do nothing
- if (!this.options.keyMap.pc.TAB) {
- this.trigger_up('wysiwyg_blur', {
- key: 'TAB',
- keyCode: 9,
- shiftKey: e.shiftKey,
- });
- return true;
- }
- var range = this.context.invoke('editor.createRange');
- var point = range.getStartPoint();
- var startSpace = this.context.invoke('HelperPlugin.getRegex', 'startSpace');
-
- if (!range.isOnCell()) {
- // If on left edge point: indent/outdent
- if (!point.node.tagName) { // Clean up start spaces on textNode
- point.node.textContent.replace(startSpace, function (startSpaces) {
- point.offset = startSpaces.length === point.offset ? 0 : point.offset;
- return '';
- });
- }
- if (this.context.invoke('HelperPlugin.isLeftEdgeOfBlock', point) || dom.isEmpty(point.node)) {
- if (e.shiftKey) {
- this.context.invoke('BulletPlugin.outdent');
- } else {
- this.context.invoke('BulletPlugin.indent');
- }
- this.context.invoke('HelperPlugin.normalize');
- return true;
- }
- // Otherwise insert a tab or do nothing
- if (!e.shiftKey) {
- this.context.invoke('TextPlugin.insertTab');
- this.context.invoke('HelperPlugin.normalize');
- }
- return true;
- }
- // In table, on tab switch to next cell
- return false;
- },
- /**
- * Handle char input.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _onKeyPress: function (e) {
- console.log(e.originalEvent);
- if (!isSafari || iPhone) {
- e.preventDefault();
- this.context.invoke('HelperPlugin.insertTextInline', e.key);
- }
- },
- /**
- * Handle char input.
- *
- * @private
- * @param {jQueryEvent} e
- */
- _onTextInput: function (e) {
- if (isSafari && !iPhone) {
- return;
- }
- e.originalEvent.preventDefault();
-
- var text = e.originalEvent.data;
- if (!text) {
- return;
- }
- if (text === 'Spacebar') {
- text = ' ';
- }
- this.context.invoke('HelperPlugin.insertTextInline', text);
- },
-});
-
-registry.add('KeyboardPlugin', KeyboardPlugin);
-
-return KeyboardPlugin;
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/link.js b/addons/web_editor/static/src/js/wysiwyg/plugin/link.js
deleted file mode 100644
index d8d261df980..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/link.js
+++ /dev/null
@@ -1,493 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.link', function (require) {
-'use strict';
-
-var core = require('web.core');
-var LinkDialog = require('wysiwyg.widgets.LinkDialog');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-
-var _t = core._t;
-var dom = $.summernote.dom;
-dom.isAnchor = function (node) {
- return (node.tagName === 'A' || node.tagName === 'BUTTON' || $(node).hasClass('btn')) &&
- !$(node).hasClass('fa') && !$(node).hasClass('o_image');
-};
-
-//--------------------------------------------------------------------------
-// link
-//--------------------------------------------------------------------------
-
-var LinkPlugin = Plugins.linkDialog.extend({
- events: {
- 'dblclick .note-editable': '_onDblclick',
- },
-
- //--------------------------------------------------------------------------
- // Public summernote module API
- //--------------------------------------------------------------------------
-
- /**
- * @param {Object} layoutInfo
- */
- show: function () {
- var self = this;
- var linkInfo = this.context.invoke('editor.getLinkInfo');
- this.context.invoke('editor.saveRange');
- return this.showLinkDialog(linkInfo)
- .then(function (linkInfo) {})
- .guardedCatch(function () {});
- },
- /**
- * @override
- * @param {Object} linkInfo
- * @returns {Promise}
- */
- showLinkDialog: function (linkInfo) {
- var self = this;
- this.context.invoke('editor.hidePopover');
- var media = this.context.invoke('editor.restoreTarget');
-
- if (linkInfo.range) {
- var r = linkInfo.range.getPoints();
-
- // move carret in icon, video...
- if (!media && (dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon))) {
- media = dom.ancestor(r.sc, dom.isImg) || dom.ancestor(r.sc, dom.isIcon);
- }
- // if select a text content in anchor then click on image then click on anchor button
- if (dom.isImg(media) || dom.isIcon(media)) {
- r.sc = media.parentNode;
- r.so = [].indexOf.call(media.parentNode.childNodes, media);
- r.ec = media;
- r.eo = dom.nodeLength(media);
-
- linkInfo.range.sc = r.sc;
- linkInfo.range.so = r.so;
- linkInfo.range.ec = r.ec;
- linkInfo.range.eo = r.eo;
- }
-
- linkInfo.isAnchor = linkInfo.range.isOnAnchor();
- linkInfo.className = linkInfo.isAnchor ? dom.ancestor(r.sc, dom.isAnchor).className : '';
- linkInfo.url = linkInfo.isAnchor ? ($(dom.ancestor(r.sc, dom.isAnchor)).attr('href') || '').replace(window.location.origin, '') : '';
-
- var nodes = [];
- if (linkInfo.isAnchor) {
- nodes = dom.ancestor(r.sc, dom.isAnchor).childNodes;
- } else if (!linkInfo.range.isCollapsed()) {
- if (dom.isImg(media) || dom.isIcon(media)) {
- nodes.push(media);
- } else {
- if (r.sc.tagName) {
- r.sc = (r.so ? r.sc.childNodes[r.so] : r.sc).firstChild || r.sc;
- r.so = 0;
- } else if (r.so !== r.sc.textContent.length) {
- if (r.sc === r.ec) {
- r.ec = r.sc = r.sc.splitText(r.so);
- r.eo -= r.so;
- } else {
- r.sc = r.sc.splitText(r.so);
- }
- r.so = 0;
- }
- if (r.ec.tagName) {
- r.ec = (r.eo ? r.ec.childNodes[r.eo - 1] : r.ec).lastChild || r.ec;
- r.eo = r.ec.textContent.length;
- } else if (r.eo !== r.ec.textContent.length) {
- r.ec.splitText(r.eo);
- }
-
- // browsers can't target a picture or void node
- if (dom.isVoid(r.sc) || dom.isImg(r.sc)) {
- r.so = dom.listPrev(r.sc).length - 1;
- r.sc = r.sc.parentNode;
- }
- if (dom.isBR(r.ec)) {
- r.eo = dom.listPrev(r.ec).length - 1;
- r.ec = r.ec.parentNode;
- } else if (dom.isVoid(r.ec) || dom.isImg(r.sc)) {
- r.eo = dom.listPrev(r.ec).length;
- r.ec = r.ec.parentNode;
- }
- linkInfo.range.sc = r.sc;
- linkInfo.range.so = r.so;
- linkInfo.range.ec = r.ec;
- linkInfo.range.eo = r.eo;
- linkInfo.range.select();
- this.context.invoke('editor.saveRange');
- linkInfo.range = this.context.invoke('editor.createRange');
-
- // search nodes to insert in the anchor
-
- var startPoint = this.context.invoke('HelperPlugin.makePoint', r.sc, r.so);
- var endPoint = this.context.invoke('HelperPlugin.makePoint', r.ec, r.eo);
- dom.walkPoint(startPoint, endPoint, function (point) {
- var node = point.node.childNodes && point.node.childNodes[point.offset] || point.node;
- nodes.push(node);
- });
-
- nodes = _.filter(_.uniq(nodes), function (node) {
- return nodes.indexOf(node.parentNode) === -1;
- });
- }
- }
-
- if (nodes.length > 0) {
- var text = "";
- linkInfo.images = [];
- for (var i = 0; i < nodes.length; i++) {
- if (dom.ancestor(nodes[i], dom.isImg)) {
- text += dom.ancestor(nodes[i], dom.isImg).outerHTML;
- } else if (dom.ancestor(nodes[i], dom.isIcon)) {
- text += dom.ancestor(nodes[i], dom.isIcon).outerHTML;
- } else if (!linkInfo.isAnchor && nodes[i].nodeType === 1) {
- // just use text nodes from listBetween
- } else if (!linkInfo.isAnchor && i === 0) {
- text += nodes[i].textContent;
- } else if (!linkInfo.isAnchor && i === nodes.length - 1) {
- text += nodes[i].textContent;
- } else {
- text += nodes[i].textContent;
- }
- }
- linkInfo.text = text.replace(this.context.invoke('HelperPlugin.getRegex', 'space', 'g'), ' ');
- }
-
- linkInfo.needLabel = !linkInfo.text.length;
- }
-
- var def = new Promise(function (resolve, reject) {
- var linkDialog = new LinkDialog(self.options.parent, {
- onClose: function () {
- setTimeout(function () {
- self.context.invoke('editor.focus');
- });
- }
- },
- _.omit(linkInfo, 'range')
- );
-
- linkDialog.on('save', self, self._wrapCommand(function (newLinkInfo) {
- var isCollapsed = linkInfo.range.isCollapsed();
- linkInfo.range.select();
- newLinkInfo.range = linkInfo.range;
-
- var $anchor;
- if (linkInfo.isAnchor) {
- $anchor = $(dom.ancestor(r.sc, dom.isAnchor));
- $anchor.css(newLinkInfo.style || {});
- if (newLinkInfo.isNewWindow) {
- $anchor.attr('target', '_blank');
- } else {
- $anchor.removeAttr('target');
- }
- } else {
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.createLink', newLinkInfo);
- var range = self.context.invoke('editor.createRange');
- var anchor = dom.ancestor(range.sc.childNodes[range.so] || range.sc, dom.isAnchor);
- $anchor = $(anchor);
- if (isCollapsed) {
- // move the range just after the link
- var point = dom.nextPoint({
- node: anchor,
- offset: dom.nodeLength(anchor),
- });
- range = self.context.invoke('editor.setRange', point.node, point.offset);
- range.select();
- } else {
- $anchor.selectContent();
- }
- }
- if ((dom.isImg(media) || dom.isIcon(media)) && !$anchor.find(media).length) {
- $(media).remove();
- }
- $anchor.attr('class', newLinkInfo.className);
- $anchor.attr('href', newLinkInfo.url);
- self.context.invoke('editor.saveRange');
- self.context.invoke('editor.saveTarget', $anchor[0]);
- self.context.triggerEvent('focusnode', $anchor[0]);
- resolve(linkInfo);
- }));
- linkDialog.on('closed', self, function () {
- this.context.invoke('editor.restoreRange');
- this.context.invoke('LinkPopover.update');
- reject();
- });
- linkDialog.open();
- });
- return def;
- },
- /**
- * Remove the current link, keep its contents.
- *
- * @override
- */
- unlink: function () {
- var rng = this.context.invoke('editor.createRange');
- var anchor = rng.sc;
- while (anchor && anchor.tagName !== 'A') {
- anchor = anchor.parentElement;
- }
- if (!anchor) {
- this.context.invoke('editor.hidePopover');
- return;
- }
- var startAndEndInvisible = this.context.invoke('HelperPlugin.getRegex', 'startAndEndInvisible', 'g');
- anchor.innerHTML = anchor.innerHTML.replace(startAndEndInvisible, '');
- var $contents = $(anchor).contents();
- $(anchor).before($contents).remove();
-
- this.context.invoke('editor.hidePopover');
-
- var start = $contents[0];
- var end = $contents.last()[0];
- rng = this.context.invoke('editor.setRange', start, 0, end, dom.nodeLength(end));
- rng.select();
- this.editable.normalize();
- this.context.invoke('editor.saveRange');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * @override
- */
- _addButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('help.LinkPlugin.show', this.options.langInfo.help['linkDialog.show']);
-
- this.context.memo('button.linkPlugin', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.link),
- tooltip: self.lang.link.link + self.context.invoke('buttons.representShortcut', 'LinkPlugin.show'),
- click: self.context.createInvokeHandler('LinkPlugin.show')
- }).render();
- });
- },
- /**
- * @param {jQueryEvent} e
- */
- _onDblclick: function (e) {
- if (dom.ancestor(e.target, dom.isAnchor) && !dom.isMedia(e.target)) {
- this.show();
- }
- },
-});
-
-var LinkPopover = Plugins.linkPopover.extend({
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Prevent links without text: replace empty text with the word "Label".
- *
- * @param {DOM} anchor
- * @param {bool} select
- */
- fillEmptyLink: function (anchor, select) {
- if (dom.isAnchor(anchor)) {
- var text = _t('Label');
- if (this.context.invoke('HelperPlugin.getRegex', 'onlyEmptySpace').test(anchor.innerHTML)) {
- $(anchor).contents().remove();
- $(anchor).append(this.document.createTextNode(text));
- }
- var rng = this.context.invoke('editor.createRange');
- if (select && anchor.innerHTML === text) {
- this._cleanLastLink();
- rng.sc = rng.ec = anchor.firstChild;
- rng.so = 0;
- rng.eo = dom.nodeLength(rng.sc);
- rng.select();
- this.context.invoke('editor.saveRange');
- }
- if (select) {
- this.lastAnchor = anchor;
- }
- }
- },
- /**
- * @override
- */
- hide: function () {
- this._cleanLastLink();
- this._super();
- },
- /**
- * @override
- */
- update: function () {
- var rng = this.context.invoke('editor.createRange');
- var anchor = dom.ancestor(rng.sc, dom.isAnchor);
- if (anchor && anchor === dom.ancestor(rng.ec, dom.isAnchor)) {
- anchor = dom.ancestor(rng.sc, dom.isAnchor);
- if (!$(anchor).is(':o_editable')) {
- this.hide();
- return;
- }
- } else {
- anchor = false;
- }
-
- if ($(anchor).data('toggle') === 'tab') {
- anchor = false;
- }
-
- if (!this.options.displayPopover(anchor)) {
- anchor = false;
- }
-
- if (anchor !== this.lastAnchor) {
- this._cleanLastLink();
- }
-
- if (!anchor) {
- this.hide();
- return;
- }
-
- if (dom.isAnchor(anchor)) {
- this.lastAnchor = anchor;
- }
-
-
- var $target = $(anchor);
- if (!$target.data('show_tooltip') && this.context.options.tooltip) {
- $target.data('show_tooltip', true);
- setTimeout(function () {
- $target.tooltip({
- title: _t('Double-click to edit'),
- trigger: 'manual',
- container: this.document.body,
- placement: 'top'
- }).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 2000);
- }, 400);
- }
-
- var innerHTML = anchor.innerHTML.replace(' ', '\u00A0').replace('', '\u200B');
-
- // prevent links without text
- this.fillEmptyLink(anchor, true);
-
- // add invisible char to prevent the carret to leave the link at the begin or end
- if (innerHTML[0] !== '\u200B') {
- var before = this.document.createTextNode('\u200B');
- $(anchor).prepend(before);
- }
- if (innerHTML[innerHTML.length - 1] !== '\u200B') {
- var after = this.document.createTextNode('\u200B');
- $(anchor).append(after);
- }
-
- anchor.normalize();
-
- var href = $(anchor).attr('href');
- this.$popover.find('a').attr('href', href).html(href);
-
- var pos = $(anchor).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + 10;
- pos.top = pos.top - posContainer.top + $(anchor).outerHeight();
-
- this.$popover.css({
- display: 'block',
- left: pos.left,
- top: pos.top,
- });
-
- this.context.layoutInfo.editor.after(this.$popover);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Clean the last link and prevent links without text.
- *
- * @private
- */
- _cleanLastLink: function () {
- if (this.lastAnchor) {
- this.fillEmptyLink(this.lastAnchor);
-
- var range = this.context.invoke('editor.createRange');
- var rangeChange;
- var innerHTML = this.lastAnchor.innerHTML.replace(' ', '\u00A0').replace('', '\u200B');
- // prevent links without text
- if (
- this.$editable.has(this.lastAnchor).length &&
- this.context.invoke('HelperPlugin.getRegexBlank', {
- space: true,
- invisible: true,
- nbsp: true,
- }).test(innerHTML)
- ) {
- $(this.lastAnchor).contents().remove();
- $(this.lastAnchor).append(this.document.createTextNode(_t('Label')));
- range.sc = range.ec = this.lastAnchor.firstChild;
- range.so = 0;
- range.eo = dom.nodeLength(range.sc);
- rangeChange = true;
- }
- var firstChild = this.context.invoke('HelperPlugin.firstLeaf', this.lastAnchor);
- if (
- !firstChild.tagName &&
- this.context.invoke('HelperPlugin.getRegex', 'startInvisible').test(firstChild.textContent)
- ) {
- firstChild.textContent = firstChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'startInvisible'), '');
- if (range.sc === firstChild && range.so) {
- range.so -= 1;
- rangeChange = true;
- }
- if (range.ec === firstChild && range.eo) {
- range.eo -= 1;
- rangeChange = true;
- }
- }
- var lastChild = this.context.invoke('HelperPlugin.lastLeaf', this.lastAnchor);
- if (
- lastChild.textContent.length > 1 &&
- !lastChild.tagName &&
- this.context.invoke('HelperPlugin.getRegex', 'endInvisible').test(lastChild.textContent)
- ) {
- lastChild.textContent = lastChild.textContent.replace(this.context.invoke('HelperPlugin.getRegex', 'endInvisible'), '');
- if (range.sc === lastChild && range.so > dom.nodeLength(lastChild)) {
- range.so = dom.nodeLength(lastChild);
- rangeChange = true;
- }
- if (range.ec === lastChild && range.eo > dom.nodeLength(lastChild)) {
- range.eo = dom.nodeLength(lastChild);
- rangeChange = true;
- }
- }
- if (rangeChange) {
- range.select();
- this.context.invoke('editor.saveRange');
- }
- this.lastAnchor = null;
- }
- },
-});
-
-registry.add('LinkPlugin', LinkPlugin)
- .add('LinkPopover', LinkPopover)
- .add('linkDialog', null)
- .add('linkPopover', null)
- .add('autoLink', null);
-
-return {
- LinkPlugin: LinkPlugin,
- LinkPopover: LinkPopover,
-};
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js b/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
deleted file mode 100644
index b8b4a7582fe..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/media.js
+++ /dev/null
@@ -1,1288 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugin.media', function (require) {
-'use strict';
-
-var core = require('web.core');
-var weWidgets = require('wysiwyg.widgets');
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var Plugins = require('web_editor.wysiwyg.plugins');
-var wysiwygTranslation = require('web_editor.wysiwyg.translation');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-var _t = core._t;
-
-var dom = $.summernote.dom;
-var ui = $.summernote.ui;
-
-//--------------------------------------------------------------------------
-// Media (for image, video, icon, document)
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is a media (image, icon, document or video).
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isMedia = function (node) {
- return dom.isImg(node) ||
- dom.isIcon(node) ||
- dom.isDocument(node) ||
- dom.isVideo(node);
-};
-
-var MediaPlugin = AbstractPlugin.extend({
- events: {
- 'summernote.mousedown': '_onMouseDown',
- 'summernote.keydown': '_onKeydown',
- 'summernote.keyup': '_onKeyup',
- 'summernote.scroll': '_onScroll',
- 'summernote.disable': '_onDisable',
- 'summernote.change': '_onChange',
- 'summernote.codeview.toggled': '_onToggled',
- 'dblclick .note-editable': '_onDblclick',
- },
-
- mousePosition: {},
- _modalOpen: false,
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Open the image dialog and listen to its saved/closed events.
- */
- showImageDialog: function () {
- if (this._modalOpen) {
- return;
- }
- this._modalOpen = true;
- this.context.invoke('editor.saveRange');
- var media = this.context.invoke('editor.restoreTarget');
-
- var $mediaParent = $(media).parent();
- if ($mediaParent.hasClass('media_iframe_video')) {
- media = $mediaParent[0];
- $mediaParent = $mediaParent.parent();
- }
-
- // If the image is for a field the model should handle the resize so we
- // use an arbitrary high size of 1920 instead of using the width of the
- // the element.
- var mediaWidth = $(media).parent().data('oeField') ? 1920 : $(media).width();
-
- var mediaDialog = new weWidgets.MediaDialog(this.options.parent, {
- mediaWidth: mediaWidth,
- onlyImages: $mediaParent.data('oeField') === 'image' || $mediaParent.data('oeType') === 'image',
- },
- $(media).clone()[0]
- );
-
- mediaDialog.on('save', this, function (element) {
- this.insertMedia(media, element);
- });
- mediaDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- this._modalOpen = false;
- });
- mediaDialog.open();
- },
- /**
- * Remove the current target media and hide its popover.
- */
- removeMedia: function () {
- this.context.invoke('editor.beforeCommand');
- var target = this.context.invoke('editor.restoreTarget');
- var point = this.context.invoke('HelperPlugin.removeBlockNode', target);
- var rng = this.context.invoke('editor.setRange', point.node, point.offset);
- rng.normalize().select();
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.clearTarget');
- this.hidePopovers();
- this.context.invoke('editor.afterCommand');
- },
- /**
- * Update the target media and its popover.
- *
- * @param {Node} target
- */
- update: function (target) {
- if (!target || !dom.isMedia(target)) {
- return;
- }
- if (!this.options.displayPopover(target)) {
- if (dom.isImg(target)) {
- this.context.invoke('HandlePlugin.update', target);
- }
- return;
- }
-
- this.lastPos = this.context.invoke('HelperPlugin.makePoint', target, $(target).offset());
-
- this.context.triggerEvent('focusnode', target);
-
- if (!dom.isMedia(target)) {
- return;
- }
-
- this.context.invoke('editor.saveTarget', target);
-
- var $target = $(target);
- if (!$target.data('show_tooltip') && this.context.options.tooltip) {
- $target.data('show_tooltip', true);
- setTimeout(function () {
- $target.tooltip({
- title: _t('Double-click to edit'),
- trigger: 'manual',
- container: this.document.body,
- }).tooltip('show');
- setTimeout(function () {
- $target.tooltip('dispose');
- }, 2000);
- }, 400);
- }
-
- if (dom.isImg(target)) {
- this.context.invoke('ImagePlugin.show', target, this.mousePosition);
- } else if (dom.isIcon(target)) {
- this.context.invoke('IconPlugin.show', target, this.mousePosition);
- } else if (dom.isVideo(target)) {
- this.context.invoke('VideoPlugin.show', target, this.mousePosition);
- } else if (dom.isDocument(target)) {
- this.context.invoke('DocumentPlugin.show', target, this.mousePosition);
- }
- },
- /**
- * Hide all open popovers.
- * Warning: removes the saved target.
- */
- hidePopovers: function () {
- var media = this.context.invoke('editor.restoreTarget');
- this.context.invoke('HandlePlugin.hide');
- this.context.invoke('ImagePlugin.hide');
- this.context.invoke('IconPlugin.hide');
- this.context.invoke('VideoPlugin.hide');
- this.context.invoke('DocumentPlugin.hide');
- this.context.invoke('editor.saveTarget', media);
- },
- /**
- * Insert or replace a media.
- *
- * @param {Node} previous the media to replace, if any
- * @param {Node} newMedia contains the media to insert
- */
- insertMedia: function (previous, newMedia) {
- if (!newMedia) {
- return;
- }
- this._wrapCommand(function () {
- this.$editable.focus();
- var rng = this.context.invoke('editor.createRange');
- var point;
-
- if (newMedia.tagName === "IMG") {
- $(newMedia).one('load error abort', this.updatePopoverAfterEdit.bind(this, newMedia));
- }
-
- if (previous) {
- this.context.invoke('editor.clearTarget');
- var start = previous.parentNode;
- rng = this.context.invoke('editor.setRange', start, _.indexOf(start.childNodes, previous));
- if (previous.tagName === newMedia.tagName) {
- // Eg: replace an image with an image -> reapply classes removed by `clear`
- var reFaIcons = /fa-(?!spin(\s|$))\S+/g; // do not keep fontawesome icons but keep fa-spin
- $(newMedia).addClass(previous.className.replace(reFaIcons, ''));
- }
-
- if (dom.isVideo(previous) || dom.isVideo(newMedia)) {
- var doNotInsertP = previous.tagName === newMedia.tagName;
- point = this.context.invoke('HelperPlugin.removeBlockNode', previous, doNotInsertP);
- if (!rng.sc.parentNode || !rng.sc.childNodes[rng.so]) {
- rng = this.context.invoke('editor.setRange', point.node, point.offset);
- }
- previous = null;
- }
- rng.select();
- this.hidePopovers();
- }
-
- if (dom.isVideo(newMedia)) {
- this.context.invoke('HelperPlugin.insertBlockNode', newMedia);
- } else {
- rng = this.context.invoke('editor.createRange');
- point = rng.getStartPoint();
- if (!rng.isCollapsed()) {
- point = this.context.invoke('HelperPlugin.deleteBetween', point, rng.getEndPoint());
- }
-
- if (point.node.tagName) {
- if (previous) {
- $(previous).replaceWith(newMedia);
- } else if (dom.isVoid(point.node)) {
- point.node.parentNode.insertBefore(newMedia, point.node);
- } else {
- var node = point.node.childNodes[point.offset];
- if (point.node.tagName === 'BR') {
- $(point.node).replaceWith(newMedia);
- } else if (node && node.tagName === 'BR') {
- $(node).replaceWith(newMedia);
- } else {
- point.node.insertBefore(newMedia, node || null);
- }
- }
- if (!this._isFakeNotEditable(newMedia)) {
- if (!newMedia.previousSibling) {
- $(newMedia).before(this.document.createTextNode('\u200B'), newMedia);
- }
- if (!newMedia.nextSibling) {
- $(newMedia).after(this.document.createTextNode('\u200B'), newMedia);
- }
- }
- } else {
- var next = this.document.createTextNode(point.node.textContent.slice(point.offset));
- point.node.textContent = point.node.textContent.slice(0, point.offset);
-
- $(point.node).after(next).after(newMedia);
- point.node.parentNode.normalize();
- if (!this._isFakeNotEditable(newMedia)) {
- if (!newMedia.previousSibling) {
- $(newMedia).before(this.document.createTextNode('\u200B'), newMedia);
- }
- if (!newMedia.nextSibling) {
- $(newMedia).after(this.document.createTextNode('\u200B'), newMedia);
- }
- }
- rng = this.context.invoke('editor.setRange', newMedia.nextSibling || newMedia, 0);
- rng.normalize().select();
- }
- }
- this.context.invoke('editor.saveRange');
- this.context.invoke('editor.saveTarget', newMedia);
- this.context.triggerEvent('focusnode', newMedia);
- this.context.invoke('UnbreakablePlugin.secureArea', newMedia);
-
- this.updatePopoverAfterEdit(newMedia);
- })();
- },
- /**
- * Update the media's popover and its position after editing the media.
- *
- * @param {Node} media
- */
- updatePopoverAfterEdit: function (media) {
- this.mousePosition = {
- pageX: $(media).offset().left + $(media).width() / 2,
- pageY: $(media).offset().top + $(media).height() / 2,
- };
- $(media).trigger('mousedown').trigger('mouseup');
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add the media popovers' buttons:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- *
- * @override
- */
- _addButtons: function () {
- var self = this;
- this._super();
-
- this.context.memo('button.mediaPlugin', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.picture),
- tooltip: self.lang.image.image,
- click: self.context.createInvokeHandler('MediaPlugin.showImageDialog')
- }).render();
- });
-
- this.context.memo('button.removePluginMedia', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.trash),
- tooltip: self.lang.image.remove,
- click: self._wrapCommand(function () {
- this.context.invoke('MediaPlugin.removeMedia');
- })
- }).render();
- });
-
- _.each(['left', 'center', 'right', 'none'], function (align) {
- var alignName = _.str.camelize('align_' + align);
- self._createButton(alignName, self.options.icons[alignName], self.lang.image[alignName], function () {
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.css('float', '').removeClass('mx-auto pull-right pull-left');
- if (align === 'center') {
- $target.addClass('mx-auto');
- } else if (align !== 'none') {
- $target.addClass('pull-' + align);
- }
- });
- });
-
- var padding = [null, 'padding-small', 'padding-medium', 'padding-large', 'padding-xl'];
- var zipped = _.zip(padding, this.lang.image.paddingList);
- var values = _.map(zipped, function (z) {
- return {
- value: z[0],
- string: z[1],
- };
- });
- this._createDropdownButton('padding', this.options.icons.padding, this.lang.image.padding, values);
- },
- /**
- * Return true if the node is a fake not-editable.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
- _isFakeNotEditable: function (node) {
- var contentEditableAncestor = dom.ancestor(node, function (n) {
- return !!n.contentEditable && n.contentEditable !== 'inherit';
- });
- return !!contentEditableAncestor && contentEditableAncestor.contentEditable === 'false';
- },
- /**
- * Select the target media based on the
- * currently saved target or on the current range.
- *
- * @private
- * @param {Node} [target] optional
- * @returns {Node} target
- */
- _selectTarget: function (target) {
- if (!target) {
- target = this.context.invoke('editor.restoreTarget');
- }
-
- if (this.context.isDisabled()) {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
- var range = this.context.invoke('editor.createRange');
- if (!target && range.isCollapsed()) {
- target = range.sc.childNodes[range.so] || range.sc;
- }
- if (!target || !dom.isMedia(target)) {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
-
- while (target.parentNode && dom.isMedia(target.parentNode)) {
- target = target.parentNode;
- }
-
- if (!this.options.isEditableNode(target)) {
- if (!target.parentNode) {
- target = this.editable;
- }
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- return target;
- }
-
- this.context.invoke('editor.saveTarget', target);
- this.context.triggerEvent('focusnode', target);
-
- return target;
- },
- /**
- * Select the target media on the right (or left)
- * of the currently selected target media.
- *
- * @private
- * @param {Node} target
- * @param {Boolean} left
- */
- _moveTargetSelection: function (target, left) {
- if (!target || !dom.isMedia(target)) {
- return;
- }
- var range = this.context.invoke('editor.createRange');
- var $contentEditable;
-
- if (
- range.sc.tagName && $.contains(target, range.sc) &&
- $(range.sc).hasClass('o_fake_editable') &&
- left === !range.sc.previousElementSibling
- ) {
- $contentEditable = $(range.sc).closest('[contentEditable]');
- if ($(target).closest('[contentEditable]')[0] !== $contentEditable[0]) {
- $contentEditable.focus();
- }
- this.context.invoke('editor.saveRange');
- return;
- }
-
- var next = this.context.invoke('HelperPlugin.makePoint', target, 0);
- if (left) {
- if (dom.isVideo(target)) {
- next = this.context.invoke('HelperPlugin.makePoint', target.firstElementChild, 0);
- } else {
- next = dom.prevPointUntil(next, function (point) {
- return point.node !== target && !$.contains(target, point.node);
- }) || next;
- }
- } else {
- if (dom.isVideo(target)) {
- next = this.context.invoke('HelperPlugin.makePoint', target.lastElementChild, 0);
- } else {
- next = dom.nextPointUntil(next, function (point) {
- return point.node !== target && !$.contains(target, point.node);
- }) || next;
- }
- }
-
- $contentEditable = $(next.node).closest('[contentEditable]');
- if ($(target).closest('[contentEditable]')[0] !== $contentEditable[0]) {
- // move the focus only if the new contentEditable is not the same (avoid scroll up)
- // (like in the case of a video, which uses two contentEditable in the media, so as to write text)
- $contentEditable.focus();
- }
-
- range = this.context.invoke('editor.setRange', next.node, next.offset);
- range.select();
-
- this.context.invoke('editor.saveRange');
- },
-
- //--------------------------------------------------------------------------
- // handle
- //--------------------------------------------------------------------------
-
- /**
- * @private
- */
- _onDisable: function () {
- this.hidePopovers();
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @private
- * @param {jQueryEvent} e
- */
- _onDblclick: function (e) {
- if (dom.isMedia(e.target)) {
- var target = this._selectTarget(e.target);
- this._moveTargetSelection(target);
- this.showImageDialog();
- }
- },
- /**
- * @private
- **/
- _onKeydown: function () {
- this.context.invoke('editor.clearTarget');
- },
- /**
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onKeyup: function (se, e) {
- var target = this._selectTarget();
- var range = this.context.invoke('editor.createRange');
- if (e.keyCode === 37) {
- var point = dom.prevPoint(range.getStartPoint());
- if (dom.isMedia(point.node)) {
- target = point.node;
- }
- }
- this._moveTargetSelection(target, e.keyCode === 37);
- return this.update(target);
- },
- /**
- * @private
- */
- _onScroll: function () {
- var target = this._selectTarget();
- if (this.lastPos && this.lastPos.target === target && $(target).offset()) {
- var newTop = $(target).offset().top;
- var movement = this.lastPos.offset.top - newTop;
- if (movement && this.mousePosition) {
- this.mousePosition.pageY -= movement;
- }
- }
- return this.update(target);
- },
- /**
- * @private
- */
- _onChange: function () {
- var target = this._selectTarget();
- this._moveTargetSelection(target);
- if (!this.$editable.has(target).length) {
- return;
- }
- return this.update(target);
- },
- /**
- * @private
- * @param {SummernoteEvent} se
- * @param {jQueryEvent} e
- */
- _onMouseDown: function (se, e) {
- var target = this._selectTarget(e.target);
- if (target && dom.isMedia(target)) {
- var pos = $(target).offset();
-
- if (e.pageX) {
- this.mousePosition = {
- pageX: e.pageX,
- pageY: e.pageY,
- };
- } else {
- // for testing triggers
- this.mousePosition = {
- pageX: pos.left,
- pageY: pos.top,
- };
- }
-
- var width = $(target).width();
- // we put the cursor to the left if we click in the first tier of the media
- var left = this.mousePosition.pageX < (pos.left + width / 3);
- this._moveTargetSelection(target, left);
-
- this.update(target);
- e.preventDefault();
- } else {
- this.mousePosition = {};
- }
- },
- /**
- * @private
- */
- _onToggled: function () {
- this.update();
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- alignCenter: 'note-icon-align-center',
- alignNone: wysiwygOptions.icons.alignJustify,
- picture: 'fa fa-file-image-o',
-});
-_.extend(wysiwygTranslation.image, {
- alignRight: wysiwygTranslation.image.floatRight,
- alignCenter: _t('Align center'),
- alignLeft: wysiwygTranslation.image.floatLeft,
- alignNone: wysiwygTranslation.image.floatNone,
-});
-
-//--------------------------------------------------------------------------
-// Abstract
-//--------------------------------------------------------------------------
-
-var AbstractMediaPlugin = AbstractPlugin.extend({
- targetType: null,
- initialize: function () {
- this._super.apply(this, arguments);
- this.$popover = this.ui.popover({
- className: 'note-' + this.targetType + '-popover',
- })
- .render().appendTo(this.options.container);
- var $content = this.$popover.find('.popover-content, .note-popover-content');
- this.context.invoke('buttons.build', $content, this.options.popover[this.targetType]);
- this.options.POPOVER_MARGIN = 15;
- },
- destroy: function () {
- this.$popover.remove();
- },
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide this popover.
- */
- hide: function () {
- this.$popover.hide();
- },
- /**
- * Show this popover.
- *
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- this._popoverPosition(target, mousePosition);
- ui.toggleBtnActive(this.$popover.find('a, button'), false);
- var $target = $(target);
-
- var float = $target.css('float');
- if (float === 'none' && $target.hasClass('mx-auto')) {
- float = 'center';
- }
- var floatIcon = this.options.icons[_.str.camelize('align_' + (float !== 'none' ? float : 'justify'))];
- ui.toggleBtnActive(this.$popover.find('.note-float button:has(.' + floatIcon + ')'), true);
-
- var padding = (($target.attr('class') || '').match(/(^| )(padding-[^\s]+)( |$)/) || ['fa-1x'])[2];
- ui.toggleBtnActive(this.$popover.find('.note-padding a:has(li[data-value="' + padding + '"])'), true);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Takes the left and top position of the popover and an optional margin
- * and returns updated positions to force the popover to fit the editor container.
- *
- * @private
- * @param {Number} left
- * @param {Number} top
- * @param {Number} [margin] optional
- * @returns {Object} {left: Number, top: Number}
- */
- _popoverFitEditor: function (left, top, margin) {
- margin = margin || this.options.POPOVER_MARGIN;
-
- var $container = $(this.options.container);
- var containerWidth = $container.width();
- var containerHeight = $container.height();
-
- var popoverWidth = this.$popover.width();
- var popoverHeight = this.$popover.height();
-
- var isBeyondXBounds = left + popoverWidth >= containerWidth - margin;
- var isBeyondYBounds = top + popoverHeight >= containerHeight - margin;
- return {
- left: isBeyondXBounds ? containerWidth - popoverWidth - margin : left,
- top: isBeyondYBounds ? top = containerHeight - popoverHeight - margin : (top > 0 ? top : margin),
- };
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @private
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(this.options.container).offset();
- pos.left = mousePosition.pageX - pos.left + this.options.POPOVER_MARGIN;
- pos.top = mousePosition.pageY - pos.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left, pos.top);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
- /**
- * Override to return whether the target is a media (specific to its class) or not.
- *
- * @private
- * @param {Node} target
- */
- _isMedia: function (target) {},
-});
-
-//--------------------------------------------------------------------------
-// Image
-//--------------------------------------------------------------------------
-
-var ImagePlugin = AbstractMediaPlugin.extend({
- targetType: 'image',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Open the crop image dialog and listen to its save/closed events.
- */
- cropImageDialog: function () {
- this.context.invoke('editor.saveRange');
-
- var media = this.context.invoke('editor.restoreTarget');
- var cropImageDialog = new weWidgets.CropImageDialog(this.options.parent, {},
- $(media).clone()[0]
- );
- cropImageDialog.on('save', this, function (img) {
- this.context.invoke('MediaPlugin.insertMedia', media, img);
- });
- cropImageDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- });
-
- cropImageDialog.open();
- },
- /**
- * Open the alt dialog (change image title and alt) and listen to its save/closed events.
- */
- altDialg: function () {
- this.context.invoke('editor.saveRange');
-
- var media = this.context.invoke('editor.restoreTarget');
- var altDialog = new weWidgets.AltDialog(this.options.parent, {},
- $(media).clone()[0]
- );
- altDialog.on('save', this, this._wrapCommand(function (img) {
- $(media).attr('alt', img.alt)
- .attr('title', img.title)
- .trigger('content_changed');
- }));
- altDialog.on('closed', this, function () {
- this.context.invoke('editor.restoreRange');
- });
-
- altDialog.open();
- },
- /**
- * Show the image target's popover.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- var self = this;
- this._super.apply(this, arguments);
- var $target = $(target);
-
- this.context.invoke('HandlePlugin.update', target);
-
- _.each(this.options.icons.imageShape, function (icon, className) {
- var thisIconSel = '.note-imageShape button:has(.' +
- icon.replace(self.context.invoke('HelperPlugin.getRegex', 'space', 'g'), '.') +
- ')';
- ui.toggleBtnActive(self.$popover.find(thisIconSel), $target.hasClass(className));
- });
-
- var size = (($target.attr('style') || '').match(/width:\s*([0-9]+)%/i) || [])[1];
- ui.toggleBtnActive(this.$popover.find('.note-imagesize button:contains(' + (size ? size + '%' : this.lang.image.imageSizeAuto) + ')'), true);
-
- ui.toggleBtnActive(this.$popover.find('.note-cropImage button'), $target.hasClass('o_cropped_img_to_save'));
-
- // update alt button in popover
- if ($target.attr('alt')) {
- var $altLabel = $(this.altBtnPrefix).text(function (i, v) {
- var newText = v + '\u00A0' + $target.attr('alt');
- return $.trim(newText).substring(0, 30).trim(this) + "...";
- });
- this.$popover.find('.note-alt button').contents().replaceWith($altLabel);
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Save cropped images.
- *
- * @private
- * @returns {Promise}
- */
- _saveCroppedImages: function () {
- var self = this;
- var defs = this.$editables.find('.o_cropped_img_to_save').map(function () {
- var $croppedImg = $(this);
- $croppedImg.removeClass('o_cropped_img_to_save');
- var resModel = $croppedImg.data('crop:resModel');
- var resID = $croppedImg.data('crop:resID');
- var cropID = $croppedImg.data('crop:id');
- var mimetype = $croppedImg.data('crop:mimetype');
- var originalSrc = $croppedImg.data('crop:originalSrc');
- var datas = $croppedImg.attr('src').split(',')[1];
- if (!cropID) {
- var name = originalSrc + '.crop';
- return self._rpc({
- model: 'ir.attachment',
- method: 'create',
- args: [{
- res_model: resModel,
- res_id: resID,
- name: name,
- datas: datas,
- mimetype: mimetype,
- url: originalSrc, // To save the original image that was cropped
- }],
- }).then(function (attachmentID) {
- return self._rpc({
- model: 'ir.attachment',
- method: 'generate_access_token',
- args: [
- [attachmentID],
- ],
- }).then(function (access_token) {
- $croppedImg.attr('src', '/web/image/' + attachmentID + '?access_token=' + access_token[0]);
- });
- });
- } else {
- return self._rpc({
- model: 'ir.attachment',
- method: 'write',
- args: [
- [cropID], {
- datas: datas,
- },
- ],
- });
- }
- }).get();
- return Promise.all(defs);
- },
- /**
- * Add the image popovers' buttons:
- * From _super:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- * From this override:
- * - Shape
- * - Crop
- * - Alt
- * - Size
- *
- * @private
- */
- _addButtons: function () {
- var self = this;
- this._super();
- // add all shape buttons if this option is active
- this.context.memo('button.imageShape', function () {
- var $el = $();
- _.each(['rounded', 'rounded-circle', 'shadow', 'img-thumbnail'], function (shape) {
- $el = $el.add(self._createToggleButton(null, self.options.icons.imageShape[shape], self.lang.image.imageShape[shape], shape));
- });
- return $el;
- });
- this.context.memo('button.cropImage', function () {
- return self.context.invoke('buttons.button', {
- contents: self.ui.icon(self.options.icons.cropImage),
- tooltip: self.lang.image.cropImage,
- click: self.context.createInvokeHandler('ImagePlugin.cropImageDialog')
- }).render();
- });
- this.altBtnPrefix = '' + self.lang.image.alt + '';
- this.context.memo('button.alt', function () {
- return self.context.invoke('buttons.button', {
- contents: self.altBtnPrefix,
- click: self.context.createInvokeHandler('ImagePlugin.altDialg')
- }).render();
- });
- this.context.memo('button.imageSizeAuto', function () {
- return self.context.invoke('buttons.button', {
- contents: '' + self.lang.image.imageSizeAuto + '',
- click: self._wrapCommand(function () {
- var target = this.context.invoke('editor.restoreTarget');
- $(target).css({
- width: '',
- height: ''
- });
- })
- }).render();
- });
- },
- /**
- * Return true if the target is an image.
- *
- * @override
- * @param {Node} target
- * @returns {Boolean} true if the target is an image
- */
- _isMedia: function (target) {
- return dom.isImg(target);
- },
-});
-
-_.extend(wysiwygOptions.icons, {
- padding: 'fa fa-plus-square-o',
- cropImage: 'fa fa-crop',
- imageShape: {
- rounded: 'fa fa-square',
- 'rounded-circle': 'fa fa-circle-o',
- shadow: 'fa fa-sun-o',
- 'img-thumbnail': 'fa fa-picture-o',
- },
-});
-_.extend(wysiwygTranslation.image, {
- padding: _t('Padding'),
- paddingList: [_t('None'), _t('Small'), _t('Medium'), _t('Large'), _t('Xl')],
- imageSizeAuto: _t('Auto'),
- cropImage: _t('Crop image'),
- imageShape: {
- rounded: _t('Shape: Rounded'),
- 'rounded-circle': _t('Shape: Circle'),
- shadow: _t('Shape: Shadow'),
- 'img-thumbnail': _t('Shape: Thumbnail'),
- },
- alt: _t('Description:'),
-});
-
-//--------------------------------------------------------------------------
-// Video
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is a video.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isVideo = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return (node.tagName === "IFRAME" || node.tagName === "DIV") &&
- (node.parentNode && node.parentNode.className && node.parentNode.className.indexOf('media_iframe_video') !== -1 ||
- node.className.indexOf('media_iframe_video') !== -1);
-};
-
-var VideoPlugin = AbstractMediaPlugin.extend({
- targetType: 'video',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Show the video target's popover.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- show: function (target, mousePosition) {
- if (target.tagName === "DIV" && target.className.indexOf('css_editable_mode_display') !== -1) {
- target = target.parentNode;
- this.context.invoke('editor.saveTarget', target);
- }
- return this._super.apply(this, arguments);
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Return true if the target is a video.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isVideo(target);
- },
-});
-
-//--------------------------------------------------------------------------
-// Icons: Icon Awsome (and other with themes)
-//--------------------------------------------------------------------------
-
-/**
- * Return true if the node is an icon.
- *
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isIcon = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return node && node.className && node.className.indexOf(' fa-') !== -1;
-};
-
-var IconPlugin = AbstractMediaPlugin.extend({
- targetType: 'icon',
-
- //--------------------------------------------------------------------------
- // Public
- //--------------------------------------------------------------------------
-
- /**
- * Hide this icon's popover.
- *
- * @override
- */
- hide: function () {
- this._super();
- this.removeSelectedClass(this.lastIcon);
- },
- /**
- * Show the icon target's popover.
- *
- * @override
- * @param {Node} target
- */
- show: function (target) {
- this.removeSelectedClass(this.lastIcon);
- this.lastIcon = target;
-
- this._super.apply(this, arguments);
-
- var $target = $(target);
- ui.toggleBtnActive(this.$popover.find('.note-faSpin button'), $target.hasClass('fa-spin'));
- var faSize = parseInt((($target.attr('style') || '').match(/font-size:\s*([0-9](em|px))(;|$)/) || [])[1] || 0);
- if (!faSize) {
- faSize = (($target.attr('class') || '').match(/(^| )fa-([0-9])x( |$)/) || [])[2];
- }
- ui.toggleBtnActive(this.$popover.find('.note-faSize a[data-value="fa-' + faSize + 'x"]'), true);
- this.addSelectedClass(target);
- },
- /**
- * Add a class to the current target so as to show it's selected.
- *
- * @param {Node} target
- */
- addSelectedClass: function (target) {
- if (target) {
- $(target).addClass('o_we_selected_image');
- }
- },
- /**
- * Remove a class to the current target so as to show it's not selected.
- *
- * @param {Node} target
- */
- removeSelectedClass: function (target) {
- if (target) {
- $(target).removeClass('o_we_selected_image');
- }
- },
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Add the icon popovers' buttons:
- * From _super:
- * - Replacement
- * - Removal
- * - Alignment
- * - Padding
- * From this override:
- * - Size
- * - Spin
- *
- * @private
- */
- _addButtons: function () {
- var self = this;
- this._super();
- var values = [{
- value: '',
- string: '1x',
- },
- {
- value: 'fa-2x',
- string: '2x',
- },
- {
- value: 'fa-3x',
- string: '3x',
- },
- {
- value: 'fa-4x',
- string: '4x',
- },
- {
- value: 'fa-5x',
- string: '5x',
- },
- ];
- var onFaSize = function (e) {
- var classNames = _.map(values, function (item) {
- return item.value;
- }).join(' ');
- var $target = $(self.context.invoke('editor.restoreTarget'));
- $target.removeClass(classNames);
- if ($(e.target).data('value')) {
- $target.addClass($(e.target).data('value'));
- $target.css('fontSize', '');
- }
- };
- this._createDropdownButton('faSize', this.options.icons.faSize, this.lang.image.faSize, values, onFaSize);
- this._createToggleButton('faSpin', this.options.icons.faSpin, this.lang.image.faSpin, 'fa-spin');
- },
- /**
- * Return true if the target is an icon.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isIcon(target);
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(target).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + this.options.POPOVER_MARGIN + parseInt($(target).css('font-size')) + 10;
- pos.top = pos.top - posContainer.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left + 10, pos.top - 15);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
-});
-_.extend(wysiwygOptions.icons, {
- faSize: 'fa fa-expand',
- faSpin: 'fa fa-refresh',
-});
-_.extend(wysiwygTranslation.image, {
- faSize: _t('Icon size'),
- faSpin: _t('Spin'),
-});
-
-//--------------------------------------------------------------------------
-// Media Document
-//--------------------------------------------------------------------------
-
-/**
- * Return true is the node is a document.
- * @param {Node} node
- * @returns {Boolean}
- */
-dom.isDocument = function (node) {
- node = node && !node.tagName ? node.parentNode : node;
- return node && (node.tagName === "A" && node.className.indexOf('o_image') !== -1);
-};
-
-var DocumentPlugin = AbstractMediaPlugin.extend({
- targetType: 'document',
-
- //--------------------------------------------------------------------------
- // Private
- //--------------------------------------------------------------------------
-
- /**
- * Return true if the target is an icon.
- *
- * @override
- * @param {Node} target
- */
- _isMedia: function (target) {
- return dom.isDocument(target);
- },
- /**
- * Update the position of the popover in CSS.
- *
- * @override
- * @param {Node} target
- * @param {Object} mousePosition {pageX: Number, pageY: Number}
- */
- _popoverPosition: function (target, mousePosition) {
- var pos = $(target).offset();
- var posContainer = $(this.options.container).offset();
- pos.left = pos.left - posContainer.left + this.options.POPOVER_MARGIN;
- pos.top = pos.top - posContainer.top + this.options.POPOVER_MARGIN;
-
- var popoverPos = this._popoverFitEditor(pos.left + 10, pos.top - 15);
- this.$popover.css({
- display: 'block',
- left: popoverPos.left,
- top: popoverPos.top,
- });
- },
-});
-
-//--------------------------------------------------------------------------
-// Handle (hover image)
-//--------------------------------------------------------------------------
-
-var HandlePlugin = Plugins.handle.extend({
- /**
- * Update the handle.
- *
- * @param {Node} target
- * @returns {Boolean}
- */
- update: function (target) {
- if (this.context.isDisabled()) {
- return false;
- }
- var isImage = dom.isImg(target);
- var $selection = this.$handle.find('.note-control-selection');
- this.context.invoke('imagePopover.update', target);
- if (!isImage) {
- return isImage;
- }
-
- var $target = $(target);
- var pos = $target.offset();
- var posContainer = $selection.closest('.note-handle').offset();
-
- // exclude margin
- var imageSize = {
- w: $target.outerWidth(false),
- h: $target.outerHeight(false)
- };
-
- $selection.css({
- display: 'block',
- left: pos.left - posContainer.left,
- top: pos.top - posContainer.top,
- width: imageSize.w,
- height: imageSize.h,
- }).data('target', $target); // save current target element.
-
- var src = $target.attr('src');
- var displayInfo = imageSize.w >= 170 || (imageSize.w >= 120 && imageSize.h >= 58) || (imageSize.w >= 80 && imageSize.h >= 76);
-
- var sizingText = '';
- if (displayInfo) {
- sizingText = imageSize.w + 'x' + imageSize.h;
- sizingText += ' (' + this.lang.image.original + ': ';
- } else if (src && imageSize.w >= 80 && imageSize.h >= 32) {
- displayInfo = true;
- sizingText = '(';
- }
-
- if (src) {
- var origImageObj = new Image();
- origImageObj.src = src;
- sizingText += origImageObj.width + 'x' + origImageObj.height + ')';
- }
-
- $selection.find('.note-control-selection-info').text(sizingText);
- this.context.invoke('editor.saveTarget', target);
-
- return isImage;
- },
-});
-
-//--------------------------------------------------------------------------
-// add to registry
-//--------------------------------------------------------------------------
-
-registry.add('MediaPlugin', MediaPlugin)
- .add('ImagePlugin', ImagePlugin)
- .add('VideoPlugin', VideoPlugin)
- .add('IconPlugin', IconPlugin)
- .add('DocumentPlugin', DocumentPlugin)
- .add('HandlePlugin', HandlePlugin);
-
-// modules to remove from summernote
-registry.add('imagePopover', null)
- .add('handle', null);
-
-return {
- MediaPlugin: MediaPlugin,
- ImagePlugin: ImagePlugin,
- VideoPlugin: VideoPlugin,
- IconPlugin: IconPlugin,
- DocumentPlugin: DocumentPlugin,
-};
-
-});
diff --git a/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js b/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js
deleted file mode 100644
index dfd5b109e74..00000000000
--- a/addons/web_editor/static/src/js/wysiwyg/plugin/plugins.js
+++ /dev/null
@@ -1,78 +0,0 @@
-odoo.define('web_editor.wysiwyg.plugins', function (require) {
-'use strict';
-
-var AbstractPlugin = require('web_editor.wysiwyg.plugin.abstract');
-var registry = require('web_editor.wysiwyg.plugin.registry');
-var wysiwygOptions = require('web_editor.wysiwyg.options');
-
-
-var plugins = _.mapObject(wysiwygOptions.modules, function (Module, pluginName) {
- var prototype = {
- init: function () {
- var self = this;
- this._super.apply(this, arguments);
- var events = _.clone(this.events);
- this.summernote.options.modules[pluginName].apply(this, arguments);
- _.each(events, function (value, key) {
- self.events[key] = value;
- });
- },
- };
- _.each(Module.prototype, function (prop, name) {
- if (typeof prop === 'function') {
- prototype[name] = function () {
- return this.summernote.options.modules[pluginName].prototype[name].apply(this, arguments);
- };
- } else {
- prototype[name] = prop;
- }
- });
-
- var Plugin = AbstractPlugin.extend(prototype).extend({
- destroy: function () {
- if (this.shouldInitialize()) {
- this._super();
- }
- },
- });
-
- // override summernote default buttons
- registry.add(pluginName, Plugin);
-
- return Plugin;
-});
-
-// export table plugin to convert it in module (see editor)
-
-var $textarea = $('