summaryrefslogtreecommitdiff
path: root/addons/web_editor/static/src
diff options
context:
space:
mode:
Diffstat (limited to 'addons/web_editor/static/src')
-rw-r--r--addons/web_editor/static/src/js/base.js93
-rw-r--r--addons/web_editor/static/src/js/editor/rte.js4
-rw-r--r--addons/web_editor/static/src/js/editor/rte.summernote.js7
-rw-r--r--addons/web_editor/static/src/js/editor/snippets.options.js4
-rw-r--r--addons/web_editor/static/src/js/editor/transcoder.js12
-rw-r--r--addons/web_editor/static/src/js/editor/translator.js2
-rw-r--r--addons/web_editor/static/src/js/inline.js4
-rw-r--r--addons/web_editor/static/src/js/tours/rte.js10
-rw-r--r--addons/web_editor/static/src/js/widgets/widgets.js1481
-rw-r--r--addons/web_editor/static/src/less/web_editor.ui.less16
-rw-r--r--addons/web_editor/static/src/xml/editor.xml124
11 files changed, 1010 insertions, 747 deletions
diff --git a/addons/web_editor/static/src/js/base.js b/addons/web_editor/static/src/js/base.js
index 012068776e7..d69cace3100 100644
--- a/addons/web_editor/static/src/js/base.js
+++ b/addons/web_editor/static/src/js/base.js
@@ -9,6 +9,99 @@ $(domReady.resolve.bind(domReady));
return {
/**
+ * Retrieves all the CSS rules which match the given parser (Regex).
+ *
+ * @param {Regex} filter
+ * @returns {Object[]} Array of CSS rules descriptions (objects). A rule is
+ * defined by 3 values: 'selector', 'css' and 'names'. 'selector'
+ * is a string which contains the whole selector, 'css' is a string
+ * which contains the css properties and 'names' is an array of the
+ * first captured groups for each selector part. E.g.: if the
+ * filter is set to match .fa-* rules and capture the icon names,
+ * the rule:
+ * '.fa-alias1::before, .fa-alias2::before { hello: world; }'
+ * will be retrieved as
+ * {
+ * selector: '.fa-alias1::before, .fa-alias2::before',
+ * css: 'hello: world;',
+ * names: ['.fa-alias1', '.fa-alias2'],
+ * }
+ */
+ cacheCssSelectors: {},
+ getCssSelectors: function (filter) {
+ if (this.cacheCssSelectors[filter]) {
+ return this.cacheCssSelectors[filter];
+ }
+ this.cacheCssSelectors[filter] = [];
+ var sheets = document.styleSheets;
+ for (var i = 0; i < sheets.length; i++) {
+ var rules;
+ try {
+ // try...catch because Firefox not able to enumerate
+ // document.styleSheets[].cssRules[] for cross-domain
+ // stylesheets.
+ 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) {
+ continue;
+ }
+
+ for (var r = 0 ; r < rules.length ; r++) {
+ var selectorText = rules[r].selectorText;
+ if (!selectorText) {
+ continue;
+ }
+ var selectors = selectorText.split(/\s*,\s*/);
+ var data = null;
+ for (var s = 0; s < selectors.length; s++) {
+ var match = selectors[s].trim().match(filter);
+ if (!match) {
+ continue;
+ }
+ if (!data) {
+ data = {
+ selector: match[0],
+ css: rules[r].cssText.replace(/(^.*\{\s*)|(\s*\}\s*$)/g, ''),
+ names: [match[1]]
+ };
+ } else {
+ data.selector += (', ' + match[0]);
+ data.names.push(match[1]);
+ }
+ }
+ if (data) {
+ this.cacheCssSelectors[filter].push(data);
+ }
+ }
+ }
+ return this.cacheCssSelectors[filter];
+ },
+ /**
+ * 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
+ * - parser: regular expression used to select all font in css stylesheets
+ *
+ * @type Array
+ */
+ fontIcons: [{base: 'fa', parser: /\.(fa-(?:\w|-)+)::?before/i}],
+ /**
+ * Searches the fonts described by the @see fontIcons variable.
+ */
+ computeFonts: _.once(function () {
+ var self = this;
+ _.each(this.fontIcons, function (data) {
+ data.cssData = self.getCssSelectors(data.parser);
+ data.alias = _.flatten(_.map(data.cssData, _.property('names')));
+ });
+ }),
+ /**
* If a widget needs to be instantiated on page loading, it needs to wait
* for appropriate resources to be loaded. This function returns a Deferred
* which is resolved when the dom is ready, the session is bound
diff --git a/addons/web_editor/static/src/js/editor/rte.js b/addons/web_editor/static/src/js/editor/rte.js
index 6da2ea72ea1..6974179cd39 100644
--- a/addons/web_editor/static/src/js/editor/rte.js
+++ b/addons/web_editor/static/src/js/editor/rte.js
@@ -1,12 +1,12 @@
odoo.define('web_editor.rte', function (require) {
'use strict';
+var base = require('web_editor.base');
var concurrency = require('web.concurrency');
var core = require('web.core');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var summernote = require('web_editor.summernote');
-var weWidgets = require('web_editor.widget');
var _t = core._t;
@@ -257,7 +257,7 @@ var RTEWidget = Widget.extend({
this._getConfig = getConfig || this._getDefaultConfig;
- weWidgets.computeFonts();
+ base.computeFonts();
},
/**
* @override
diff --git a/addons/web_editor/static/src/js/editor/rte.summernote.js b/addons/web_editor/static/src/js/editor/rte.summernote.js
index fb1ce3e12b4..a82514500c5 100644
--- a/addons/web_editor/static/src/js/editor/rte.summernote.js
+++ b/addons/web_editor/static/src/js/editor/rte.summernote.js
@@ -5,6 +5,7 @@ var ajax = require('web.ajax');
var Class = require('web.Class');
var core = require('web.core');
var mixins = require('web.mixins');
+var base = require('web_editor.base');
var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
var weWidgets = require('web_editor.widget');
@@ -427,8 +428,8 @@ dom.isImgFont = function (node) {
var className = (node && node.className || "");
if (node && (nodeName === "SPAN" || nodeName === "I") && className.length) {
var classNames = className.split(/\s+/);
- for (var k=0; k<weWidgets.fontIcons.length; k++) {
- if (_.intersection(weWidgets.fontIcons[k].alias, classNames).length) {
+ for (var k=0; k<base.fontIcons.length; k++) {
+ if (_.intersection(base.fontIcons[k].alias, classNames).length) {
return true;
}
}
@@ -1029,7 +1030,7 @@ var SummernoteManager = Class.extend(mixins.EventDispatcherMixin, {
return;
}
data.__alreadyDone = true;
- var altDialog = new weWidgets.alt(this,
+ var altDialog = new weWidgets.AltDialog(this,
data.options || {},
data.$editable,
data.media
diff --git a/addons/web_editor/static/src/js/editor/snippets.options.js b/addons/web_editor/static/src/js/editor/snippets.options.js
index 90807565990..f2ebdf30dab 100644
--- a/addons/web_editor/static/src/js/editor/snippets.options.js
+++ b/addons/web_editor/static/src/js/editor/snippets.options.js
@@ -5,7 +5,7 @@ var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
-var widget = require('web_editor.widget');
+var weWidgets = require('web_editor.widget');
var qweb = core.qweb;
var _t = core._t;
@@ -813,7 +813,7 @@ registry.background = SnippetOption.extend({
// Put fake image in the DOM, edit it and use it as background-image
var $image = $('<img/>', {class: 'hidden', src: value}).appendTo(this.$target);
- var _editor = new widget.MediaDialog(this, {}, null, $image[0]).open();
+ var _editor = new weWidgets.MediaDialog(this, {}, null, $image[0]).open();
_editor.opened(function () {
_editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
});
diff --git a/addons/web_editor/static/src/js/editor/transcoder.js b/addons/web_editor/static/src/js/editor/transcoder.js
index 11d23bf603b..058ea8ebcc7 100644
--- a/addons/web_editor/static/src/js/editor/transcoder.js
+++ b/addons/web_editor/static/src/js/editor/transcoder.js
@@ -1,7 +1,7 @@
odoo.define('web_editor.transcoder', function (require) {
'use strict';
-var widget = require('web_editor.widget');
+var base = require('web_editor.base');
var rulesCache = [];
@@ -156,11 +156,11 @@ function fontToImg($editable) {
$editable.find('.fa').each(function () {
var $font = $(this);
var icon, content;
- _.find(widget.fontIcons, function (font) {
- return _.find(widget.getCssSelectors(font.parser), function (css) {
- if ($font.is(css[0].replace(/::?before/g, ''))) {
- icon = css[2].split('-').shift();
- content = css[1].match(/content:\s*['"]?(.)['"]?/)[1];
+ _.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];
return true;
}
});
diff --git a/addons/web_editor/static/src/js/editor/translator.js b/addons/web_editor/static/src/js/editor/translator.js
index c28a578fac0..e3ba65a54d7 100644
--- a/addons/web_editor/static/src/js/editor/translator.js
+++ b/addons/web_editor/static/src/js/editor/translator.js
@@ -49,7 +49,7 @@ var AttributeTranslateDialog = weWidgets.Dialog.extend({
this._super(parent, _.extend({
title: _t("Translate Attribute"),
buttons: [
- {text: _t("Close"), classes: 'btn-primary o_save_button', click: this.save}
+ {text: _t("Close"), classes: 'btn-primary', click: this.save}
],
}, options || {}));
this.$target = $(node);
diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js
index ac0dee435ae..7480b2228fa 100644
--- a/addons/web_editor/static/src/js/inline.js
+++ b/addons/web_editor/static/src/js/inline.js
@@ -4,11 +4,11 @@ odoo.define('web_editor.inline', function (require) {
var core = require('web.core');
var editor = require('web_editor.editor');
var rte = require('web_editor.rte');
-var widget = require('web_editor.widget');
+var weWidgets = require('web_editor.widget');
var transcoder = require('web_editor.transcoder');
var snippet_editor = require('web_editor.snippet.editor');
-widget.MediaDialog.include({
+weWidgets.MediaDialog.include({
start: function () {
this.$('[href="#editor-media-video"]').addClass('hidden');
return this._super.apply(this, arguments);
diff --git a/addons/web_editor/static/src/js/tours/rte.js b/addons/web_editor/static/src/js/tours/rte.js
index 0714afadc6f..a2108208912 100644
--- a/addons/web_editor/static/src/js/tours/rte.js
+++ b/addons/web_editor/static/src/js/tours/rte.js
@@ -157,7 +157,7 @@ tour.register('rte', {
trigger: '#editor-media-icon.active span.fa:first',
}, {
content: "save pictogram",
- trigger: 'button.o_save_button',
+ trigger: '.modal-footer > .btn-primary',
extra_trigger: '#editor-media-icon.active span.o_selected',
}, {
content: "select a size for the pictogram",
@@ -176,7 +176,7 @@ tour.register('rte', {
extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
}, {
content: "insert a link url",
- trigger: '#o_link_dialog_url_input',
+ trigger: 'input[name="url"]',
extra_trigger: 'a#link-preview.btn',
run: "text http://www.odoo.com",
}, {
@@ -185,7 +185,7 @@ tour.register('rte', {
run: "text ABC[IMG] DEF",
}, {
content: "save link",
- trigger: 'button.o_save_button',
+ trigger: '.modal-footer > .btn-primary',
extra_trigger: 'a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
}, {
content: "click on other picture",
@@ -201,12 +201,12 @@ tour.register('rte', {
extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
}, {
content: "insert an email",
- trigger: '#o_link_dialog_url_input',
+ trigger: 'input[name="url"]',
extra_trigger: 'a#link-preview.btn',
run: "text test@test.test",
}, {
content: "save link",
- trigger: 'button.o_save_button',
+ trigger: '.modal-footer > .btn-primary',
extra_trigger: 'a#link-preview.btn[href="mailto:test@test.test"]',
}, {
content: "select for triple enter then double backspace",
diff --git a/addons/web_editor/static/src/js/widgets/widgets.js b/addons/web_editor/static/src/js/widgets/widgets.js
index e499e83843d..2be8b8d394d 100644
--- a/addons/web_editor/static/src/js/widgets/widgets.js
+++ b/addons/web_editor/static/src/js/widgets/widgets.js
@@ -1,6 +1,7 @@
odoo.define('web_editor.widget', function (require) {
'use strict';
+var base = require('web_editor.base');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
@@ -13,28 +14,49 @@ 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 o_save_button", click: this.save},
+ {text: options.save_text || _t("Save"), classes: 'btn-primary', click: this.save},
{text: _t("Discard"), close: true}
]
}, options));
- this.destroyAction = "cancel";
+ this.destroyAction = 'cancel';
var self = this;
this.opened().then(function () {
self.$('input:first').focus();
});
- this.on("closed", this, function () {
+ 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();
@@ -42,14 +64,17 @@ Dialog = Dialog.extend({
});
/**
- * alt widget. Lets users change a alt & title on a media
+ * Let users change the alt & title of a media.
*/
-var alt = Dialog.extend({
+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")
@@ -59,229 +84,233 @@ var alt = Dialog.extend({
this.alt = ($(this.media).attr('alt') || "").replace(/&quot;/g, '"');
this.title = ($(this.media).attr('title') || "").replace(/&quot;/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, "&quot;") : null).attr('title', title ? title.replace(/"/g, "&quot;") : null);
- _.defer((function () {
- click_event(this.media, "mouseup");
- }).bind(this));
+ _.defer(function () {
+ simulateMouseEvent(self.media, 'mouseup');
+ });
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);
-}
+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];
+ },
+});
/**
- * ImageDialog widget. Let users change an image, including uploading a
- * new image in odoo or selecting the image style (if supported by
- * the caller).
+ * Let users choose an image, including uploading a new image in odoo.
*/
-var ImageDialog = Widget.extend({
+var ImageWidget = MediaWidget.extend({
template: 'web_editor.dialog.image',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
+ 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,
- 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 () {
- this.getParent().save();
- },
- 'click .o_existing_attachment_remove': 'try_remove',
- 'keydown.dismiss.bs.modal': function () {},
- }),
+
+ /**
+ * @constructor
+ */
init: function (parent, media, options) {
this._super.apply(this, arguments);
- this.options = options || {};
- this.accept = this.options.accept || this.options.document ? "*/*" : "image/*";
- this.domain = this.options.domain || ['|', ['mimetype', '=', false], ['mimetype', this.options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]];
- this.parent = parent;
- this.old_media = media;
- this.media = media;
+
+ this.IMAGES_PER_PAGE = this.IMAGES_PER_ROW * this.IMAGES_ROWS;
+
+ this.accept = options.accept || (options.document ? '*/*' : 'image/*');
+ this.domain = options.domain
+ || [
+ '|',
+ ['mimetype', '=', false],
+ ['mimetype', options.document ? 'not in' : 'in', ['image/gif', 'image/jpe', 'image/jpeg', 'image/jpg', 'image/gif', 'image/png']]
+ ];
+ this.multiImages = options.multiImages;
+
this.images = [];
- this.page = 0;
},
+ /**
+ * @override
+ */
+ willStart: function () {
+ return $.when(
+ this._super.apply(this, arguments),
+ this.search('', true)
+ );
+ },
+ /**
+ * @override
+ */
start: function () {
- this.$preview = this.$('.preview-container').detach();
+ var def = this._super.apply(this, arguments);
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._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];
}
- 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);
- }
+ 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,
});
- return res;
},
- push: function (attachment) {
- if (this.options.select_images) {
- var img = _.select(this.images, function (v) { return v.id === attachment.id; });
- if (img.length) {
- this.images.splice(this.images.indexOf(img[0]),1);
- } else {
- this.images.push(attachment);
- }
- } else {
- this.images = [attachment];
- }
+ /**
+ * @override
+ */
+ goToPage: function (page) {
+ this._super.apply(this, arguments);
+ this._renderImages();
},
+ /**
+ * @override
+ */
save: function () {
- if (this.options.select_images) {
+ if (this.multiImages) {
return this.images;
}
var img = this.images[0];
- if (!img) {
- var id = this.$(".existing-attachments [data-src]:first").data('id');
- img = _.find(this.images, function (img) { return img.id === id;});
- }
-
- var media;
- if (!img.is_document) {
- if (this.media.tagName !== "IMG" || !this.old_media) {
- this.add_class = "pull-left";
- this.style = {"width": "100%"};
+ if (!img.isDocument) {
+ if (!this.$media.is('img')) {
+ this._replaceMedia($('<img/>', {class: 'img img-responsive'}));
}
- if (this.media.tagName !== "IMG") {
- media = document.createElement('img');
- $(this.media).replaceWith(media);
- this.media = media;
- }
- this.media.setAttribute('src', img.src);
+ this.$media.attr('src', img.src);
} else {
- if (this.media.tagName !== "A") {
+ if (!this.$media.is('a')) {
$('.note-control-selection').hide();
- media = document.createElement('a');
- $(this.media).replaceWith(media);
- this.media = media;
+ this._replaceMedia($('<a/>'));
}
- this.media.setAttribute('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true');
- $(this.media).addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
+ this.$media.attr('href', '/web/content/' + img.id + '?unique=' + img.checksum + '&download=true');
+ this.$media.addClass('o_image').attr('title', img.name).attr('data-mimetype', img.mimetype);
}
- $(this.media).attr('alt', img.alt);
+ this.$media.attr('alt', img.alt);
var style = this.style;
- if (style) { $(this.media).css(style); }
+ if (style) {
+ this.$media.css(style);
+ }
return this.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) {
+ /**
+ * @override
+ */
+ search: function (needle, noRender) {
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('<span class="fa fa-spin fa-3x fa-refresh"/>');
-
- 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<attachments.length; i++) {
- self.file_selected(attachments[i], error);
- }
- };
- },
- file_selection: function () {
- var $form = this.$('form');
- this.$el.addClass('nosave');
- $form.removeClass('has-error').find('.help-block').empty();
- this.$('button.filepicker').removeClass('btn-danger btn-success');
- $form.submit();
- },
- file_selected: function (attachment, error) {
- var $button = this.$('button.filepicker');
- if (!error) {
- $button.addClass('btn-success');
- this.set_image(attachment);
- } else {
- this.$('form').addClass('has-error').find('.help-block').text(error);
- $button.addClass('btn-danger');
+ if (!noRender) {
+ this.$('input.url').val('').trigger('input').trigger('change');
}
-
- if (!this.options.select_images) {
- this.parent.save(); // auto save and close popup
- }
- },
- fetch_existing: function (needle) {
var domain = [['res_model', '=', 'ir.ui.view']].concat(this.domain);
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
@@ -295,77 +324,192 @@ var ImageDialog = Widget.extend({
fields: ['name', 'mimetype', 'checksum', 'url', 'type'],
order: [{name: 'id', asc: false}],
context: weContext.get(),
+ },
+ }).then(function (records) {
+ self.records = _.uniq(_.filter(records, function (r) {
+ return (r.type === "binary" || r.url && r.url.length > 0);
+ }), function (r) {
+ return (r.url || r.id);
+ });
+ _.each(self.records, function (record) {
+ record.src = record.url || _.str.sprintf('/web/image/%s/%s', record.id, record.name); // Name is added for SEO purposes
+ record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype));
+ });
+ if (!noRender) {
+ self._renderImages();
}
- }).then(this.proxy('fetched_existing'));
- },
- fetched_existing: function (records) {
- this.records = _.uniq(_.filter(records, function (r) {
- return (r.type === "binary" || r.url && r.url.length > 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, record.name); // Name is added for SEO purposes
- record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype));
+ },
+
+ //--------------------------------------------------------------------------
+ // 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"));
+ });
});
- this.display_attachments();
+ $select.closest('.o_existing_attachment_cell').addClass("o_selected");
+ return $select;
},
- display_attachments: function () {
+ /**
+ * @private
+ */
+ _renderImages: 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); })
+ 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}));
- 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);
+ 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 = $('<img/>').addClass('img img-responsive').attr('src', $div.data('url') || $div.data('src'));
+ var $img = $('<img/>', {
+ class: 'img 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;
}
});
- this.selected_existing();
+ $.when.apply($, imageDefs).then(function () {
+ _.delay(function () {
+ $divs.removeClass('o_image_loading');
+ }, 400);
+ });
+ this._highlightSelectedImages();
},
- select_existing: function (e) {
- var $img = $(e.currentTarget);
- var attachment = _.find(this.records, function (record) { return record.id === $img.data('id'); });
- this.push(attachment);
- this.selected_existing();
+ /**
+ * @private
+ */
+ _toggleImage: function (attachment, clearSearch) {
+ if (this.multiImages) {
+ var img = _.select(this.images, function (v) { return v.id === attachment.id; });
+ if (img.length) {
+ this.images.splice(this.images.indexOf(img[0]),1);
+ } else {
+ this.images.push(attachment);
+ }
+ } else {
+ this.images = [attachment];
+ }
+ this._highlightSelectedImages();
+
+ if (clearSearch) {
+ this.search('');
+ }
},
- selected_existing: function () {
+ /**
+ * @private
+ */
+ _uploadImage: 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"));
+
+ /**
+ * @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);
+ }
+
+ 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');
+ }
+ }
});
- $select.closest('.o_existing_attachment_cell').addClass("o_selected");
- return $select;
+ this.$el.submit();
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onImageClick: function (ev) {
+ var $img = $(ev.currentTarget);
+ var attachment = _.find(this.records, function (record) {
+ return record.id === $img.data('id');
+ });
+ this._toggleImage(attachment);
},
- try_remove: function (e) {
- var $help_block = this.$('.help-block').empty();
+ /**
+ * @private
+ */
+ _onImageDblClick: function (ev) {
+ 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 $a = $(e.target);
+ var $helpBlock = this.$('.help-block').empty();
+ var $a = $(ev.currentTarget);
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',
@@ -375,177 +519,157 @@ var ImageDialog = Widget.extend({
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
- self.display_attachments();
+ self._renderImages();
return;
}
- $both.css({borderWidth: "", borderColor: ""});
- $help_block.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', {
+ $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();
+ },
});
/**
- * 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;
- if (sheets[i].rules) {
- rules = sheets[i].rules;
- } else {
- //try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
- try {
- 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.
+ * Let users choose a font awesome icon, support all font awesome loaded in the
+ * css files.
*/
-var fontIconsDialog = Widget.extend({
+var IconWidget = MediaWidget.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 () {},
- }),
+ events : {
+ 'click .font-icons-icon': '_onIconClick',
+ 'dblclick .font-icons-icon': '_onIconDblClick',
+ },
+ /**
+ * @constructor
+ */
init: function (parent, media) {
this._super.apply(this, arguments);
- this.parent = parent;
- this.media = media;
- computeFonts();
+
+ base.computeFonts();
+ this.iconsParser = base.fontIcons;
+ this.alias = _.flatten(_.map(this.iconsParser, function (data) {
+ return data.alias;
+ }));
},
+ /**
+ * @override
+ */
start: function () {
- return this._super.apply(this, arguments).then(this.proxy('load_data'));
+ 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);
},
- 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);
+
+ //--------------------------------------------------------------------------
+ // 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/>');
+ $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) {
+ if (needle && needle.length) {
iconsParser = [];
_.filter(this.iconsParser, function (data) {
var cssData = _.filter(data.cssData, function (cssData) {
- return _.find(cssData[3], function (alias) {
- return alias.indexOf(needle) !== -1;
+ return _.find(cssData.names, function (alias) {
+ return alias.indexOf(needle) >= 0;
});
});
if (cssData.length) {
iconsParser.push({
base: data.base,
- cssData: cssData
+ cssData: cssData,
});
}
});
}
this.$('div.font-icons-icons').html(
- QWeb.render('web_editor.dialog.font-icons.icons', {'iconsParser': iconsParser}));
+ QWeb.render('web_editor.dialog.font-icons.icons', {iconsParser: iconsParser})
+ );
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
- * Removes existing FontAwesome classes on the bound element, and sets
- * all the new ones if necessary.
+ * @override
*/
- 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;
+ _clear: function () {
+ this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
},
/**
- * return the data font object (with base, parser and icons) or null
+ * @private
*/
- getFont: function (classNames) {
+ _getFont: function (classNames) {
if (!(classNames instanceof Array)) {
classNames = (classNames||"").split(/\s+/);
}
@@ -554,82 +678,55 @@ var fontIconsDialog = Widget.extend({
fontIcon = this.iconsParser[k];
for (var s=0; s<fontIcon.cssData.length; s++) {
cssData = fontIcon.cssData[s];
- if (_.intersection(classNames, cssData[3]).length) {
+ if (_.intersection(classNames, cssData.names).length) {
return {
- 'base': fontIcon.base,
- 'parser': fontIcon.parser,
- 'icons': fontIcon.icons,
- 'font': cssData[2]
+ base: fontIcon.base,
+ parser: fontIcon.parser,
+ font: cssData.names[0],
};
}
}
}
return null;
},
-
/**
- * Looks up the various FontAwesome classes on the bound element and
- * sets the corresponding template/form elements to the right state.
- * If multiple classes of the same category are present on an element
- * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
- * which may not match the visual look of the element.
+ * @private
*/
- load_data: function () {
- var classes = (this.media&&this.media.className||"").split(/\s+/);
- for (var i = 0; i < classes.length; i++) {
- var cls = classes[i];
- switch (cls) {
- case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
- // size classes
- this.$('#fa-size').val(cls);
- continue;
- case 'fa-spin':
- case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
- case 'fa-flip-horizontal':case 'fa-rotate-vertical':
- this.$('#fa-rotation').val(cls);
- continue;
- case 'fa-fw':
- continue;
- case 'fa-border':
- this.$('#fa-border').prop('checked', true);
- continue;
- case '': continue;
- default:
- this.$(".font-icons-icon").removeClass("o_selected").filter("[data-alias*=',"+cls+",']").addClass("o_selected");
- if (this.alias.indexOf(cls) !== -1) {
- this.$('#fa-icon').val(cls);
- }
- }
- }
+ _highlightSelectedIcon: function () {
+ var self = this;
+ this.$icons.removeClass('o_selected');
+ this.$icons.filter(function (i, el) {
+ return _.contains($(el).data('alias').split(','), self.selectedIcon);
+ }).addClass('o_selected');
},
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
/**
- * Serializes the dialog to an array of FontAwesome classes. Includes the base ``fa``.
+ * @private
*/
- get_fa_classes: function () {
- var font = this.getFont(this.$('#fa-icon').val());
- return [
- font ? font.base : 'fa',
- font ? font.font : "",
- this.$('#fa-size').val(),
- this.$('#fa-rotation').val(),
- this.$('#fa-border').prop('checked') ? 'fa-border' : ''
- ];
+ _onIconClick: function (ev) {
+ ev.preventDefault();
+ ev.stopPropagation();
+
+ this.selectedIcon = $(ev.currentTarget).data('id');
+ this._highlightSelectedIcon();
},
- clear: function () {
- if (!this.media) {
- return;
- }
- this.media.className = this.media.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
+ /**
+ * @private
+ */
+ _onIconDblClick: function () {
+ this.trigger_up('save_request');
},
});
/**
- * VideoDialog widget. Let users change a video, support all summernote
- * video, and embed iframe.
+ * Let users choose a video, support all summernote video, and embed iframe.
*/
-var VideoDialog = Widget.extend({
+var VideoWidget = MediaWidget.extend({
template: 'web_editor.dialog.video',
- xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : {
'change .o_video_dialog_options input': '_onUpdateVideoOption',
'input textarea#o_video_text': '_onVideoCodeInput',
@@ -641,15 +738,12 @@ var VideoDialog = Widget.extend({
*/
init: function (parent, media) {
this._super.apply(this, arguments);
- this.parent = parent;
- this.media = media;
this._onVideoCodeInput = _.debounce(this._onVideoCodeInput, 1000);
},
/**
* @override
*/
start: function () {
- this.$preview = this.$('.preview-container').detach();
this.$content = this.$('.o_video_dialog_iframe');
var $media = $(this.media);
@@ -678,29 +772,37 @@ var VideoDialog = Widget.extend({
/**
* @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')) {
- var $content = $(
+ this._replaceMedia($(
'<div class="media_iframe_video" data-oe-expression="' + this.$content.attr('src') + '">'+
'<div class="css_editable_mode_display">&nbsp;</div>'+
'<div class="media_iframe_video_size" contenteditable="false">&nbsp;</div>'+
'<iframe src="' + this.$content.attr('src') + '" frameborder="0" contenteditable="false"></iframe>'+
'</div>'
- );
- $(this.media).replaceWith($content);
- this.media = $content[0];
- return this.media;
+ ));
}
+ return this.media;
},
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
/**
* @override
*/
- clear: function () {
- if (!this.media) {
- return;
- }
+ _clear: function () {
if (this.media.dataset.src) {
try {
delete this.media.dataset.src;
@@ -710,11 +812,6 @@ var VideoDialog = Widget.extend({
}
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.
@@ -878,7 +975,6 @@ var VideoDialog = Widget.extend({
this.$content = $content;
},
-
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
@@ -914,9 +1010,7 @@ var VideoDialog = Widget.extend({
/**
* MediaDialog widget. Lets users change a media, including uploading a
* new image, font awsome or video and can change a media into an other
- * media
- *
- * options: select_images: allow the selection of more of one image
+ * media.
*/
var MediaDialog = Dialog.extend({
template: 'web_editor.dialog.media',
@@ -924,167 +1018,232 @@ var MediaDialog = Dialog.extend({
['/web_editor/static/src/xml/editor.xml']
),
events : _.extend({}, Dialog.prototype.events, {
- 'input input#icon-search': 'search',
+ 'input input#icon-search': '_onSearchInput',
+ 'shown.bs.tab a[data-toggle="tab"]': '_onTabChange',
+ 'click .pager > li > a:not(.disabled)': '_onPagerClick',
+ }),
+ custom_events: _.extend({}, Dialog.prototype.custom_events || {}, {
+ save_request: '_onSaveRequest',
}),
+
+ /**
+ * @constructor
+ */
init: function (parent, options, $editable, media) {
+ var self = this;
this._super(parent, _.extend({}, {
title: _t("Select a Media"),
}, options));
+
if ($editable) {
this.$editable = $editable;
this.rte = this.$editable.rte || this.$editable.data('rte');
}
- this.options = options || {};
- this.old_media = media;
+
this.media = media;
- this.isNewMedia = !media;
+ this.$media = $(media);
this.range = range.create();
+
+ this.multiImages = options.multiImages;
+ this.onlyImages = options.onlyImages || this.multiImages || (this.media && (this.$media.parent().data('oeField') === 'image' || this.$media.parent().data('oeType') === 'image'));
+
+ this.imageDialog = new ImageWidget(this, this.media, options);
+ this.documentDialog = new ImageWidget(this, this.media, _.extend({}, options, {document: true}));
+ if (!this.onlyImages) {
+ this.iconDialog = new IconWidget(this, this.media, options);
+ this.videoDialog = new VideoWidget(this, this.media, options);
+ }
+
+ this.opened(function () {
+ if (!self.media) {
+ return;
+ }
+ if (self.$media.is('img')) {
+ self.$('[href="#editor-media-image"]').tab('show');
+ } else if (self.$media.is('a.o_image')) {
+ self.$('[href="#editor-media-document"]').tab('show');
+ } else if (self.$media.attr('class').match(/(^|\s)media_iframe_video($|\s)/)) {
+ self.$('[href="#editor-media-video"]').tab('show');
+ } else if (self.$media.parent().attr('class').match(/(^|\s)media_iframe_video($|\s)/)) {
+ self.$media = self.$media.parent();
+ self.media = self.$media[0];
+ self.$('[href="#editor-media-video"]').tab('show');
+ } else if (self.$media.attr('class').match(/(^|\s)fa($|\s)/)) {
+ self.$('[href="#editor-media-icon"]').tab('show');
+ }
+ });
+
+ this._onSearchInput = _.debounce(this._onSearchInput, 250);
},
+ /**
+ * @override
+ */
start: function () {
var self = this;
-
+ var defs = [this._super.apply(this, arguments)];
this.$modal.addClass('note-image-dialog');
this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
- this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image"));
- if (this.only_images) {
- this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
+ if (this.imageDialog) {
+ defs.push(this.imageDialog.appendTo(this.$("#editor-media-image")));
}
-
- this.opened((function () {
- if (this.media) {
- if (this.media.nodeName === "IMG") {
- this.$('[href="#editor-media-image"]').tab('show');
- } else if ($(this.media).is('a.o_image')) {
- this.$('[href="#editor-media-document"]').tab('show');
- } else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
- this.$('[href="#editor-media-video"]').tab('show');
- } else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
- this.media = this.media.parentNode;
- this.$('[href="#editor-media-video"]').tab('show');
- } else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
- this.$('[href="#editor-media-icon"]').tab('show');
- }
- }
- }).bind(this));
-
- this.imageDialog = new ImageDialog(this, this.media, this.options);
- this.imageDialog.appendTo(this.$("#editor-media-image"));
- this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
- this.documentDialog.appendTo(this.$("#editor-media-document"));
- if (!this.only_images) {
- this.iconDialog = new fontIconsDialog(this, this.media, this.options);
- this.iconDialog.appendTo(this.$("#editor-media-icon"));
- this.videoDialog = new VideoDialog(this, this.media, this.options);
- this.videoDialog.appendTo(this.$("#editor-media-video"));
+ if (this.documentDialog) {
+ defs.push(this.documentDialog.appendTo(this.$("#editor-media-document")));
+ }
+ if (this.iconDialog) {
+ defs.push(this.iconDialog.appendTo(this.$("#editor-media-icon")));
+ }
+ if (this.videoDialog) {
+ defs.push(this.videoDialog.appendTo(this.$("#editor-media-video")));
}
- this.active = this.imageDialog;
-
- this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
- if ($(event.target).is('[href="#editor-media-image"]')) {
- self.active = self.imageDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-document"]')) {
- self.active = self.documentDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-icon"]')) {
- self.active = self.iconDialog;
- self.$('li.search, li.previous, li.next').removeClass("hidden");
- self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
- } else if ($(event.target).is('[href="#editor-media-video"]')) {
- self.active = self.videoDialog;
- self.$('.nav-tabs li.search').addClass("hidden");
- }
+ return $.when.apply($, defs).then(function () {
+ self._setActive(self.imageDialog);
});
-
- return this._super.apply(this, arguments);
},
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
save: function () {
- if (this.options.select_images) {
+ var self = this;
+ if (this.multiImages) {
+ // In the case of multi images selection we suppose this was not to
+ // replace an old media, so we only retrieve the images and save.
this.final_data = this.active.save();
- this._super.apply(this, arguments);
- return;
+ return this._super.apply(this, arguments);
}
+
if (this.rte) {
this.range.select();
this.rte.historyRecordUndo(this.media);
}
- var self = this;
- if (self.media) {
- this.media.innerHTML = "";
- if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
- this.imageDialog.clear();
- }
- // if not mode only_images
- if (this.iconDialog && this.active !== this.iconDialog) {
- this.iconDialog.clear();
- }
- if (this.videoDialog && this.active !== this.videoDialog) {
- this.videoDialog.clear();
- }
- } else {
- this.media = document.createElement("img");
- this.range.insertNode(this.media, true);
- this.active.media = this.media;
+ if (this.media) {
+ this.$media.html('');
+ _.each(['imageDialog', 'documentDialog', 'iconDialog', 'videoDialog'], function (v) {
+ // Note: hack since imageDialog is the same type as the documentDialog
+ if (self[v] && self.active._clear.toString() !== self[v]._clear.toString()) {
+ self[v].clear();
+ }
+ });
}
- this.active.save();
-
- if (this.active.add_class) {
- $(this.active.media).addClass(this.active.add_class);
+ var media = this.active.save();
+ if (!this.media) {
+ this.range.insertNode(media, true);
}
- var media = this.active.media;
+ this.media = media;
+ this.$media = $(media);
- this.final_data = [media, self.old_media];
- $(document.body).trigger("media-saved", this.final_data);
- $(self.old_media).trigger("save", this.final_data);
- $(this.final_data).trigger('input');
+ this.final_data = this.media;
+ $(this.final_data).trigger('input').trigger('save');
+ $(document.body).trigger("media-saved", this.final_data); // TODO get rid of this
// Update editor bar after image edition (in case the image change to icon or other)
_.defer(function () {
- if (!media.parentNode) return;
- range.createFromNode(media).select();
- click_event(media, "mousedown");
- click_event(media, "mouseup");
+ if (!self.media.parentNode) {
+ return;
+ }
+ range.createFromNode(self.media).select();
+ simulateMouseEvent(self.media, 'mousedown');
+ simulateMouseEvent(self.media, 'mouseup');
});
- this._super.apply(this, arguments);
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ //
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _setActive: function (widget) {
+ this.active = widget;
+ this.active.goToPage(0);
+ this._updateControlPanel();
},
- searchTimer: null,
- search: function () {
+ /**
+ * @private
+ */
+ _updateControlPanel: function () {
+ var cpConfig = this.active.getControlPanelConfig();
+ this.$('li.search').toggleClass("hidden", !cpConfig.searchEnabled);
+ this.$('li.previous, li.next').toggleClass("hidden", !cpConfig.pagerEnabled);
+ this.$('li.previous > a').toggleClass("disabled", !cpConfig.pagerLeftEnabled);
+ this.$('li.next > a').toggleClass("disabled", !cpConfig.pagerRightEnabled);
+ },
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onPagerClick: function (ev) {
+ this.active.goToPage(this.active.page + ($(ev.currentTarget).parent().hasClass('previous') ? -1 : 1));
+ this._updateControlPanel();
+ },
+ /**
+ * @private
+ */
+ _onSaveRequest: function (ev) {
+ ev.stopPropagation();
+ this.save();
+ },
+ /**
+ * @private
+ */
+ _onSearchInput: function (ev) {
var self = this;
- var needle = this.$("input#icon-search").val();
- clearTimeout(this.searchTimer);
- this.searchTimer = setTimeout(function () {
- self.active.search(needle || "");
- },250);
- }
+ this.active.goToPage(0);
+ this.active.search($(ev.currentTarget).val() || '').then(function () {
+ self._updateControlPanel();
+ });
+ },
+ /**
+ * @private
+ */
+ _onTabChange: function (ev) {
+ var $target = $(ev.target);
+ if ($target.is('[href="#editor-media-image"]')) {
+ this._setActive(this.imageDialog);
+ } else if ($target.is('[href="#editor-media-document"]')) {
+ this._setActive(this.documentDialog);
+ } else if ($target.is('[href="#editor-media-icon"]')) {
+ this._setActive(this.active = this.iconDialog);
+ } else if ($target.is('[href="#editor-media-video"]')) {
+ this._setActive(this.active = this.videoDialog);
+ }
+ },
});
/**
- * The Link Dialog allows to customize link content and style.
+ * Allows to customize link content and style.
*/
var LinkDialog = Dialog.extend({
template: 'web_editor.dialog.link',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
- events: _.extend({}, Dialog.prototype.events, {
- 'change :input.url-source': 'changed',
- 'keyup :input.url': 'onkeyup',
- 'keyup :input': 'preview',
- 'click button.remove': 'remove_link',
- 'change input#o_link_dialog_label_input': function (e) {
- this.text = $(e.target).val();
- },
- 'change .link-style': function (e) {
- this.preview();
- },
+ events: _.extend({}, Dialog.prototype.events || {}, {
+ 'input': '_onAnyChange',
+ 'change': '_onAnyChange',
+ 'input input[name="url"]': '_onURLInput',
}),
+
+ /**
+ * @constructor
+ */
init: function (parent, options, editable, linkInfo) {
- this._super(parent, _.extend({}, {
- title: _t("Link to")
+ this._super(parent, _.extend({
+ title: _t("Link to"),
}, options || {}));
this.editable = editable;
this.data = linkInfo || {};
@@ -1168,132 +1327,142 @@ var LinkDialog = Dialog.extend({
this.data.text = this.data.text.replace(/[ \t\r\n]+/g, ' ');
},
+ /**
+ * @override
+ */
start: function () {
- this.bind_data();
- this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active');
- this.$('#o_link_dialog_label_input').focus();
+ var self = this;
+
+ this.$('input.link-style').prop('checked', false).first().prop('checked', true);
+ if (this.data.iniClassName) {
+ _.each(this.$('input.link-style, select.link-style > option'), function (el) {
+ var $option = $(el);
+ if ($option.val() && self.data.iniClassName.indexOf($option.val()) >= 0) {
+ if ($option.is("input")) {
+ $option.prop("checked", true);
+ } else {
+ $option.parent().val($option.val());
+ }
+ }
+ });
+ }
+ if (this.data.url) {
+ var match = /mailto:(.+)/.exec(this.data.url);
+ this.$('input[name="url"]').val(match ? match[1] : this.data.url);
+ }
+
+ this._adaptPreview();
+
+ this.$('input:visible:first').focus();
+
return this._super.apply(this, arguments);
},
- get_data: function (test) {
- var self = this;
- var def = new $.Deferred();
- var $e = this.$('.active input.url-source');
- if (!$e.length) {
- $e = this.$('input.url-source:first');
+
+ //--------------------------------------------------------------------------
+ // Public
+ //--------------------------------------------------------------------------
+
+ /**
+ * @override
+ */
+ save: function () {
+ var data = this._getData();
+ if (data === null) {
+ var $url = this.$('input[name="url"]');
+ $url.closest('.form-group').addClass('has-error');
+ $url.focus();
+ return $.Deferred().reject();
+ }
+ this.data.text = data.label;
+ this.data.url = data.url;
+ this.data.className = data.classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');
+ if (data.classes.replace(/(^|[ ])(btn-default|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi, ' ')) {
+ this.data.style = {'background-color': '', 'color': ''};
+ }
+ this.data.isNewWindow = data.isNewWindow;
+ this.final_data = this.data;
+ return this._super.apply(this, arguments);
+ },
+
+ //--------------------------------------------------------------------------
+ // Private
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _adaptPreview: function () {
+ var $preview = this.$("#link-preview");
+ var data = this._getData();
+ if (data === null) {
+ return;
}
- var val = $e.val();
- var label = this.$('#o_link_dialog_label_input').val() || val;
+ $preview.attr({
+ target: data.isNewWindow ? '_blank' : '',
+ href: data.url && data.url.length ? data.url : '#',
+ class: data.classes.replace(/pull-\w+/, '') + ' o_btn_preview',
+ }).html((data.label && data.label.length) ? data.label : data.url);
+ },
+ /**
+ * @private
+ */
+ _getData: function () {
+ var $url = this.$('input[name="url"]');
+ var url = $url.val();
+ var label = this.$('input[name="label"]').val() || url;
if (label && this.data.images) {
- for (var i=0; i<this.data.images.length; i++) {
+ for (var i = 0 ; i < this.data.images.length ; i++) {
label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
}
}
- if (!test && $e.prop('required') && (!val || !$e[0].checkValidity())) {
- // FIXME: error message
- $e.closest('.form-group').addClass('has-error');
- $e.focus();
- def.reject();
+ if ($url.prop('required') && (!url || !$url[0].checkValidity())) {
+ return null;
}
- var style = this.$("input[name='link-style-type']:checked").val() || '';
- var size = this.$("select.link-style").val() || '';
- var classes = (this.data.className || "") + (style && style.length ? " btn " : "") + style + " " + size;
- var isNewWindow = this.$('input.window-new').prop('checked');
+ var style = this.$('input[name="link_style_color"]:checked').val() || '';
+ var size = this.$('select[name="link_style_size"]').val() || '';
+ var classes = (this.data.className || '')
+ + ((style && style.length) ? (' btn btn-' + style) : '')
+ + ((size && size.length) ? (' btn-' + size) : '');
+ var isNewWindow = this.$('input[name="is_new_window"]').prop('checked');
- if ($e.hasClass('email-address') && $e.val().indexOf("@") !== -1) {
- self.get_data_buy_mail(def, $e, isNewWindow, label, classes, test);
- } else {
- self.get_data_buy_url(def, $e, isNewWindow, label, classes, test);
+ if (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0) {
+ url = ('mailto:' + url);
}
- return def;
+ return {
+ label: label,
+ url: url,
+ classes: classes,
+ isNewWindow: isNewWindow,
+ };
},
- get_data_buy_mail: function (def, $e, isNewWindow, label, classes, test) {
- var val = $e.val();
- def.resolve(val.indexOf("mailto:") === 0 ? val : 'mailto:' + val, isNewWindow, label, classes);
+
+ //--------------------------------------------------------------------------
+ // Handlers
+ //--------------------------------------------------------------------------
+
+ /**
+ * @private
+ */
+ _onAnyChange: function () {
+ this._adaptPreview();
},
- get_data_buy_url: function (def, $e, isNewWindow, label, classes, test) {
- def.resolve($e.val(), isNewWindow, label, classes);
+ /**
+ * @private
+ */
+ _onURLInput: function (ev) {
+ $(ev.currentTarget).closest('.form-group').removeClass('has-error');
+ var isLink = $(ev.currentTarget).val().indexOf('@') < 0;
+ this.$('input[name="is_new_window"]').closest('.form-group').toggleClass('hidden', !isLink);
},
- save: function () {
- var self = this;
- var _super = this._super.bind(this);
- return this.get_data().then(function (url, new_window, label, classes) {
- self.data.url = url;
- self.data.isNewWindow = new_window;
- self.data.text = label;
- self.data.className = classes.replace(/\s+/gi, ' ').replace(/^\s+|\s+$/gi, '');
- if (classes.replace(/(^|[ ])(btn-default|btn-success|btn-primary|btn-info|btn-warning|btn-danger)([ ]|$)/gi, ' ')) {
- self.data.style = {'background-color': '', 'color': ''};
- }
- self.final_data = self.data;
- }).then(_super);
- },
- bind_data: function () {
- var href = this.data.url;
- var new_window = this.data.isNewWindow;
- var text = this.data.text;
- var classes = this.data.iniClassName;
-
- this.$('input#o_link_dialog_label_input').val(text);
- this.$('input.window-new').prop('checked', new_window);
- this.$('input.link-style').prop('checked', false).first().prop("checked", true);
-
- if (classes) {
- this.$('input.link-style, select.link-style > option').each(function () {
- var $option = $(this);
- if ($option.val() && classes.indexOf($option.val()) >= 0) {
- if ($option.is("input")) {
- $option.prop("checked", true);
- } else {
- $option.parent().val($option.val());
- }
- }
- });
- }
-
- if (href) {
- var match = /mailto:(.+)/.exec(href);
- if (match) {
- this.$('input.email-address').val(match = /mailto:(.+)/.exec(href) ? match[1] : '');
- } else {
- this.$('input.url').val(href);
- }
- }
- this.preview();
- },
- changed: function (e) {
- $(e.target).closest('.list-group-item')
- .addClass('active')
- .siblings().removeClass('active')
- .addBack().removeClass('has-error');
- this.preview();
- },
- onkeyup: function (e) {
- var $e = $(e.target);
- var is_link = $e.val().indexOf("@") === -1;
- this.$('input.window-new').closest("div").toggle(is_link);
- this.preview();
- },
- preview: function () {
- var $preview = this.$("#link-preview");
- this.get_data(true).then(function (url, new_window, label, classes) {
- $preview.attr("target", new_window ? '_blank' : "")
- .attr("href", url && url.length ? url : "#")
- .html((label && label.length ? label : url))
- .attr("class", classes.replace(/pull-\w+/, '') + " o_btn_preview");
- });
- }
});
return {
Dialog: Dialog,
- alt: alt,
+ AltDialog: AltDialog,
MediaDialog: MediaDialog,
LinkDialog: LinkDialog,
- getCssSelectors: getCssSelectors,
- fontIcons: fontIcons,
- computeFonts: computeFonts,
};
});
diff --git a/addons/web_editor/static/src/less/web_editor.ui.less b/addons/web_editor/static/src/less/web_editor.ui.less
index e2a25e8e9e0..75cac40d823 100644
--- a/addons/web_editor/static/src/less/web_editor.ui.less
+++ b/addons/web_editor/static/src/less/web_editor.ui.less
@@ -1354,8 +1354,6 @@ body .modal {
}
li.search {
- float: right;
-
ul.pager li:last-child{
margin-left: 2px;
}
@@ -1378,7 +1376,8 @@ body .modal {
}
.o_existing_attachment_cell {
- @odoo-border-image: 1px;
+ @odoo-border-image: 2px;
+
.o_existing_attachment_remove {
.o-position-absolute(@odoo-border-image, 15px + @odoo-border-image);
padding: 3px 5px 5px 5px;
@@ -1396,21 +1395,24 @@ body .modal {
.o_image {
border: @odoo-border-image solid transparent;
cursor: pointer;
+ .o-transition(opacity, 400ms);
&.o_webimage {
width: 100%;
height: auto;
background-image: none!important;
}
+
+ &.o_image_loading {
+ opacity: 0;
+ }
}
&.o_selected .o_image {
border-color: @odoo-brand-primary;
box-shadow: 0px 0px 2px 2px @odoo-brand-secondary;
}
- &:hover {
- .o_existing_attachment_remove {
- opacity: 1;
- }
+ &:hover .o_existing_attachment_remove {
+ opacity: 1;
}
}
.font-icons-icons {
diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml
index e8e6374305e..f64d8595549 100644
--- a/addons/web_editor/static/src/xml/editor.xml
+++ b/addons/web_editor/static/src/xml/editor.xml
@@ -48,10 +48,10 @@
<div t-name="web_editor.dialog.media">
<ul class="nav nav-tabs">
<li class="active"><a href="#editor-media-image" data-toggle="tab">Image</a></li>
- <li><a href="#editor-media-document" data-toggle="tab">Document</a></li>
- <li><a href="#editor-media-icon" data-toggle="tab">Pictogram</a></li>
- <li><a href="#editor-media-video" data-toggle="tab">Video</a></li>
- <li class="search">
+ <li><a href="#editor-media-document" data-toggle="tab" t-att-class="widget.onlyImages ? 'hidden' : ''">Document</a></li>
+ <li><a href="#editor-media-icon" data-toggle="tab" t-att-class="widget.onlyImages ? 'hidden' : ''">Pictogram</a></li>
+ <li><a href="#editor-media-video" data-toggle="tab" t-att-class="widget.onlyImages ? 'hidden' : ''">Video</a></li>
+ <li class="search pull-right">
<ul class="pager mb0 mt0">
<li class="previous">
<a class="btn btn-default disabled" href="#"><i class="fa fa-angle-left"/>&#32;Previous</a>
@@ -61,7 +61,7 @@
</li>
</ul>
</li>
- <li class="search">
+ <li class="search pull-right">
<div class="form-group">
<input class="form-control" id="icon-search" type="search"/>
<span class="fa fa-search"/>
@@ -79,48 +79,44 @@
<!-- Image choosing part of the Media Dialog -->
<t t-name="web_editor.dialog.image">
- <div>
- <form method="POST"
- action="/web_editor/attachment/add"
- enctype="multipart/form-data"
- target="fileframe"
- class="form-inline">
- <input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
- <div class="well">
- <div class="form-group pull-left">
- <input type="file" name="upload" t-att-accept="widget.accept" multiple="multiple" style="position: absolute; opacity: 0; width: 1px; height: 1px;"/>
- <input type="hidden" name="disable_optimization" value=""/>
- <div class="btn-group">
- <button class="btn btn-primary filepicker" type="button">Upload an image</button>
- <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" type="button">
- <span class="caret"/>
- <span class="sr-only">Alternate Upload</span>
- </button>
- <ul class="dropdown-menu" role="menu">
- <li>
- <a class="js_disable_optimization">Upload image without optimization</a>
- </li>
- </ul>
- </div>
- <button class="btn hidden wait" disabled="disabled" type="button">Uploading...</button>
- </div>
- <div>
- <span class="text-muted">— or — </span>
- <label for="iamgeurl">Add an image URL</label>
- <div class="form-group btn-group">
- <input class="form-control url pull-left" id="iamgeurl" name="url" placeholder="https://www.odoo.com/logo.png" style="width: 320px;" type="text"/>
- <button class="btn btn-default" type="submit">Add</button>
- </div>
+ <form class="form-inline"
+ method="POST"
+ action="/web_editor/attachment/add"
+ enctype="multipart/form-data"
+ target="fileframe">
+ <input type="hidden" name="csrf_token" t-att-value="csrf_token"/>
+ <div class="well">
+ <div class="form-group pull-left">
+ <input type="file" class="hidden" name="upload" t-att-accept="widget.accept" multiple="multiple"/>
+ <input type="hidden" name="disable_optimization" value=""/>
+ <div class="btn-group">
+ <button class="btn btn-primary o_upload_image_button" type="button">Upload an image</button>
+ <button class="btn btn-primary dropdown-toggle" data-toggle="dropdown" type="button">
+ <span class="caret"/>
+ <span class="sr-only">Alternate Upload</span>
+ </button>
+ <ul class="dropdown-menu" role="menu">
+ <li>
+ <a class="o_upload_image_button_no_optimization">Upload image without optimization</a>
+ </li>
+ </ul>
</div>
</div>
- <input type="hidden" name="func"/>
- <div class="help-block"/>
- <div class="existing-attachments">
- <t t-call="web_editor.dialog.image.existing"/>
+ <div>
+ <span class="text-muted">— or —</span>
+ <label for="imageurl">Add an image URL</label>
+ <div class="form-group btn-group">
+ <input type="text" class="form-control url pull-left" id="imageurl" name="url" placeholder="https://www.odoo.com/logo.png" style="width: 320px;"/>
+ <button type="button" class="btn btn-default o_upload_image_url_button" disabled="disabled">Add</button>
+ </div>
</div>
- </form>
- </div>
- <iframe class="hidden" name="fileframe" src="about:blank"/>
+ </div>
+ <div class="help-block"/>
+ <div class="existing-attachments">
+ <t t-call="web_editor.dialog.image.existing"/>
+ </div>
+ <iframe class="hidden" name="fileframe"/>
+ </form>
</t>
<t t-name="web_editor.dialog.image.existing">
<div class="modal" tabindex="-1" role="dialog" aria-hidden="true">
@@ -146,7 +142,7 @@
<div class="row mt16" t-as="row" t-foreach="rows">
<div class="col-sm-2 o_existing_attachment_cell" t-as="attachment" t-foreach="row">
<i class="fa fa-times o_existing_attachment_remove" t-att-data-id="attachment.id"/>
- <div class="o_attachment_border"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image"/></div>
+ <div class="o_attachment_border"><div t-att-data-src="attachment.src" t-att-data-url="attachment.url" t-att-alt="attachment.name" t-att-title="attachment.name" t-att-data-id="attachment.id" t-att-data-mimetype="attachment.mimetype" class="o_image o_image_loading"/></div>
</div>
</div>
</div>
@@ -168,8 +164,6 @@
<!-- Icon choosing part of the Media Dialog -->
<t t-name="web_editor.dialog.font-icons">
<form action="#">
- <input type="hidden" id="fa-icon"/>
- <input type="hidden" id="fa-size" value="fa-1x"/>
<div class="font-icons-icons">
<t t-call="web_editor.dialog.font-icons.icons">
<t t-set="iconsParser" t-value="widget.iconsParser"/>
@@ -179,7 +173,11 @@
</t>
<t t-name="web_editor.dialog.font-icons.icons">
<t t-as="data" t-foreach="iconsParser">
- <span t-as="cssData" t-att-data-id="cssData[2]" t-att-title="cssData[3].join(', ')" t-attf-class="#{data.base} font-icons-icon #{cssData[2]}" t-attf-data-alias=",#{cssData[3]}," t-foreach="data.cssData"/>
+ <span t-foreach="data.cssData" t-as="cssData"
+ t-att-data-id="cssData.names[0]"
+ t-att-title="cssData.names[0]"
+ t-attf-class="font-icons-icon #{data.base} #{cssData.names[0]}"
+ t-att-data-alias="cssData.names.join(',')"/>
</t>
</t>
@@ -239,23 +237,23 @@
<div class="form-group">
<label class="control-label col-sm-3" for="o_link_dialog_label_input">Link Label</label>
<div class="col-sm-9">
- <input class="form-control" id="o_link_dialog_label_input" required="required"/>
+ <input type="text" name="label" class="form-control" id="o_link_dialog_label_input" required="required" t-att-value="widget.data.text"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3" for="o_link_dialog_url_input">URL or Email</label>
<div class="col-sm-9">
- <input class="form-control url email-address url-source" id="o_link_dialog_url_input" required="required"/>
+ <input type="text" name="url" class="form-control" id="o_link_dialog_url_input" required="required"/>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-3">Size</label>
<div class="col-sm-9">
- <select class="form-control col-sm-3 link-style">
- <option value="btn-xs">Extra Small</option>
- <option value="btn-sm">Small</option>
+ <select name="link_style_size" class="form-control col-sm-3 link-style">
+ <option value="xs">Extra Small</option>
+ <option value="sm">Small</option>
<option value="" selected="selected">Medium</option>
- <option value="btn-lg">Large</option>
+ <option value="lg">Large</option>
</select>
</div>
</div>
@@ -265,12 +263,12 @@
<div class="o_link_dialog_color">
<label t-attf-class="o_link_dialog_color_item o_btn_preview btn btn-link text-center" data-color="">
<span>L</span>
- <input class="hidden link-style" name="link-style-type" type="radio" value=""/>
+ <input class="hidden link-style" name="link_style_color" type="radio" value=""/>
<i class="fa"/>
</label>
- <t t-foreach="['btn-primary', 'btn-default', 'btn-success', 'btn-info', 'btn-warning', 'btn-danger', 'btn-alpha', 'btn-beta', 'btn-gamma', 'btn-delta', 'btn-epsilon']" t-as="color">
- <label t-attf-class="o_link_dialog_color_item o_btn_preview btn #{color}" t-att-data-color="color">
- <input class="hidden link-style" name="link-style-type" type="radio" t-att-value="color"/>
+ <t t-foreach="['primary', 'default', 'success', 'info', 'warning', 'danger', 'alpha', 'beta', 'gamma', 'delta', 'epsilon']" t-as="color">
+ <label t-attf-class="o_link_dialog_color_item o_btn_preview btn btn-#{color}">
+ <input type="radio" name="link_style_color" class="hidden link-style" t-att-value="color"/>
<i class="fa"/>
</label>
</t>
@@ -279,11 +277,11 @@
</div>
<div class="form-group">
<div class="col-sm-offset-3 col-sm-9">
- <label class="o_switch">
- <input class="window-new" type="checkbox"/>
- <span/>
- Open in new window
- </label>
+ <label class="o_switch">
+ <input type="checkbox" name="is_new_window" t-att-checked="widget.data.isNewWindow ? 'checked' : undefined"/>
+ <span/>
+ Open in new window
+ </label>
</div>
</div>
</form>