From abde8bddf8ec45cf12aab59e95c330321158cfba Mon Sep 17 00:00:00 2001 From: qsm-odoo Date: Tue, 12 Dec 2017 13:06:14 +0100 Subject: [PATCH] [REF] web_editor, website(_*): refactore media & link dialogs * blog, gengo, sale --- addons/web_editor/controllers/main.py | 7 +- addons/web_editor/static/src/js/base.js | 93 + addons/web_editor/static/src/js/editor/rte.js | 4 +- .../static/src/js/editor/rte.summernote.js | 7 +- .../static/src/js/editor/snippets.options.js | 4 +- .../static/src/js/editor/transcoder.js | 12 +- .../static/src/js/editor/translator.js | 2 +- addons/web_editor/static/src/js/inline.js | 4 +- addons/web_editor/static/src/js/tours/rte.js | 10 +- .../static/src/js/widgets/widgets.js | 1517 +++++++++-------- .../static/src/less/web_editor.ui.less | 16 +- addons/web_editor/static/src/xml/editor.xml | 124 +- addons/website/static/src/js/editor/editor.js | 13 +- .../static/src/js/editor/snippets.options.js | 10 +- addons/website/static/src/js/menu/content.js | 69 +- addons/website/static/src/js/tours/rte.js | 2 +- .../static/src/js/website.tour.blog.js | 2 +- .../static/src/js/website_blog.editor.js | 4 +- .../static/src/xml/website.gengo.xml | 4 +- .../static/src/js/website_sale_tour_shop.js | 2 +- 20 files changed, 1092 insertions(+), 814 deletions(-) diff --git a/addons/web_editor/controllers/main.py b/addons/web_editor/controllers/main.py index 50a9540323a..3b123704973 100644 --- a/addons/web_editor/controllers/main.py +++ b/addons/web_editor/controllers/main.py @@ -140,7 +140,7 @@ class Web_Editor(http.Controller): # add attachment (images or link) #------------------------------------------------------ @http.route('/web_editor/attachment/add', type='http', auth='user', methods=['POST']) - def attach(self, func, upload=None, url=None, disable_optimization=None, **kwargs): + def attach(self, upload=None, url=None, disable_optimization=None, **kwargs): # the upload argument doesn't allow us to access the files if more than # one file is uploaded, as upload references the first file # therefore we have to recover the files from the request object @@ -189,8 +189,9 @@ class Web_Editor(http.Controller): message = pycompat.text_type(e) return """""" % (func, json.dumps(uploads), json.dumps(message)) + window.attachments = %s; + window.error = %s; + """ % (json.dumps(uploads), json.dumps(message)) #------------------------------------------------------ # remove attachment (images or link) 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 @@ -8,6 +8,99 @@ var domReady = $.Deferred(); $(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 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', {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.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> DEF$/)', }, { content: "click on other picture", @@ -201,12 +201,12 @@ tour.register('rte', { extra_trigger: 'a#link-preview:containsRegex(/^]+>$/) 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'); @@ -12,29 +13,50 @@ 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(/"/g, '"'); this.title = ($(this.media).attr('title') || "").replace(/"/g, '"'); }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ save: function () { + var self = this; range.createFromNode(this.media).select(); this.$editable.data('NoteHistory').recordUndo(); var alt = this.$('#alt').val(); var title = this.$('#title').val(); $(this.media).attr('alt', alt ? alt.replace(/"/g, """) : null).attr('title', title ? title.replace(/"/g, """) : null); - _.defer((function () { - 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); -} - -/** - * ImageDialog widget. Let users change an image, including uploading a - * new image in odoo or selecting the image style (if supported by - * the caller). - */ -var ImageDialog = Widget.extend({ - template: 'web_editor.dialog.image', +var MediaWidget = Widget.extend({ 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 () { - 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 = []; + this.$media = $(media); 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]; + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @todo comment + */ + clear: function () { + if (!this.media) { + return; } - 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; + this._clear(); }, - 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]; + /** + * @todo comment + */ + getControlPanelConfig: function () { + return { + searchEnabled: true, + pagerEnabled: true, + pagerLeftEnabled: false, + pagerRightEnabled: false, + }; + }, + /** + * @override + */ + goToPage: function (page) { + this.page = page; + }, + /** + * @abstract + * @param {string} needle + * @returns {Deferred} + */ + search: function (needle) {}, + /** + * @abstract + * @returns {*} + */ + save: function () {}, + + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @abstract + */ + _clear: function () {}, + /** + * @private + */ + _replaceMedia: function ($media) { + this.$media.replaceWith($media); + this.$media = $media; + this.media = $media[0]; + }, +}); + +/** + * Let users choose an image, including uploading a new image in odoo. + */ +var ImageWidget = MediaWidget.extend({ + template: 'web_editor.dialog.image', + events: { + 'click .o_upload_image_button': '_onUploadButtonClick', + 'click .o_upload_image_button_no_optimization': '_onUploadButtonNoOptimizationClick', + 'change input[type=file]': '_onImageSelection', + 'click .o_upload_image_url_button': '_onUploadURLButtonClick', + 'input input.url': "_onSearchInput", + 'click .existing-attachments [data-src]': '_onImageClick', + 'dblclick .existing-attachments [data-src]': '_onImageDblClick', + 'click .o_existing_attachment_remove': '_onRemoveClick', + }, + + IMAGES_PER_ROW: 6, + IMAGES_ROWS: 2, + + /** + * @constructor + */ + init: function (parent, media, options) { + this._super.apply(this, arguments); + + this.IMAGES_PER_PAGE = this.IMAGES_PER_ROW * this.IMAGES_ROWS; + + this.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 = []; + }, + /** + * @override + */ + willStart: function () { + return $.when( + this._super.apply(this, arguments), + this.search('', true) + ); + }, + /** + * @override + */ + start: function () { + var def = this._super.apply(this, arguments); + var self = this; + + this._renderImages(); + + var o = { + url: null, + alt: null, + }; + if (this.$media.is('img')) { + o.url = this.$media.attr('src'); + } else if (this.$media.is('a.o_image')) { + o.url = this.$media.attr('href').replace(/[?].*/, ''); + o.id = +o.url.match(/\/web\/content\/(\d+)/, '')[1]; } + if (o.url) { + self._toggleImage(_.find(self.records, function (record) { return record.url === o.url;}) || o, true); + } + + return def; }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getControlPanelConfig: function () { + return _.extend(this._super.apply(this, arguments), { + pagerLeftEnabled: this.page > 0, + pagerRightEnabled: (this.page + 1) * this.IMAGES_PER_PAGE < this.records.length, + }); + }, + /** + * @override + */ + goToPage: function (page) { + this._super.apply(this, arguments); + this._renderImages(); + }, + /** + * @override + */ save: function () { - 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($('', {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($('')); } - 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) { + /** + * @override + */ + search: function (needle, noRender) { 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; - } + if (!noRender) { + this.$('input.url').val('').trigger('input').trigger('change'); } - $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, 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); + }, + }).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(); } }); - this.selected_existing(); }, - 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 + //-------------------------------------------------------------------------- + + /** + * @override + */ + _clear: function () { + this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' '); }, - selected_existing: function () { + /** + * @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 () { @@ -357,15 +366,150 @@ var ImageDialog = Widget.extend({ $select.closest('.o_existing_attachment_cell').addClass("o_selected"); return $select; }, - try_remove: function (e) { - var $help_block = this.$('.help-block').empty(); + /** + * @private + */ + _renderImages: function () { var self = this; - var $a = $(e.target); + var startIndex = this.page * this.IMAGES_PER_PAGE; + var rows = _(this.records).chain() + .slice(startIndex, startIndex + this.IMAGES_PER_PAGE) + .groupBy(function (a, index) { return Math.floor(index / self.IMAGES_PER_ROW); }) + .values() + .value(); + + this.$('.help-block').empty(); + + this.$('.existing-attachments').replaceWith(QWeb.render('web_editor.dialog.image.existing.content', {rows: rows})); + + var $divs = this.$('.o_image'); + var imageDefs = _.map($divs, function (el) { + var $div = $(el); + if (/gif|jpe|jpg|png/.test($div.data('mimetype'))) { + var $img = $('', { + class: 'img img-responsive', + src: $div.data('url') || $div.data('src'), + }); + var def = $.Deferred(); + $img[0].onload = def.resolve.bind(def); + $div.addClass('o_webimage').append($img); + return def; + } + }); + $.when.apply($, imageDefs).then(function () { + _.delay(function () { + $divs.removeClass('o_image_loading'); + }, 400); + }); + this._highlightSelectedImages(); + }, + /** + * @private + */ + _toggleImage: function (attachment, clearSearch) { + 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(''); + } + }, + /** + * @private + */ + _uploadImage: function () { + var self = this; + + /** + * @todo file upload cannot be handled with _rpc smoothly. This uses the + * form posting in iframe trick to handle the upload. + */ + var $iframe = this.$('iframe'); + $iframe.on('load', function () { + var iWindow = $iframe[0].contentWindow; + var attachments = iWindow.attachments; + var error = iWindow.error; + + self.$('.well > span').remove(); + self.$('.well > div').show(); + _.each(attachments, function (record) { + record.src = record.url || '/web/image/' + record.id; + record.isDocument = !(/gif|jpe|jpg|png/.test(record.mimetype)); + }); + if (error || !attachments.length) { + _processFile(null, error || !attachments.length); + } + self.images = attachments; + for (var i = 0 ; i < attachments.length ; i++) { + _processFile(attachments[i], error); + } + + function _processFile(attachment, error) { + var $button = self.$('.o_upload_image_button'); + if (!error) { + $button.addClass('btn-success'); + self._toggleImage(attachment, true); + } else { + $button.addClass('btn-danger'); + self.$el.addClass('has-error').find('.help-block').text(error); + } + + if (!self.multiImages) { + self.trigger_up('save_request'); + } + } + }); + this.$el.submit(); + }, + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onImageClick: function (ev) { + var $img = $(ev.currentTarget); + var attachment = _.find(this.records, function (record) { + return record.id === $img.data('id'); + }); + this._toggleImage(attachment); + }, + /** + * @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 $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 + * Let users choose a font awesome icon, support all font awesome loaded in the + * css files. */ -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. - */ -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(); - }, - 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) { + + base.computeFonts(); + this.iconsParser = base.fontIcons; + this.alias = _.flatten(_.map(this.iconsParser, function (data) { return data.alias; })); - this._super.apply(this, arguments); }, + /** + * @override + */ + start: function () { + this.$icons = this.$('.font-icons-icon'); + var classes = (this.media && this.media.className || '').split(/\s+/); + for (var i = 0 ; i < classes.length ; i++) { + var cls = classes[i]; + if (_.contains(this.alias, cls)) { + this.selectedIcon = cls; + this._highlightSelectedIcon(); + } + } + this.nonIconClasses = _.without(classes, this.selectedIcon); + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + getControlPanelConfig: function () { + return _.extend(this._super.apply(this, arguments), { + pagerEnabled: false, + }); + }, + /** + * @override + */ + save: function () { + var style = this.$media.attr('style') || ''; + var iconFont = this._getFont(this.selectedIcon) || {base: 'fa', font: ''}; + var finalClasses = _.uniq(this.nonIconClasses.concat([iconFont.base, iconFont.font])); + if (!this.$media.is('span')) { + var $span = $(''); + $span.data(this.$media.data()); + this._replaceMedia($span); + style = style.replace(/\s*width:[^;]+/, ''); + } + this.$media.attr({ + class: _.compact(finalClasses).join(' '), + style: style, + }); + return this.media; + }, + /** + * @override + */ search: function (needle) { var iconsParser = this.iconsParser; - if (needle) { + 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}) + ); }, - /** - * 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; + //-------------------------------------------------------------------------- + // Private + //-------------------------------------------------------------------------- + + /** + * @override + */ + _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'+ '
 
