odoo.define('web_editor.widget', function (require) {
'use strict';
var ajax = require('web.ajax');
var base = require('web_editor.base');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var weContext = require("web_editor.context");
var QWeb = core.qweb;
var range = $.summernote.core.range;
var dom = $.summernote.core.dom;
var _t = core._t;
/**
* @todo we should either get rid of this or move it somewhere else
*/
function simulateMouseEvent(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);
}
/**
* Extend Dialog class to handle save/cancel of edition components.
*/
Dialog = Dialog.extend({
/**
* @constructor
*/
init: function (parent, options) {
options = options || {};
this._super(parent, _.extend({}, {
buttons: [
{text: options.save_text || _t("Save"), classes: 'btn-primary', click: this.save},
{text: _t("Discard"), close: true}
]
}, options));
this.destroyAction = 'cancel';
var self = this;
this.opened().then(function () {
self.$('input:first').focus();
});
this.on('closed', this, function () {
this.trigger(this.destroyAction, this.final_data || null);
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Called when the dialog is saved. Set the destroy action type to "save"
* and should set the final_data variable correctly before closing.
*/
save: function () {
this.destroyAction = "save";
this.close();
},
});
/**
* Let users change the alt & title of a media.
*/
var AltDialog = Dialog.extend({
template: 'web_editor.dialog.alt',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
/**
* @constructor
*/
init: function (parent, options, $editable, media) {
this._super(parent, _.extend({}, {
title: _t("Change media description and tooltip")
}, options));
this.$editable = $editable;
this.media = media;
this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"');
this.title = ($(this.media).attr('title') || "").replace(/"/g, '"');
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
save: function () {
var self = this;
range.createFromNode(this.media).select();
this.$editable.data('NoteHistory').recordUndo();
var alt = this.$('#alt').val();
var title = this.$('#title').val();
$(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null);
_.defer(function () {
simulateMouseEvent(self.media, 'mouseup');
});
return this._super.apply(this, arguments);
},
});
var MediaWidget = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
/**
* @constructor
*/
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.media = media;
this.$media = $(media);
this.page = 0;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @todo comment
*/
clear: function () {
if (!this.media) {
return;
}
this._clear();
},
/**
* @todo comment
*/
getControlPanelConfig: function () {
return {
searchEnabled: true,
pagerEnabled: true,
pagerLeftEnabled: false,
pagerRightEnabled: false,
};
},
/**
* @override
*/
goToPage: function (page) {
this.page = page;
},
/**
* @abstract
* @param {string} needle
* @returns {Deferred}
*/
search: function (needle) {},
/**
* @abstract
* @returns {*}
*/
save: function () {},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @abstract
*/
_clear: function () {},
/**
* @private
*/
_replaceMedia: function ($media) {
this.$media.replaceWith($media);
this.$media = $media;
this.media = $media[0];
},
});
/**
* Let users choose an image, including uploading a new image in odoo.
*/
var ImageWidget = MediaWidget.extend({
template: 'web_editor.dialog.image',
events: {
'click .o_upload_image_button': '_onUploadButtonClick',
'click .o_upload_image_button_no_optimization': '_onUploadButtonNoOptimizationClick',
'change input[type=file]': '_onImageSelection',
'click .o_upload_image_url_button': '_onUploadURLButtonClick',
'input input.url': "_onSearchInput",
'click .existing-attachments [data-src]': '_onImageClick',
'dblclick .existing-attachments [data-src]': '_onImageDblClick',
'click .o_existing_attachment_remove': '_onRemoveClick',
},
IMAGES_PER_ROW: 6,
IMAGES_ROWS: 2,
/**
* @constructor
*/
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.IMAGES_PER_PAGE = this.IMAGES_PER_ROW * this.IMAGES_ROWS;
this.options = options;
this.accept = options.accept || (options.document ? '*/*' : 'image/*');
if (options.domain) {
this.domain = typeof options.domain === 'function' ? options.domain() : options.domain;
} else if (options.res_id) {
this.domain = ['|',
'&', ['res_model', '=', options.res_model], ['res_id', '=', options.res_id],
['res_model', '=', 'ir.ui.view']];
} else {
this.domain = [['res_model', '=', 'ir.ui.view']];
}
this.multiImages = options.multiImages;
this.firstFilters = options.firstFilters || [];
this.lastFilters = options.lastFilters || [];
this.images = [];
},
/**
* @override
*/
willStart: function () {
return $.when(
this._super.apply(this, arguments),
this.search('', true)
);
},
/**
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
var self = this;
this._renderImages();
var o = {
url: null,
alt: null,
};
if (this.$media.is('img')) {
o.url = this.$media.attr('src');
} else if (this.$media.is('a.o_image')) {
o.url = this.$media.attr('href').replace(/[?].*/, '');
o.id = +o.url.match(/\/web\/content\/(\d+)/, '')[1];
}
if (o.url) {
self._toggleImage(_.find(self.records, function (record) { return record.url === o.url;}) || o, true);
}
return def;
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
return _.extend(this._super.apply(this, arguments), {
pagerLeftEnabled: this.page > 0,
pagerRightEnabled: (this.page + 1) * this.IMAGES_PER_PAGE < this.records.length,
});
},
/**
* @override
*/
goToPage: function (page) {
this._super.apply(this, arguments);
this._renderImages();
},
/**
* @override
*/
save: function () {
var self = this;
if (this.multiImages) {
return this.images;
}
var img = this.images[0];
var def = $.when();
if (!img.access_token) {
def = this._rpc({
model: 'ir.attachment',
method: 'generate_access_token',
args: [[img.id]]
}).then(function (access_token) {
img.access_token = access_token[0];
});
}
return def.then(function () {
if (!img.isDocument) {
if (img.access_token && self.options.res_model !== 'ir.ui.view') {
img.src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (!self.$media.is('img')) {
// Note: by default the images receive the bootstrap opt-in
// img-responsive class. We cannot make them all responsive
// by design because of libraries and client databases img.
self._replaceMedia($('
', {class: 'img-responsive o_we_custom_image'}));
}
self.$media.attr('src', img.src);
} else {
if (!self.$media.is('a')) {
$('.note-control-selection').hide();
self._replaceMedia($(''));
}
var href = '/web/content/' + img.id + '?';
if (img.access_token && self.options.res_model !== 'ir.ui.view') {
href += _.str.sprintf('access_token=%s&', img.access_token);
}
href += 'unique=' + img.checksum + '&download=true';
self.$media.attr('href', href);
self.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
}
self.$media.attr('alt', img.alt);
var style = self.style;
if (style) {
self.$media.css(style);
}
if (self.options.onUpload) {
// We consider that when selecting an image it is as if we upload it in the html content.
self.options.onUpload([img]);
}
// Remove crop related attributes
if (self.$media.attr('data-aspect-ratio')) {
var attrs = ['aspect-ratio', 'x', 'y', 'width', 'height', 'rotate', 'scale-x', 'scale-y'];
_.each(attrs, function (attr) {
self.$media.removeData(attr);
self.$media.removeAttr('data-' + attr);
});
}
return self.media;
});
},
/**
* @override
*/
search: function (needle, noRender) {
var self = this;
if (!noRender) {
this.$('input.url').val('').trigger('input').trigger('change');
}
var domain = this.domain.concat(['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]]);
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
}
domain.push('!', ['datas_fname', '=like', '%.crop'], '!', ['name', '=like', '%.crop']);
return this._rpc({
model: 'ir.attachment',
method: 'search_read',
args: [],
kwargs: {
domain: domain,
fields: ['name', 'datas_fname', 'mimetype', 'checksum', 'url', 'type', 'res_id', 'res_model', 'access_token'],
order: [{name: 'id', asc: false}],
context: weContext.get(),
},
}).then(function (records) {
self.records = _.chain(records)
.filter(function (r) {
return (r.type === "binary" || r.url && r.url.length > 0);
})
.uniq(function (r) {
return (r.url || r.id);
})
.sortBy(function (r) {
if (_.any(self.firstFilters, function (filter) {
var regex = new RegExp(filter, 'i');
return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex);
})) {
return -1;
}
if (_.any(self.lastFilters, function (filter) {
var regex = new RegExp(filter, 'i');
return r.name.match(regex) || r.datas_fname && r.datas_fname.match(regex);
})) {
return 1;
}
return 0;
})
.value();
_.each(self.records, function (record) {
record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, encodeURI(record.name)); // Name is added for SEO purposes
record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
if (!noRender) {
self._renderImages();
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_clear: function () {
this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' ');
},
/**
* @private
*/
_highlightSelectedImages: function () {
var self = this;
this.$('.o_existing_attachment_cell.o_selected').removeClass("o_selected");
var $select = this.$('.o_existing_attachment_cell [data-src]').filter(function () {
var $img = $(this);
return !!_.find(self.images, function (v) {
return (v.url === $img.data("src") || ($img.data("url") && v.url === $img.data("url")) || v.id === $img.data("id"));
});
});
$select.closest('.o_existing_attachment_cell').addClass("o_selected");
return $select;
},
/**
* @private
*/
_renderImages: function () {
var self = this;
var startIndex = this.page * this.IMAGES_PER_PAGE;
var rows = _(this.records).chain()
.slice(startIndex, startIndex + this.IMAGES_PER_PAGE)
.groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
.values()
.value();
this.$('.help-block').empty();
this.$('.existing-attachments').replaceWith(QWeb.render('web_editor.dialog.image.existing.content', {rows: rows}));
var $divs = this.$('.o_image');
var imageDefs = _.map($divs, function (el) {
var $div = $(el);
if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) {
var $img = $('
', {
class: 'img-responsive',
src: $div.data('url') || $div.data('src'),
});
var def = $.Deferred();
$img[0].onload = def.resolve.bind(def);
$div.addClass('o_webimage').append($img);
return def;
}
});
$.when.apply($, imageDefs).then(function () {
_.delay(function () {
$divs.removeClass('o_image_loading');
}, 400);
});
this._highlightSelectedImages();
},
/**
* @private
*/
_toggleImage: function (attachment, clearSearch, force_select) {
if (this.multiImages) {
var img = _.select(this.images, function (v) { return v.id === attachment.id; });
if (img.length) {
if (!force_select) {
this.images.splice(this.images.indexOf(img[0]),1);
}
} else {
this.images.push(attachment);
}
} else {
this.images = [attachment];
}
this._highlightSelectedImages();
if (clearSearch) {
this.search('');
}
},
/**
* @private
*/
_uploadImage: function () {
var self = this;
/**
* @todo file upload cannot be handled with _rpc smoothly. This uses the
* form posting in iframe trick to handle the upload.
*/
var $iframe = this.$('iframe');
$iframe.on('load', function () {
var iWindow = $iframe[0].contentWindow;
var attachments = iWindow.attachments;
var error = iWindow.error;
self.$('.well > span').remove();
self.$('.well > div').show();
_.each(attachments, function (record) {
record.src = record.url || '/web/image/' + record.id;
record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
if (error || !attachments.length) {
_processFile(null, error || !attachments.length);
}
self.images = attachments;
for (var i = 0 ; i < attachments.length ; i++) {
_processFile(attachments[i], error);
}
if (self.options.onUpload) {
self.options.onUpload(attachments);
}
function _processFile(attachment, error) {
var $button = self.$('.o_upload_image_button');
if (!error) {
$button.addClass('btn-success');
self._toggleImage(attachment, true);
} else {
$button.addClass('btn-danger');
self.$el.addClass('has-error').find('.help-block').text(error);
}
if (!self.multiImages) {
self.trigger_up('save_request');
}
}
});
this.$el.submit();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* @private
*/
_onImageClick: function (ev, force_select) {
var $img = $(ev.currentTarget);
var attachment = _.find(this.records, function (record) {
return record.id === $img.data('id');
});
this._toggleImage(attachment, false, force_select);
},
/**
* @private
*/
_onImageDblClick: function (ev) {
this._onImageClick(ev, true);
this.trigger_up('save_request');
},
/**
* @private
*/
_onImageSelection: function () {
var $form = this.$('form');
this.$el.addClass('nosave');
$form.removeClass('has-error').find('.help-block').empty();
this.$('.o_upload_image_button').removeClass('btn-danger btn-success');
this._uploadImage();
},
/**
* @private
*/
_onRemoveClick: function (ev) {
var self = this;
var $helpBlock = this.$('.help-block').empty();
var $a = $(ev.currentTarget);
var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(this.records, {id: id});
return this._rpc({
route: '/web_editor/attachment/remove',
params: {
ids: [id],
},
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
self._renderImages();
return;
}
$helpBlock.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', {
views: prevented[id]
}));
});
},
/**
* @private
*/
_onSearchInput: function (ev) {
var $input = $(ev.currentTarget);
var $button = $input.next('button');
var emptyValue = ($input.val() === '');
$button.toggleClass('btn-default', emptyValue).toggleClass('btn-primary', !emptyValue)
.prop('disabled', emptyValue);
},
/**
* @private
*/
_onUploadButtonClick: function () {
this.$('input[type=file]').click();
},
/**
* @private
*/
_onUploadButtonNoOptimizationClick: function () {
this.$('input[name="disable_optimization"]').val('1');
this.$('.o_upload_image_button').click();
},
/**
* @private
*/
_onUploadURLButtonClick: function () {
this._uploadImage();
},
});
/**
* Let users choose a font awesome icon, support all font awesome loaded in the
* css files.
*/
var IconWidget = MediaWidget.extend({
template: 'web_editor.dialog.font-icons',
events : {
'click .font-icons-icon': '_onIconClick',
'dblclick .font-icons-icon': '_onIconDblClick',
},
/**
* @constructor
*/
init: function (parent, media) {
this._super.apply(this, arguments);
base.computeFonts();
this.iconsParser = base.fontIcons;
this.alias = _.flatten(_.map(this.iconsParser, function (data) {
return data.alias;
}));
},
/**
* @override
*/
start: function () {
this.$icons = this.$('.font-icons-icon');
var classes = (this.media && this.media.className || '').split(/\s+/);
for (var i = 0 ; i < classes.length ; i++) {
var cls = classes[i];
if (_.contains(this.alias, cls)) {
this.selectedIcon = cls;
this._highlightSelectedIcon();
}
}
this.nonIconClasses = _.without(classes, this.selectedIcon);
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
return _.extend(this._super.apply(this, arguments), {
pagerEnabled: false,
});
},
/**
* @override
*/
save: function () {
var style = this.$media.attr('style') || '';
var iconFont = this._getFont(this.selectedIcon) || {base: 'fa', font: ''};
var finalClasses = _.uniq(this.nonIconClasses.concat([iconFont.base, iconFont.font]));
if (!this.$media.is('span')) {
var $span = $('');
$span.data(this.$media.data());
this._replaceMedia($span);
style = style.replace(/\s*width:[^;]+/, '');
}
this.$media.attr({
class: _.compact(finalClasses).join(' '),
style: style,
});
return this.media;
},
/**
* @override
*/
search: function (needle) {
var iconsParser = this.iconsParser;
if (needle && needle.length) {
iconsParser = [];
_.filter(this.iconsParser, function (data) {
var cssData = _.filter(data.cssData, function (cssData) {
return _.find(cssData.names, function (alias) {
return alias.indexOf(needle) >= 0;
});
});
if (cssData.length) {
iconsParser.push({
base: data.base,
cssData: cssData,
});
}
});
}
this.$('div.font-icons-icons').html(
QWeb.render('web_editor.dialog.font-icons.icons', {iconsParser: iconsParser})
);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_clear: function () {
this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
},
/**
* @private
*/
_getFont: function (classNames) {
if (!(classNames instanceof Array)) {
classNames = (classNames||"").split(/\s+/);
}
var fontIcon, cssData;
for (var k=0; k= 0);
this.$('input#o_video_hide_controls').prop('checked', src.indexOf('controls=0') >= 0);
this.$('input#o_video_loop').prop('checked', src.indexOf('loop=1') >= 0);
this.$('input#o_video_hide_fullscreen').prop('checked', src.indexOf('fs=0') >= 0);
this.$('input#o_video_hide_yt_logo').prop('checked', src.indexOf('modestbranding=1') >= 0);
this.$('input#o_video_hide_dm_logo').prop('checked', src.indexOf('ui-logo=0') >= 0);
this.$('input#o_video_hide_dm_share').prop('checked', src.indexOf('sharing-enable=0') >= 0);
this._updateVideo();
}
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
getControlPanelConfig: function () {
return _.extend(this._super.apply(this, arguments), {
searchEnabled: false,
pagerEnabled: false,
});
},
/**
* @override
*/
save: function () {
this._updateVideo();
if (this.$('.o_video_dialog_iframe').is('iframe')) {
this._replaceMedia($(
''
));
}
return this.media;
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_clear: function () {
if (this.media.dataset.src) {
try {
delete this.media.dataset.src;
} catch (e) {
this.media.dataset.src = undefined;
}
}
this.media.className = this.media.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
},
/**
* Creates a video node according to the given URL and options. If not
* possible, returns an error code.
*
* @private
* @param {string} url
* @param {Object} options
* @returns {Object}
* $video -> the created video jQuery node
* type -> the type of the created video
* errorCode -> if defined, either '0' for invalid URL or '1' for
* unsupported video provider
*/
_createVideoNode: function (url, options) {
options = options || {};
// Video url patterns(youtube, instagram, vimeo, dailymotion, youku, ...)
var ytRegExp = /^(?:(?:https?:)?\/\/)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?$/;
var ytMatch = url.match(ytRegExp);
var insRegExp = /(.*)instagram.com\/p\/(.[a-zA-Z0-9]*)/;
var insMatch = url.match(insRegExp);
var vinRegExp = /\/\/vine.co\/v\/(.[a-zA-Z0-9]*)/;
var vinMatch = url.match(vinRegExp);
var vimRegExp = /\/\/(player.)?vimeo.com\/([a-z]*\/)*([0-9]{6,11})[?]?.*/;
var vimMatch = url.match(vimRegExp);
var dmRegExp = /.+dailymotion.com\/(video|hub|embed)\/([^_]+)[^#]*(#video=([^_&]+))?/;
var dmMatch = url.match(dmRegExp);
var ykuRegExp = /(.*).youku\.com\/(v_show\/id_|embed\/)(.+)/;
var ykuMatch = url.match(ykuRegExp);
var $video = $('