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();
}
var mediaDialog = new weWidgets.MediaDialog(this.options.parent, {
onlyImages: $mediaParent.data('oeField') === 'image' || $mediaParent.data('oeType') === 'image',
},
$(media).clone()[0]
);
mediaDialog.on('saved', this, function (data) {
this.insertMedia(media, data);
});
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 {Object} data contains the media to insert
*/
insertMedia: function (previous, data) {
if (!data.media) {
return;
}
var newMedia = data.media;
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 saved/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()
);
cropImageDialog.on('saved', this, function (data) {
this.context.invoke('MediaPlugin.insertMedia', media, data);
});
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 saved/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()
);
altDialog.on('saved', this, this._wrapCommand(function (data) {
$(media).attr('alt', $(data.media).attr('alt'))
.attr('title', $(data.media).attr('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_fname: 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,
};
});