'+ '
 
'+ ''+ '' - ); - $(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(); - }, - start: function () { - var self = this; - this.$modal.addClass('note-image-dialog'); - this.$modal.find('.modal-dialog').addClass('o_select_media_dialog'); + 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.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'); + 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 (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'); - } + this.opened(function () { + if (!self.media) { + return; } - }).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")); - } - - 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"); + 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'); } }); - return this._super.apply(this, arguments); + this._onSearchInput = _.debounce(this._onSearchInput, 250); }, - save: function () { - if (this.options.select_images) { - this.final_data = this.active.save(); - this._super.apply(this, arguments); - return; + /** + * @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'); + + if (this.imageDialog) { + defs.push(this.imageDialog.appendTo(this.$("#editor-media-image"))); } + 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"))); + } + + return $.when.apply($, defs).then(function () { + self._setActive(self.imageDialog); + }); + }, + + //-------------------------------------------------------------------------- + // Public + //-------------------------------------------------------------------------- + + /** + * @override + */ + save: function () { + 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(); + 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); }, - searchTimer: null, - search: function () { + + //-------------------------------------------------------------------------- + // + //-------------------------------------------------------------------------- + + /** + * @private + */ + _setActive: function (widget) { + this.active = widget; + this.active.goToPage(0); + this._updateControlPanel(); + }, + /** + * @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,82 +1327,17 @@ 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(); - 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'); - } - var val = $e.val(); - var label = this.$('#o_link_dialog_label_input').val() || val; - if (label && this.data.images) { - for (var i=0; i/, ">").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(); - } - - 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'); - - 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); - } - return def; - }, - 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); - }, - get_data_buy_url: function (def, $e, isNewWindow, label, classes, test) { - def.resolve($e.val(), isNewWindow, label, classes); - }, - 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) { + 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 { @@ -1252,48 +1346,123 @@ var LinkDialog = Dialog.extend({ } }); } + if (this.data.url) { + var match = /mailto:(.+)/.exec(this.data.url); + this.$('input[name="url"]').val(match ? match[1] : this.data.url); + } - 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._adaptPreview(); + + this.$('input:visible:first').focus(); + + return this._super.apply(this, arguments); + }, + + //-------------------------------------------------------------------------- + // 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; + } + $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++) { + label = label.replace(//, ">").replace(/\[IMG\]/, this.data.images[i].outerHTML); } } - this.preview(); + + if ($url.prop('required') && (!url || !$url[0].checkValidity())) { + return null; + } + + 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 (url.indexOf('@') >= 0 && url.indexOf('mailto:') < 0) { + url = ('mailto:' + url); + } + return { + label: label, + url: url, + classes: classes, + isNewWindow: isNewWindow, + }; }, - changed: function (e) { - $(e.target).closest('.list-group-item') - .addClass('active') - .siblings().removeClass('active') - .addBack().removeClass('has-error'); - this.preview(); + + //-------------------------------------------------------------------------- + // Handlers + //-------------------------------------------------------------------------- + + /** + * @private + */ + _onAnyChange: function () { + this._adaptPreview(); }, - 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(); + /** + * @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); }, - 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 @@