flectra.define('web_editor.widget', function (require) {
'use strict';
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var utils = require('web.utils');
var weContext = require("web_editor.context");
var QWeb = core.qweb;
var range = $.summernote.core.range;
var dom = $.summernote.core.dom;
var _t = core._t;
/**
* Extend Dialog class to handle save/cancel of edition components.
*/
Dialog = Dialog.extend({
init: function (parent, options) {
options = options || {};
this._super(parent, _.extend({}, {
buttons: [
{text: options.save_text || _t("Save"), classes: "btn-primary o_save_button", 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);
});
},
save: function () {
this.destroyAction = "save";
this.close();
},
});
/**
* alt widget. Lets users change a alt & title on a media
*/
var alt = Dialog.extend({
template: 'web_editor.dialog.alt',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
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, '"');
},
save: function () {
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 () {
click_event(this.media, "mouseup");
}).bind(this));
return this._super.apply(this, arguments);
},
});
function click_event(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);
}
/**
* ImageDialog widget. Let users change an image, including uploading a
* new image in flectra or selecting the image style (if supported by
* the caller).
*/
var ImageDialog = Widget.extend({
template: 'web_editor.dialog.image',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
IMAGES_PER_ROW: 6,
IMAGES_ROWS: 2,
events: _.extend({}, Dialog.prototype.events, {
'change .url-source': function (e) {
this.changed($(e.target));
},
'click button.filepicker': function () {
var filepicker = this.$('input[type=file]');
if (!_.isEmpty(filepicker)) {
filepicker[0].click();
}
},
'click .js_disable_optimization': function () {
this.$('input[name="disable_optimization"]').val('1');
var filepicker = this.$('button.filepicker');
if (!_.isEmpty(filepicker)) {
filepicker[0].click();
}
},
'change input[type=file]': 'file_selection',
'submit form': 'form_submit',
'change input.url': "change_input",
'keyup input.url': "change_input",
'click .existing-attachments [data-src]': 'select_existing',
'dblclick .existing-attachments [data-src]': function (e) {
this.select_existing(e, true);
this.getParent().save();
},
'click .o_existing_attachment_remove': 'try_remove',
'keydown.dismiss.bs.modal': function () {},
}),
init: function (parent, media, options) {
this._super.apply(this, arguments);
this.options = options || {};
this.accept = this.options.accept || this.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.parent = parent;
this.old_media = media;
this.media = media;
this.images = [];
this.page = 0;
},
start: function () {
this.$preview = this.$('.preview-container').detach();
var self = this;
var res = this._super.apply(this, arguments);
var o = {url: null, alt: null};
if ($(this.media).is("img")) {
o.url = this.media.getAttribute('src');
} else if ($(this.media).is("a.o_image")) {
o.url = this.media.getAttribute('href').replace(/[?].*/, '');
o.id = +o.url.match(/\/web\/content\/([0-9]*)/, '')[1];
}
this.parent.$(".pager > li").click(function (e) {
if (!self.$el.is(':visible')) {
return;
}
e.preventDefault();
var $target = $(e.currentTarget);
if ($target.hasClass('disabled')) {
return;
}
self.page += $target.hasClass('previous') ? -1 : 1;
self.display_attachments();
});
this.fetch_existing().then(function () {
if (o.url) {
self.set_image(_.find(self.records, function (record) { return record.url === o.url;}) || o);
}
});
return res;
},
push: function (attachment, force_select) {
if (this.options.select_images) {
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];
}
},
save: function () {
var self = this;
if (this.options.select_images) {
return this.images;
}
var img = this.images[0];
if (!img) {
return this.media;
}
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 () {
var media;
if (!img.is_document) {
if (img.access_token && self.options.res_model !== 'ir.ui.view') {
img.src += _.str.sprintf('?access_token=%s', img.access_token);
}
if (self.media.tagName !== "IMG" || !self.old_media) {
self.add_class = "pull-left";
self.style = {"width": "100%"};
}
if (self.media.tagName !== "IMG") {
media = document.createElement('img');
$(self.media).replaceWith(media);
self.media = media;
}
self.media.setAttribute('src', img.src);
} else {
if (self.media.tagName !== "A") {
$('.note-control-selection').hide();
media = document.createElement('a');
$(self.media).replaceWith(media);
self.media = media;
}
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.setAttribute('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]);
}
return self.media;
});
},
clear: function () {
this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' ');
},
change_input: function (e) {
var $input = $(e.target);
var $button = $input.parent().find("button");
var emptyValue = ($input.val() === "");
$button.toggleClass("btn-default", emptyValue).toggleClass("btn-primary", !emptyValue);
},
search: function (needle) {
var self = this;
this.fetch_existing(needle).then(function () {
self.selected_existing();
});
},
set_image: function (attachment) {
this.push(attachment);
this.$('input.url').val('');
this.search();
},
form_submit: function (event) {
var self = this;
var $form = this.$('form[action="/web_editor/attachment/add"]');
if (!$form.find('input[name="upload"]').val().length) {
if (this.selected_existing().size()) {
event.preventDefault();
return false;
}
}
$form.find('.well > div').hide().last().after('');
var callback = _.uniqueId('func_');
this.$('input[name=func]').val(callback);
window[callback] = function (attachments, error) {
delete window[callback];
$form.find('.well > span').remove();
$form.find('.well > div').show();
_.each(attachments, function (record) {
record.src = record.url || '/web/image/' + record.id;
record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
if (error || !attachments.length) {
self.file_selected(null, error || !attachments.length);
}
self.images = attachments;
for (var i=0; i 0);
}), function (r) {
return (r.url || r.id);
});
_.each(this.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.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
});
this.display_attachments();
},
display_attachments: function () {
var self = this;
var per_screen = this.IMAGES_PER_ROW * this.IMAGES_ROWS;
var from = this.page * per_screen;
var records = this.records;
// Create rows of 3 records
var rows = _(records).chain()
.slice(from, from + per_screen)
.groupBy(function (_, index) { return Math.floor(index / self.IMAGES_PER_ROW); })
.values()
.value();
this.$('.help-block').empty();
this.$('.existing-attachments').replaceWith(QWeb.render('web_editor.dialog.image.existing.content', {rows: rows}));
this.parent.$('.pager')
.find('li.previous a').toggleClass('disabled', (from === 0)).end()
.find('li.next a').toggleClass('disabled', (from + per_screen >= records.length));
this.$el.find('.o_image').each(function () {
var $div = $(this);
if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) {
var $img = $('').addClass('img img-responsive').attr('src', $div.data('url') || $div.data('src'));
$div.addClass('o_webimage').append($img);
}
});
this.selected_existing();
},
select_existing: function (e, force_select) {
var $img = $(e.currentTarget);
var attachment = _.find(this.records, function (record) { return record.id === $img.data('id'); });
this.push(attachment, force_select);
this.selected_existing();
},
selected_existing: 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;
},
try_remove: function (e) {
var $help_block = this.$('.help-block').empty();
var self = this;
var $a = $(e.target);
var id = parseInt($a.data('id'), 10);
var attachment = _.findWhere(this.records, {id: id});
var $both = $a.parent().children();
$both.css({borderWidth: "5px", borderColor: "#f00"});
return this._rpc({
route: '/web_editor/attachment/remove',
params: {
ids: [id],
},
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
self.display_attachments();
return;
}
$both.css({borderWidth: "", borderColor: ""});
$help_block.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', {
views: prevented[id]
}));
});
},
});
/**
* list of font icons to load by editor. The icons are displayed in the media editor and
* identified like font and image (can be colored, spinned, resized with fa classes).
* To add font, push a new object {base, parser}
*
* - base: class who appear on all fonts (eg: fa fa-refresh)
* - parser: regular expression used to select all font in css style sheets
*
* @type Array
*/
var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}];
var cacheCssSelectors = {};
var getCssSelectors = function (filter) {
var css = [];
if (cacheCssSelectors[filter]) {
return cacheCssSelectors[filter];
}
var sheets = document.styleSheets;
for (var i = 0; i < sheets.length; i++) {
var rules;
// try...catch because browser may not able to enumerate rules for cross-domain stylesheets
try {
rules = sheets[i].rules || sheets[i].cssRules;
} catch(e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
if (rules) {
for (var r = 0; r < rules.length; r++) {
var selectorText = rules[r].selectorText;
if (selectorText) {
selectorText = selectorText.split(/\s*,\s*/);
var data = null;
for (var s = 0; s < selectorText.length; s++) {
var match = selectorText[s].match(filter);
if (match) {
var clean = match[1].slice(1).replace(/::?before$/, '');
if (!data) {
data = [match[1], rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''), clean, [clean]];
} else {
data[0] += (", " + match[1]);
data[3].push(clean);
}
}
}
if (data) {
css.push(data);
}
}
}
}
}
return cacheCssSelectors[filter] = css;
};
var computeFonts = _.once(function () {
_.each(fontIcons, function (data) {
data.cssData = getCssSelectors(data.parser);
data.alias = [];
data.icons = _.map(data.cssData, function (css) {
data.alias.push.apply(data.alias, css[3]);
return css[2];
});
});
});
/**
* FontIconsDialog widget. Lets users change a font awesome, support all
* font awesome loaded in the css files.
*/
var fontIconsDialog = Widget.extend({
template: 'web_editor.dialog.font-icons',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : _.extend({}, Dialog.prototype.events, {
'click .font-icons-icon': function (e) {
e.preventDefault();
e.stopPropagation();
this.$('#fa-icon').val(e.target.getAttribute('data-id'));
this.$(".font-icons-icon").removeClass("o_selected");
$(e.target).addClass("o_selected");
},
'dblclick .font-icons-icon': function () {
this.getParent().save();
},
'keydown.dismiss.bs.modal': function () {},
}),
init: function (parent, media) {
this._super.apply(this, arguments);
this.parent = parent;
this.media = media;
computeFonts();
},
start: function () {
return this._super.apply(this, arguments).then(this.proxy('load_data'));
},
renderElement: function () { // extract list of font (like awesome) from the cheatsheet.
this.iconsParser = fontIcons;
this.icons = _.flatten(_.map(fontIcons, function (data) { // TODO maybe useless now
return data.icons;
}));
this.alias = _.flatten(_.map(fontIcons, function (data) {
return data.alias;
}));
this._super.apply(this, arguments);
},
search: function (needle) {
var iconsParser = this.iconsParser;
if (needle) {
iconsParser = [];
_.filter(this.iconsParser, function (data) {
var cssData = _.filter(data.cssData, function (cssData) {
return _.find(cssData[3], function (alias) {
return alias.indexOf(needle) !== -1;
});
});
if (cssData.length) {
iconsParser.push({
base: data.base,
cssData: cssData
});
}
});
}
this.$('div.font-icons-icons').html(
QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
},
/**
* Removes existing FontAwesome classes on the bound element, and sets
* all the new ones if necessary.
*/
save: function () {
var self = this;
var style = this.media.attributes.style ? this.media.attributes.style.value : '';
var classes = (this.media.className||"").split(/\s+/);
var custom_classes = /^fa(-[1-5]x|spin|rotate-(9|18|27)0|flip-(horizont|vertic)al|fw|border)?$/;
var non_fa_classes = _.reject(classes, function (cls) {
return self.getFont(cls) || custom_classes.test(cls);
});
var final_classes = non_fa_classes.concat(this.get_fa_classes());
if (this.media.tagName !== "SPAN") {
var media = document.createElement('span');
$(media).data($(this.media).data());
$(this.media).replaceWith(media);
this.media = media;
style = style.replace(/\s*width:[^;]+/, '');
}
$(this.media).attr("class", _.compact(final_classes).join(' ')).attr("style", style);
return this.media;
},
/**
* return the data font object (with base, parser and icons) or null
*/
getFont: function (classNames) {
if (!(classNames instanceof Array)) {
classNames = (classNames||"").split(/\s+/);
}
var fontIcon, cssData;
for (var k=0; k= 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
*/
save: function () {
this._updateVideo();
if (this.$('.o_video_dialog_iframe').is('iframe')) {
var $content = $(
''
);
$(this.media).replaceWith($content);
this.media = $content[0];
return this.media;
}
},
/**
* @override
*/
clear: function () {
if (!this.media) {
return;
}
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, ' ');
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* 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 = $('