diff options
| author | qsm-odoo <qsm@odoo.com> | 2016-05-18 18:02:51 +0200 |
|---|---|---|
| committer | qsm-odoo <qsm@odoo.com> | 2016-07-04 17:23:59 +0200 |
| commit | 62fb1bb92493b00bd45ed3f4eced5ca8b23f81d6 (patch) | |
| tree | 911fcb10158fa89ddd75ff2aac195f79236f3e59 /addons/web_editor/static/src | |
| parent | c1ab17337bdc816222be372aff314fcf5de8ba92 (diff) | |
[REF] web_editor: code refactoring and simplification
* Remove dead code + optimization
* Use Dialog API from web module
* Reorganize files
* Correct buggy $() function for snippet option class
* Add comments
* Fix the snippet parent navigation :
If a page contained a three level snippet structure (i.e A contains
B contains C), when editing the following bug occured :
If B or C was selected first, parenting buttons were correctly set.
But if selecting A then C, clicking on the parent lead to going back
on A instead of going on B.
* Fix snippet thumbnail layout
* Better isolate ui css from themes
* MediaDialog image list : prevent displaying empty attachments
* ...
Diffstat (limited to 'addons/web_editor/static/src')
| -rw-r--r-- | addons/web_editor/static/src/js/editor.js | 52 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/iframe.js | 12 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/inline.js | 18 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/rte.summernote.js | 10 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.animation.js | 102 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.editor.js | 274 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/snippets.options.js | 295 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/translator.js | 26 | ||||
| -rw-r--r-- | addons/web_editor/static/src/js/widgets.js | 377 | ||||
| -rw-r--r-- | addons/web_editor/static/src/less/web_editor.ui.components.less | 8 | ||||
| -rw-r--r-- | addons/web_editor/static/src/less/web_editor.ui.less | 14 | ||||
| -rw-r--r-- | addons/web_editor/static/src/xml/editor.xml | 583 | ||||
| -rw-r--r-- | addons/web_editor/static/src/xml/translator.xml | 23 |
13 files changed, 823 insertions, 971 deletions
diff --git a/addons/web_editor/static/src/js/editor.js b/addons/web_editor/static/src/js/editor.js index 9e842bed4bc..9dd08c3abd5 100644 --- a/addons/web_editor/static/src/js/editor.js +++ b/addons/web_editor/static/src/js/editor.js @@ -2,6 +2,7 @@ odoo.define('web_editor.editor', function (require) { "use strict"; var ajax = require('web.ajax'); +var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); var core = require('web.core'); var base = require('web_editor.base'); @@ -19,23 +20,22 @@ editor.editable = !!$('html').data('editable'); ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb); - $(document).on('click', '.note-editable', function (ev) { - ev.preventDefault(); - }); +$(document).on('click', '.note-editable', function (ev) { + ev.preventDefault(); +}); - $(document).on('submit', '.note-editable form .btn', function (ev) { - // Disable form submition in editable mode - ev.preventDefault(); - }); +$(document).on('submit', '.note-editable form .btn', function (ev) { + ev.preventDefault(); // Disable form submition in editable mode +}); - $(document).on('hide.bs.dropdown', '.dropdown', function (ev) { - // Prevent dropdown closing when a contenteditable children is focused - if (ev.originalEvent - && $(ev.target).has(ev.originalEvent.target).length - && $(ev.originalEvent.target).is('[contenteditable]')) { - ev.preventDefault(); - } - }); +$(document).on('hide.bs.dropdown', '.dropdown', function (ev) { + // Prevent dropdown closing when a contenteditable children is focused + if (ev.originalEvent + && $(ev.target).has(ev.originalEvent.target).length + && $(ev.originalEvent.target).is('[contenteditable]')) { + ev.preventDefault(); + } +}); ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -76,8 +76,6 @@ editor.Class = Widget.extend({ return res; }, start: function () { - var self = this; - $('.dropdown-toggle').dropdown(); this.display_placeholder(); @@ -89,11 +87,12 @@ editor.Class = Widget.extend({ window.onbeforeunload = function (event) { if (rte.history.getEditableHasUndo().length && !flag) { flag = true; - setTimeout(function () {flag=false;},0); + _.defer(function () { flag=false; }); return _t('This document is not saved!'); } }; - return this._super(); + + return this._super.apply(this, arguments); }, display_placeholder: function () { var $area = $("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]") @@ -135,18 +134,10 @@ editor.Class = Widget.extend({ if (!rte.history.getEditableHasUndo().length) { return d.resolve(); } - var $dialog = $(qweb.render('web_editor.discard')).appendTo(document.body); - $dialog.on('click', '.btn-danger', function () { - d.resolve(); - }).on('hidden.bs.modal', function () { - d.reject(); - }).on('keydown.dismiss.bs.modal', function (event) { - event.stopImmediatePropagation(); - }); - d.always(function () { - $dialog.remove(); + var confirm = Dialog.confirm(null, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), { + confirm_callback: d.resolve.bind(d) }); - $dialog.modal('show'); + confirm.on("closed", d, d.reject); }).then(function () { window.onbeforeunload = null; editor.reload(); @@ -155,5 +146,4 @@ editor.Class = Widget.extend({ }); return editor; - }); diff --git a/addons/web_editor/static/src/js/iframe.js b/addons/web_editor/static/src/js/iframe.js index 2f494cd041c..398dc2e5bf9 100644 --- a/addons/web_editor/static/src/js/iframe.js +++ b/addons/web_editor/static/src/js/iframe.js @@ -1,11 +1,15 @@ odoo.define('web_editor.iframe', function (require) { 'use strict'; +var ajax = require("web.ajax"); +var core = require("web.core"); var editor = require('web_editor.editor'); var translator = require('web_editor.translate'); var rte = require('web_editor.rte'); var snippet_editor = require('web_editor.snippet.editor'); +ajax.loadXML('/web/static/src/xml/base_common.xml', core.qweb); + window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) { var $editable = $("#editable_area"); if(value !== $editable.prop("innerHTML")) { @@ -15,7 +19,7 @@ window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name } snippet_editor.instance.make_active(false); } - + $editable.html(value); if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) { @@ -49,7 +53,7 @@ editor.Class.include({ this.on("snippets:ready", this, function () { $(window.top).trigger("resize"); }); - return this._super(); + return this._super.apply(this, arguments); } }); @@ -61,7 +65,7 @@ snippet_editor.Class.include({ rte.Class.include({ config: function ($editable) { - var config = this._super($editable); + var config = this._super.apply(this, arguments); if ($.deparam($.param.querystring()).debug !== undefined) { config.airPopover.splice(7, 0, ['view', ['codeview']]); } @@ -71,7 +75,7 @@ rte.Class.include({ translator.Class.include({ start: function () { - var res = this._super(); + var res = this._super.apply(this, arguments); $('button[data-action=save]').hide(); if (window.top.odoo[callback+"_editor"]) { window.top.odoo[callback+"_editor"](this); diff --git a/addons/web_editor/static/src/js/inline.js b/addons/web_editor/static/src/js/inline.js index 8f63d603b20..765ce7682df 100644 --- a/addons/web_editor/static/src/js/inline.js +++ b/addons/web_editor/static/src/js/inline.js @@ -9,26 +9,25 @@ var snippet_editor = require('web_editor.snippet.editor'); widget.MediaDialog.include({ start: function () { - this._super(); + this._super.apply(this, arguments); this.$('[href="#editor-media-video"]').addClass('hidden'); } }); editor.Class.include({ start: function () { - var self = this; if (location.search.indexOf("enable_editor") !== -1) { this.on('rte:start', this, function () { // move the caret at the end of the text when click after all content $("#wrapwrap").on('click', function (event) { if ($(event.target).is("#wrapwrap") || $(event.target).is("#editable_area:empty")) { - setTimeout(function () { + _.defer(function () { var node = $("#editable_area *") .filter(function () { return this.textContent.match(/\S|\u00A0/); }) .add($("#editable_area")) .last()[0]; $.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select(); - },0); + }); } }); }); @@ -39,8 +38,7 @@ editor.Class.include({ snippet_editor.Class.include({ start: function () { - var self = this; - this._super(); + this._super.apply(this, arguments); setTimeout(function () { var $editable = $("#editable_area"); transcoder.img_to_font($editable); @@ -51,7 +49,7 @@ snippet_editor.Class.include({ }); }, clean_for_save: function () { - this._super(); + this._super.apply(this, arguments); var $editable = $("#editable_area"); transcoder.font_to_img($editable); transcoder.class_to_style($editable); @@ -65,7 +63,6 @@ snippet_editor.Class.include({ }); }); }, - }); window.top.odoo[callback+"_updown"] = function (value, fields_values) { @@ -76,7 +73,7 @@ window.top.odoo[callback+"_updown"] = function (value, fields_values) { if (snippet_editor.instance) { snippet_editor.instance.make_active(false); } - + $editable.html(value); transcoder.img_to_font($editable); @@ -88,5 +85,4 @@ window.top.odoo[callback+"_updown"] = function (value, fields_values) { $editable.trigger("content_changed"); } }; - -});
\ No newline at end of file +}); diff --git a/addons/web_editor/static/src/js/rte.summernote.js b/addons/web_editor/static/src/js/rte.summernote.js index 73565e19743..a2c645808d3 100644 --- a/addons/web_editor/static/src/js/rte.summernote.js +++ b/addons/web_editor/static/src/js/rte.summernote.js @@ -399,8 +399,7 @@ eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, l $editable.data('range').select(); $editable.data('NoteHistory').recordUndo(); - var editor = new widgets.LinkDialog($editable, linkInfo); - editor.appendTo(document.body); + var editor = new widgets.LinkDialog(null, {}, $editable, linkInfo).open(); var def = new $.Deferred(); editor.on("save", this, function (linkInfo) { @@ -420,15 +419,14 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) { if (r.sc.tagName && r.sc.childNodes.length) { r.sc = r.sc.childNodes[r.so]; } - var editor = new widgets.MediaDialog($editable, dom.isImg(r.sc) ? r.sc : null); - editor.appendTo(document.body); + new widgets.MediaDialog(null, {}, $editable, dom.isImg(r.sc) ? r.sc : null).open(); return new $.Deferred().reject(); }; $.summernote.pluginEvents.alt = function (event, editor, layoutInfo, sorted) { var $editable = layoutInfo.editable(); var $selection = layoutInfo.handle().find('.note-control-selection'); var media = $selection.data('target'); - new widgets.alt($editable, media).appendTo(document.body); + new widgets.alt(null, {}, $editable, media).open(); }; ////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -758,7 +756,7 @@ eventHandler.attach = function (oLayoutInfo, options) { oLayoutInfo.editor().on('dblclick', imageSelector, function (event) { show_tooltip = false; if (!$(event.target).closest(".note-toolbar").length) { // prevent icon edition of top bar for default summernote - new widgets.MediaDialog(oLayoutInfo.editor(), event.target).appendTo(document.body); + new widgets.MediaDialog(null, {}, oLayoutInfo.editor(), event.target).open(); } }); oLayoutInfo.editor().on('click', imageSelector, function (event) { diff --git a/addons/web_editor/static/src/js/snippets.animation.js b/addons/web_editor/static/src/js/snippets.animation.js index f396bafcc30..ba311336436 100644 --- a/addons/web_editor/static/src/js/snippets.animation.js +++ b/addons/web_editor/static/src/js/snippets.animation.js @@ -7,6 +7,10 @@ var base = require('web_editor.base'); var registry = {}; var ready = []; +/** + * The Animation class provides a way for executing code once a website snippet is loaded in the dom + * and handle the case where the website edit mode is triggered. + */ var Animation = Class.extend({ selector: false, $: function () { @@ -16,67 +20,61 @@ var Animation = Class.extend({ this.$el = this.$target = $(dom); this.start(editable_mode); }, - /* - * start - * This method is called after init - */ - start: function (editable_mode) { - }, - /* - * stop - * This method is called to stop the animation (e.g.: when rte is launch) - */ - stop: function () { - }, + /** + * start + * This method is called after init + * @param editable_mode + */ + start: function () {}, + /** + * stop + * This method is called to stop the animation (e.g.: when rte is launch) + */ + stop: function () {}, +}); + +/** + * Start animations when loading the website is finished and stop/restart them + * once we enter in edit mode. + */ +base.ready().always(function () { + _.defer(start); }); -var start = function (editable_mode, $target) { +return { + Class: Animation, + registry: registry, + start: start, + stop: stop +}; + +function start(editable_mode, $init_target) { for (var k in registry) { var Animation = registry[k]; - var selector = ""; - if (Animation.prototype.selector) { - if (selector != "") selector += ", " - selector += Animation.prototype.selector; - } - if ($target) { - if ($target.is(selector)) selector = $target; - else continue; - } + var selector = Animation.prototype.selector || ""; + var $target = $init_target ? $init_target.filter(selector) : $(selector); - $(selector).each(function () { - var $snipped_id = $(this); - if ( !$snipped_id.parents("#oe_snippets").length && - !$snipped_id.parent("body").length && - !$snipped_id.data("snippet-view")) { - ready.push($snipped_id); - $snipped_id.data("snippet-view", new Animation($snipped_id, editable_mode)); - } else if ($snipped_id.data("snippet-view")) { - $snipped_id.data("snippet-view").start(editable_mode); + $target.each(function () { + var $snippet = $(this); + var animation = $snippet.data("snippet-view"); + if (!$snippet.parents("#oe_snippets").length && + !$snippet.parent("body").length && + !animation) { + ready.push($snippet); + $snippet.data("snippet-view", new Animation($snippet, editable_mode)); + } else if (animation) { + animation.start(editable_mode); } }); } -}; +} -var stop = function () { - $(ready).each(function () { - var $snipped_id = $(this); - if ($snipped_id.data("snippet-view")) { - $snipped_id.data("snippet-view").stop(); +function stop() { + _.each(ready, function ($snippet) { + var animation = $snippet.data("snippet-view"); + if (animation) { + animation.stop(); } }); -}; - -base.ready().always(function () { - setTimeout(function () { - start(); - },0); +} }); - -return { - 'Class': Animation, - 'registry': registry, - 'start': start, - 'stop': stop -}; - -});
\ No newline at end of file diff --git a/addons/web_editor/static/src/js/snippets.editor.js b/addons/web_editor/static/src/js/snippets.editor.js index ca8819bff50..1bfab9d3dc2 100644 --- a/addons/web_editor/static/src/js/snippets.editor.js +++ b/addons/web_editor/static/src/js/snippets.editor.js @@ -5,80 +5,42 @@ var Class = require('web.Class'); var ajax = require('web.ajax'); var core = require('web.core'); var Widget = require('web.Widget'); -var base = require('web_editor.base'); var editor = require('web_editor.editor'); var animation = require('web_editor.snippets.animation'); var options = require('web_editor.snippets.options'); var qweb = core.qweb; -var dummy = editor.dummy; - -/* ----- SNIPPET SELECTOR ---- */ ajax.loadXML('/web_editor/static/src/xml/snippets.xml', qweb); -editor.Class.include({ - init: function () { - var self = this; - var res = this._super.apply(this, arguments); - var $editable = this.rte.editable(); - this.buildingBlock = new BuildingBlock(this, $editable); - this.buildingBlock.on("snippets:ready", this, function () { - self.trigger("snippets:ready"); - }); - return res; - }, - start: function () { - var self = this; - animation.stop(); - this.buildingBlock.insertAfter(this.$el); - animation.start(true); - - this.rte.editable().find("*").off('mousedown mouseup click'); - return this._super(); - }, - save: function () { - this.buildingBlock.clean_for_save(); - return this._super.apply(this, arguments); - }, -}); +var data = {}; /* ----- SNIPPET SELECTOR ---- */ -$.extend($.expr[':'],{ - hasData: function (node,i,m) { +$.extend($.expr[':'], { + hasData: function (node) { return !!_.toArray(node.dataset).length; }, - data: function (node,i,m) { + data: function (node, i, m) { return $(node).data(m[3]); } }); -var globalSelector = { +data.globalSelector = { closest: function () { return $(); }, all: function () { return $(); }, is: function () { return false; }, }; -/* ----- Jquery activate block ---- */ - -$.fn.extend({ - activateBlock: function () { - var target = globalSelector.closest($(this))[0] || (dom.isBR(this) ? this.parentNode : dom.node(this)); - var evt = document.createEvent("MouseEvents"); - evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target); - target.dispatchEvent(evt); - return this; - } -}); - -/* ----- BuildingBlock (managment of drag&drop and focus/blur snippets) ---- */ - -var BuildingBlock = Widget.extend({ +/** + * Snippet editor main class. + * Management of drag&drop and focus/blur of snippets. + * Previously named "BuildingBlock". + */ +data.Class = Widget.extend({ id: "oe_snippets", activeSnippets: [], init: function (parent, $editable) { - this.parent = parent; this.$editable = $editable; this._super.apply(this, arguments); @@ -87,14 +49,14 @@ var BuildingBlock = Widget.extend({ } this.$active_snipped_id = false; this.snippets = []; - + data.instance = this; }, start: function () { var self = this; $(window).resize(function () { - setTimeout('$(document).click()',0); + _.defer(function () { $(document).click(); }); }); this.fetch_snippet_templates(); @@ -106,7 +68,7 @@ var BuildingBlock = Widget.extend({ var _isNotBreakable = $.summernote.core.dom.isNotBreakable; $.summernote.core.dom.isNotBreakable = function (node) { - return _isNotBreakable(node) || $(node).is('div') || globalSelector.is($(node)); + return _isNotBreakable(node) || $(node).is('div') || data.globalSelector.is($(node)); }; $(window).on('resize', function () { @@ -136,7 +98,7 @@ var BuildingBlock = Widget.extend({ }, _add_check_selector : function (selector, no_check, is_children) { var self = this; - var selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)"; + selector = selector.split(/\s*,/).join(":not(.o_snippet_not_selectable), ") + ":not(.o_snippet_not_selectable)"; if (no_check) { return { @@ -151,7 +113,7 @@ var BuildingBlock = Widget.extend({ } }; } else { - var selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable"; + selector = selector.split(/\s*,/).join(":o_editable, ") + ":o_editable"; return { closest: function ($from, parentNode) { var parents = self.$editable.get(); @@ -203,14 +165,14 @@ var BuildingBlock = Widget.extend({ } $ul.children().tooltip({ - delay: { "show": 500, "hide": 100 }, - container: 'body', - title: function () { - return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1); - }, - trigger: 'hover', - placement: 'top' - }).on('click', function () {$(this).tooltip('hide');}); + delay: { "show": 500, "hide": 100 }, + container: 'body', + title: function () { + return (navigator.appVersion.indexOf('Mac') > -1 ? 'CMD' : 'CTRL')+'+SHIFT+'+($(this).index()+1); + }, + trigger: 'hover', + placement: 'top' + }).on('click', function () {$(this).tooltip('hide');}); // t-snippet $html.find('[data-oe-type="snippet"]').each(function () { @@ -239,35 +201,35 @@ var BuildingBlock = Widget.extend({ }); $styles.addClass("hidden"); - globalSelector.closest = function ($from) { - var $temp; - var $target; - var len = selector.length; - for (var i = 0; i<len; i++) { - $temp = selector[i].closest($from, $target && $target[0]); - if (!$target || $temp.length) { - $target = $temp; - } + data.globalSelector.closest = function ($from) { + var $temp; + var $target; + var len = selector.length; + for (var i = 0; i<len; i++) { + $temp = selector[i].closest($from, $target && $target[0]); + if (!$target || $temp.length) { + $target = $temp; } - return $target; + } + return $target; }; - globalSelector.all = function ($from) { - var $target; - var len = selector.length; - for (var i = 0; i<len; i++) { - if (!$target) $target = selector[i].all($from); - else $target = $target.add(selector[i].all($from)); - } - return $target; + data.globalSelector.all = function ($from) { + var $target; + var len = selector.length; + for (var i = 0; i<len; i++) { + if (!$target) $target = selector[i].all($from); + else $target = $target.add(selector[i].all($from)); + } + return $target; }; - globalSelector.is = function ($from) { - var len = selector.length; - for (var i = 0; i<len; i++) { - if (selector[i].is($from)) { - return true; - } + data.globalSelector.is = function ($from) { + var len = selector.length; + for (var i = 0; i<len; i++) { + if (selector[i].is($from)) { + return true; } - return false; + } + return false; }; var number = 0; @@ -364,7 +326,6 @@ var BuildingBlock = Widget.extend({ } var pos = $target.offset(); var mt = parseInt($target.css("margin-top") || 0); - var mb = parseInt($target.css("margin-bottom") || 0); $el.css({ width: $target.outerWidth(), left: pos.left, @@ -432,8 +393,8 @@ var BuildingBlock = Widget.extend({ return; } - if (!globalSelector.is($target)) { - $target = globalSelector.closest($target); + if (!data.globalSelector.is($target)) { + $target = data.globalSelector.closest($target); } if (self.$active_snipped_id && self.$active_snipped_id.is($target)) { @@ -462,14 +423,14 @@ var BuildingBlock = Widget.extend({ var template = self.templateOptions; for (var k in template) { var Option = opt[template[k]['option']]; - if (Option && Option.prototype.clean_for_save !== dummy) { + if (Option && Option.prototype.clean_for_save !== editor.dummy) { template[k].selector.all().filter(function () { - var node = this; - while (!/o_dirty|o_editable/.test(node.className) && node !== document) { - node = node.parentNode; - } - return node.className.indexOf("o_dirty") !== -1; - }).each(function () { + var node = this; + while (!/o_dirty|o_editable/.test(node.className) && node !== document) { + node = node.parentNode; + } + return node.className.indexOf("o_dirty") !== -1; + }).each(function () { new Option(self, null, $(this), k).clean_for_save(); }); } @@ -505,7 +466,7 @@ var BuildingBlock = Widget.extend({ create_snippet_editor: function ($snippet) { if (typeof $snippet.data("snippet-editor") === 'undefined') { if (!this.activate_overlay_zones($snippet).length) return; - $snippet.data("snippet-editor", new Editor(this, $snippet)); + $snippet.data("snippet-editor", new data.Editor(this, $snippet)); } }, @@ -515,7 +476,7 @@ var BuildingBlock = Widget.extend({ var $tumb = $snippets.find(".oe_snippet_thumbnail_img:first"); var left = $tumb.outerWidth()/2; var top = $tumb.outerHeight()/2; - var $toInsert, dropped, $snippet, action, snipped_id; + var $toInsert, dropped, $snippet; $snippets.draggable({ greedy: true, @@ -535,7 +496,6 @@ var BuildingBlock = Widget.extend({ var $base_body = $snippet.find('.oe_snippet_body'); var $selector_siblings = $(); var $selector_children = $(); - var vertical = false; var temp = self.templateOptions; for (var k in temp) { if ($base_body.is(temp[k].base_selector)) { @@ -553,7 +513,7 @@ var BuildingBlock = Widget.extend({ $toInsert = $base_body.clone(); if (!$selector_siblings.length && !$selector_children.length) { - console.debug($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in"); + console.warn($snippet.find(".oe_snippet_thumbnail_title").text() + " have not insert action: data-drop-near or data-drop-in"); return; } @@ -577,7 +537,7 @@ var BuildingBlock = Widget.extend({ }, stop: function (ev, ui) { $toInsert.removeClass('oe_snippet_body'); - + if (! dropped && self.$editable.find('.oe_drop_zone') && ui.position.top > 3) { var el = self.$editable.find('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first(); if (el.length) { @@ -587,12 +547,12 @@ var BuildingBlock = Widget.extend({ } self.$editable.find('.oe_drop_zone').droppable('destroy').remove(); - + if (dropped) { var prev = $toInsert.first()[0].previousSibling; var next = $toInsert.last()[0].nextSibling; - var rte = self.parent.rte; + var rte = self.getParent().rte; if (prev) { $toInsert.detach(); @@ -638,7 +598,7 @@ var BuildingBlock = Widget.extend({ // call a method on a snippet and all his children call_for_all_snippets: function ($snippet, callback) { var self = this; - $snippet.add(globalSelector.all($snippet)).each(function () { + $snippet.add(data.globalSelector.all($snippet)).each(function () { var $snippet = $(this); setTimeout(function () { self.create_snippet_editor($snippet); @@ -652,8 +612,8 @@ var BuildingBlock = Widget.extend({ // return the original snippet in the editor bar from a snippet id (string) get_snippet_from_id: function (id) { return $('.oe_snippet').filter(function () { - return $(this).data('option') === id; - }).first(); + return $(this).data('option') === id; + }).first(); }, // Create element insertion drop zones. two css selectors can be provided @@ -677,10 +637,10 @@ var BuildingBlock = Widget.extend({ var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); if (test) { $drop.addClass("oe_vertical").css({ - 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), - 'float': 'none', - 'display': 'inline-block' - }); + 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), + 'float': 'none', + 'display': 'inline-block' + }); } else if (float === "left" || float === "right") { $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().last().outerHeight()), 30)); } @@ -688,14 +648,14 @@ var BuildingBlock = Widget.extend({ $drop = $drop.clone(); $zone.prepend($drop); - var node = $drop[0].nextSibling; - var test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); + node = $drop[0].nextSibling; + test = !!(node && ((!node.tagName && node.textContent.match(/\S/)) || node.tagName === "BR")); if (test) { $drop.addClass("oe_vertical").css({ - 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), - 'float': 'none', - 'display': 'inline-block' - }); + 'height': parseInt(window.getComputedStyle($zone[0]).lineHeight), + 'float': 'none', + 'display': 'inline-block' + }); } else if (float === "left" || float === "right") { $drop.addClass("oe_vertical").css('height', Math.max(Math.min($zone.outerHeight(), $zone.children().first().outerHeight()), 30)); } @@ -816,8 +776,8 @@ var BuildingBlock = Widget.extend({ if ($target.data('overlay') && $target.data('overlay').hasClass("oe_active")) { self.cover_target($target.data('overlay'), $target); } - },50); - }); + }, 50); + }); var resize = function () { if ($zone.parent().length) { @@ -834,9 +794,11 @@ var BuildingBlock = Widget.extend({ } }); -/* ----- Editor (object who contain overlay and the option list) ---- */ - -var Editor = Class.extend({ +/** + * Snippet editor Editor class. + * Management of the overlay and option list for a snippet. + */ +data.Editor = Class.extend({ init: function (BuildingBlock, dom) { this.buildingBlock = BuildingBlock; this.$target = $(dom); @@ -874,12 +836,12 @@ var Editor = Class.extend({ _drag_and_drop_active_drop_zone: function ($zones) { var self = this; $zones.droppable({ - over: function () { + over: function () { $(".oe_drop_zone.hide").removeClass("hide"); $(this).addClass("hide").first().after(self.$target); self.dropped = true; }, - out: function () { + out: function () { $(this).removeClass("hide"); self.$target.detach(); self.dropped = false; @@ -898,12 +860,12 @@ var Editor = Class.extend({ self.$overlay.addClass("hidden"); var $selector_siblings; - for (var i=0; i<self.selector_siblings.length; i++) { + for (var i = 0 ; i < self.selector_siblings.length ; i++) { if (!$selector_siblings) $selector_siblings = self.selector_siblings[i].all(); else $selector_siblings = $selector_siblings.add(self.selector_siblings[i].all()); } var $selector_children; - for (var i=0; i<self.selector_children.length; i++) { + for (i = 0 ; i < self.selector_children.length ; i++) { if (!$selector_children) $selector_children = self.selector_children[i].all(); else $selector_children = $selector_children.add(self.selector_children[i].all()); } @@ -931,7 +893,7 @@ var Editor = Class.extend({ $(".oe_drop_clone, .oe_drop_to_remove").remove(); if (this.dropped) { - this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); if (prev) { this.$target.insertAfter(prev); @@ -961,6 +923,7 @@ var Editor = Class.extend({ this.styles = {}; this.selector_siblings = []; this.selector_children = []; + _.each(this.buildingBlock.templateOptions, function (val, option_id) { if (!val.selector.is(self.$target)) { return; @@ -979,7 +942,7 @@ var Editor = Class.extend({ this.$overlay.find(".oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove").addClass('hidden'); } - if ($ul.find("li").length) { + if ($ul.children().length) { $styles.removeClass("hidden"); } this.$overlay.find('[data-toggle="dropdown"]').dropdown(); @@ -988,10 +951,7 @@ var Editor = Class.extend({ get_parent_block: function () { var self = this; var $button = this.$overlay.find('.oe_snippet_parent'); - var $parent = this.$target.parents().filter(function () { return $(this).data("snippet-editor"); }); - if (!$parent.length) { - $parent = globalSelector.closest(this.$target.parent()); - } + var $parent = data.globalSelector.closest(this.$target.parent()); if ($parent.length) { $button.removeClass("hidden"); $button.off("click").on('click', function (event) { @@ -1010,7 +970,6 @@ var Editor = Class.extend({ * This method is called after init and _readXMLData */ start: function () { - var self = this; if (!this.$target.parent().is(':o_editable')) { this.$overlay.find('.oe_snippet_move, .oe_snippet_clone, .oe_snippet_remove').remove(); } else { @@ -1024,7 +983,7 @@ var Editor = Class.extend({ event.preventDefault(); var $clone = this.$target.clone(false); - this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); this.$target.after($clone); this.buildingBlock.call_for_all_snippets($clone, function (editor, $snippet) { @@ -1039,7 +998,7 @@ var Editor = Class.extend({ event.preventDefault(); this.on_blur(); - this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); var index = _.indexOf(this.buildingBlock.snippets, this.$target.get(0)); this.buildingBlock.call_for_all_snippets(this.$target, function (editor, $snippet) { @@ -1049,8 +1008,6 @@ var Editor = Class.extend({ }); delete this.buildingBlock.snippets[index]; - var $editable = this.$target.closest(".o_editable"); - // remove node and his empty var node = this.$target.parent()[0]; @@ -1103,11 +1060,42 @@ var Editor = Class.extend({ }, }); -var data = { - Class: BuildingBlock, - Editor: Editor, - globalSelector: globalSelector, -}; -return data; +/** + * Add the ability on the main editor class to instantiate the snippet editor component + */ +editor.Class.include({ + init: function () { + var self = this; + var res = this._super.apply(this, arguments); + var $editable = this.rte.editable(); + this.buildingBlock = new data.Class(this, $editable); + this.buildingBlock.on("snippets:ready", this, function () { + self.trigger("snippets:ready"); + }); + return res; + }, + start: function () { + this.buildingBlock.insertAfter(this.$el); + this.rte.editable().find("*").off('mousedown mouseup click'); + return this._super.apply(this, arguments); + }, + save: function () { + this.buildingBlock.clean_for_save(); + return this._super.apply(this, arguments); + }, +}); + +/** + * Add the ability the restart the animations + */ +editor.Class.include({ + start: function () { + animation.stop(); + return this._super.apply(this, arguments).then(function () { + animation.start(true); + }); + }, +}); +return data; }); diff --git a/addons/web_editor/static/src/js/snippets.options.js b/addons/web_editor/static/src/js/snippets.options.js index 3f933f4c191..77879ce8803 100644 --- a/addons/web_editor/static/src/js/snippets.options.js +++ b/addons/web_editor/static/src/js/snippets.options.js @@ -1,89 +1,87 @@ odoo.define('web_editor.snippets.options', function (require) { 'use strict'; - var Class = require('web.Class'); var ajax = require('web.ajax'); + var Class = require('web.Class'); var core = require('web.core'); var Dialog = require('web.Dialog'); var base = require('web_editor.base'); var editor = require('web_editor.editor'); var widget = require('web_editor.widget'); - var animation = require('web_editor.snippets.animation'); var qweb = core.qweb; var _t = core._t; - /* ----- Editor option (object link the the xml with data-js) ---- */ - + /** + * The SnippetOption class handles one option for one snippet. The registry returned by + * this module contains the names of the specialized SnippetOption which can be reference + * thanks to the data-js key of the full options web_editor template. + */ var SnippetOption = Class.extend({ // initialisation (don't overwrite) init: function (BuildingBlock, editor, $target, option_id) { this.buildingBlock = BuildingBlock; this.editor = editor; this.$target = $target; + + this.option = option_id; var option = this.buildingBlock.templateOptions[option_id]; - var styles = this.$target.data("snippet-option-ids") || {}; - styles[option_id] = this; - this.$target.data("snippet-option-ids", styles); - this.$overlay = this.$target.data('overlay') || $('<div>'); - this.option= option_id; - this.$el = option.$el.find(">li").clone(); + this.$el = option.$el.children("li").clone(); this.data = option.$el.data(); + + this.$overlay = this.$target.data('overlay') || $(); }, // helper for this.$target.find - $: function (selector) { - return this.$target(selector); + $: function () { + return this.$target.find.apply(this.$target, arguments); }, _bind_li_menu: function () { this.$el.filter("li:hasData").find('a:first') .off('mouseenter click') - .on('mouseenter click', _.bind(this._mouse, this)); + .on('mouseenter click', _.bind(_mouse, this)); this.$el .off('mouseenter click', "li:hasData a") - .on('mouseenter click', "li:hasData a", _.bind(this._mouse, this)); + .on('mouseenter click', "li:hasData a", _.bind(_mouse, this)); - this.$el.closest("ul").add(this.$el) - .off('mouseleave') - .on('mouseleave', _.bind(this.reset, this)); + this.$el.off('mouseleave') + .on('mouseleave', _.bind(this.reset, this)); - this.$el - .off('mouseleave', "ul") - .on('mouseleave', "ul", _.bind(this.reset, this)); + this.$el.off('mouseleave', "ul") + .on('mouseleave', "ul", _.bind(this.reset, this)); + + this.methodsToReset = []; - this.reset_methods = []; + function _mouse(event) { + var $next = $(event.currentTarget).parent(); + + // triggers preview or apply methods if a menu item has been clicked + this.select(event.type === "click" ? "click" : "over", $next); + if (event.type === 'click') { + this.set_active(); + this.$target.trigger("snippet-option-change", [this]); + } else { + this.$target.trigger("snippet-option-preview", [this]); + } + } }, /** - * this method handles mouse:over and mouse:leave on the snippet editor menu + * The set_active method tweaks the option dropdown to show the selected value + * according to the state of the $target the option customizes. */ - _time_mouseleave: null, - _mouse: function (event) { - var $next = $(event.currentTarget).parent(); - - // triggers preview or apply methods if a menu item has been clicked - this.select(event.type === "click" ? "click" : "over", $next); - if (event.type === 'click') { - this.set_active(); - this.$target.trigger("snippet-option-change", [this]); - } else { - this.$target.trigger("snippet-option-preview", [this]); - } - }, - /* - * select and set item active or not (add highlight item and his parents) - * called before start - */ set_active: function () { - var classes = _.uniq((this.$target.attr("class") || '').split(/\s+/)); + var self = this; this.$el.find('[data-toggle_class], [data-select_class]') - .add(this.$el) - .filter('[data-toggle_class], [data-select_class]') + .addBack('[data-toggle_class], [data-select_class]') .removeClass("active") - .filter('[data-toggle_class="' + classes.join('"], [data-toggle_class="') + '"] ,'+ - '[data-select_class="' + classes.join('"], [data-select_class="') + '"]') + .filter(function () { + var $elem = $(this); + var className = $elem.data("toggle_class") || $elem.data("select_class"); + return self.$target.hasClass(className); + }) .addClass("active"); }, @@ -97,131 +95,115 @@ odoo.define('web_editor.snippets.options', function (require) { this._bind_li_menu(); }, - on_blur : function () { - }, - - on_clone: function ($clone) { - }, + on_blur : function () {}, + on_clone: function ($clone) {}, + on_move: function () {}, + on_remove: function () {}, - on_move: function () { - }, - - on_remove: function () { - }, - - drop_and_build_snippet: function () { - }, + drop_and_build_snippet: function () {}, reset: function () { var self = this; - var lis = self.$el.add(self.$el.find('li')).filter('.active').get(); - lis.reverse(); - _.each(lis, function (li) { + var lis = this.$el.find("li.active").addBack('.active').get(); + _.each(lis.reverse(), function (li) { var $li = $(li); - for (var k in self.reset_methods) { - var method = self.reset_methods[k]; - if ($li.is('[data-'+method+']') || $li.closest('[data-'+method+']').size()) { - delete self.reset_methods[k]; + for (var k in self.methodsToReset) { + var method = self.methodsToReset[k]; + if ($li.closest('[data-' + method + ']').length) { + delete self.methodsToReset[k]; } } self.select("reset", $li); }); - for (var k in self.reset_methods) { - var method = self.reset_methods[k]; + for (var k in this.methodsToReset) { + var method = this.methodsToReset[k]; if (method) { - self[method]("reset", null); + this[method]("reset", null); } } - self.reset_methods = []; - self.$target.trigger("snippet-option-reset", [this]); + this.methodsToReset = []; + this.$target.trigger("snippet-option-reset", [this]); }, - // call data-method args as method (data-only can be used) + /** + * Handle action type on a particuler $li (sub)option of the option. + * @param type action type to handle ("click", "over", "reset") + * @param $li the suboption DOM element which is concerned by the action + */ select: function (type, $li) { - var self = this, - $methods = [], - el = $li[0]; - + // (sub)options can say they respond to only one action type if ($li.data('only') && type !== $li.data('only')) { return; } - - if (type==="click") { + // If the type is click, the user selected the option for good (so reset and record action) + if (type === "click") { this.reset(); - this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); } - function filter (k) { return k !== 'oeId' && k !== 'oeModel' && k !== 'oeField' && k !== 'oeXpath' && k !== 'oeSourceId' && k !== 'only';} - function hasData(el) { - for (var k in el.dataset) { - if (filter (k)) { - return true; - } - } - return false; - } - function method(el) { - var data = {}; - for (var k in el.dataset) { - if (filter (k)) { - data[k] = el.dataset[k]; - } - } - return data; - } - - while (el && this.$el.is(el) || _.some(this.$el.map(function () {return $.contains(this, el);}).get()) ) { - if (hasData(el)) { - $methods.push(el); + // Search for methods (data-...) (i.e. data-toggle_class) on the selected (sub)option and its parents + var el = $li[0]; + var methods = []; + do { + var m = _.omit(el.dataset, ["oeId", "oeModel", "oeField", "oeXpath", "oeSourceId", "only"]); + if (_.keys(m).length) { + methods.push([el, m]); } el = el.parentNode; - } + } while (this.$el.parent().has(el).length); - $methods.reverse(); - - _.each($methods, function (el) { - var $el = $(el); - var methods = method(el); + // Call the found method in the right order (parents -> child) and save them for later reset + var self = this; + _.each(methods.reverse(), function (data) { + var $el = $(data[0]); + var methods = data[1]; for (var k in methods) { if (self[k]) { - if (type !== "reset" && self.reset_methods.indexOf(k) === -1) { - self.reset_methods.push(k); + if (type !== "reset" && self.methodsToReset.indexOf(k) === -1) { + self.methodsToReset.push(k); } self[k](type, methods[k], $el); } else { - console.error("'"+self.option+"' snippet have not method '"+k+"'"); + console.error("'" + self.option + "' snippet option have not method '" + k + "'"); } } }); }, - // default method for snippet + /** + * Default option methods toggle_class and selected_class handle the two behavior of having a list + * of option classes to toggle or not and having a list of option classes where only one must be chosen. + */ toggle_class: function (type, value, $li) { - var $lis = this.$el.find('[data-toggle_class]').add(this.$el).filter('[data-toggle_class]'); + var $lis = this.$el.find("[data-toggle_class]").addBack("[data-toggle_class]"); - function map ($lis) { + function map($lis) { return $lis.map(function () {return $(this).data("toggle_class");}).get().join(" "); } var classes = map($lis); var active_classes = map($lis.filter('.active, :has(.active)')); - this.$target.removeClass(classes); - this.$target.addClass(active_classes); - + this.$target.removeClass(classes).addClass(active_classes); if (type !== 'reset') { this.$target.toggleClass(value); } }, select_class: function (type, value, $li) { - var $lis = this.$el.find('[data-select_class]').add(this.$el).filter('[data-select_class]'); + var $lis = this.$el.find("[data-select_class]").addBack("[data-select_class]"); - var classes = $lis.map(function () {return $(this).data('select_class');}).get(); + var classes = $lis.map(function () {return $(this).data("select_class");}).get().join(" "); - this.$target.removeClass(classes.join(" ")); - if(value) this.$target.addClass(value); + this.$target.removeClass(classes); + if (value) { + this.$target.addClass(value); + } }, + + /** + * Helper method to evaluate the value expression directly as a function + */ eval: function (type, value, $li) { var fn = new Function("node", "type", "value", "$li", value); fn.call(this, this, type, value, $li); @@ -233,51 +215,14 @@ odoo.define('web_editor.snippets.options', function (require) { /* ----- default options ---- */ - // to remove after 9.0 (keep for compatibility without update with -u) - registry.media = SnippetOption.extend({ - start: function () { - var self =this; - this._super(); - animation.start(true, this.$target); - this.$target.closest(".o_editable").on("content_changed", function () { - if (!self.$target.parent().length) { - self.$target.remove(); - self.$overlay.remove(); - } - }); - }, - edition: function (type, value) { - if(type !== "click") return; - var self = this; - var _editor = new widget.MediaDialog(this.$target.closest('.o_editable'), this.$target[0]); - _editor.appendTo(document.body); - _editor.on('saved', this, function (item, old) { - self.editor.on_blur(); - self.buildingBlock.make_active(false); - if (self.$target.parent().data("oe-field") !== "image") { - setTimeout(function () { - self.buildingBlock.make_active($(item)); - },0); - } - $(item).trigger("content_changed"); - }); - }, - on_focus : function () { - var self = this; - var $parent = this.$target.parent(); - - if ($parent.data("oe-field") === "image" && $parent.hasClass('o_editable')) { - this.$overlay.addClass("hidden"); - self.edition('click', null); - self.buildingBlock.make_active(false); - } - } - }); - + /** + * The colorpicker option is designed to change the background color class of a snippet. This class change the + * default background color and text color of the snippet content. + */ registry.colorpicker = SnippetOption.extend({ start: function () { var self = this; - var res = this._super(); + var res = this._super.apply(this, arguments); if (!this.$el.find('.colorpicker').length) { var $pt = $(qweb.render('web_editor.snippet.option.colorpicker')); @@ -363,10 +308,13 @@ odoo.define('web_editor.snippets.options', function (require) { } }); + /** + * The background option is designed to change the background image of a snippet. + */ registry.background = SnippetOption.extend({ start: function ($change_target) { this.$target = $change_target || this.$target; - this._super(); + this._super.apply(this, arguments); var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); if (this.$target.hasClass('oe_custom_bg')) { this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src); @@ -383,7 +331,7 @@ odoo.define('web_editor.snippets.options', function (require) { }, select_class : function (type, value, $li) { this.background(type, '', $li); - this._super(type, value, $li); + this._super.apply(this, arguments); }, choose_image: function (type, value, $li) { if(type !== "click") return; @@ -393,8 +341,7 @@ odoo.define('web_editor.snippets.options', function (require) { $image.attr("src", value); $image.appendTo(self.$target); - var _editor = new widget.MediaDialog(null, $image[0]); - _editor.appendTo(document.body); + var _editor = new widget.MediaDialog(null, {}, null, $image[0]).open(); _editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); _editor.on('saved', self, function () { @@ -411,9 +358,8 @@ odoo.define('web_editor.snippets.options', function (require) { }); }, set_active: function () { - var self = this; var src = this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, ""); - this._super(); + this._super.apply(this, arguments); if (this.$target.hasClass('oe_custom_bg')) { this.$el.find('li[data-choose_image]').data("background", src).attr("data-background", src); @@ -436,16 +382,17 @@ odoo.define('web_editor.snippets.options', function (require) { } }); + /** + * The background_position option is designed to change the background image position of a snippet. + */ registry.background_position = SnippetOption.extend({ start: function () { + this._super.apply(this, arguments); + var self = this; var $btn = this.$overlay.find('.oe_options'); $btn.on('show.bs.dropdown', function () { - if (self.$target.css('background-image') === 'none') { - $btn.find('.background_position_li').addClass('hidden'); - } else { - $btn.find('.background_position_li').removeClass('hidden'); - } + $btn.find('.background_position_li').toggleClass('hidden', self.$target.css('background-image') === 'none'); }); }, background_position: function (type, value, $li) { @@ -654,7 +601,7 @@ odoo.define('web_editor.snippets.options', function (require) { on_focus: function () { this.$target.attr('contentEditable', 'false'); this.clear(); - this._super(); + this._super.apply(this, arguments); }, clear: function () { @@ -716,7 +663,7 @@ odoo.define('web_editor.snippets.options', function (require) { this.ID = +$(li).data("id"); this.$target.attr('data-oe-many2one-id', this.ID).data('oe-many2one-id', this.ID); - this.buildingBlock.parent.rte.historyRecordUndo(this.$target); + this.buildingBlock.getParent().rte.historyRecordUndo(this.$target); this.$target.trigger('content_changed'); if (self.$target.data('oe-type') === "contact") { @@ -745,7 +692,7 @@ odoo.define('web_editor.snippets.options', function (require) { }); return { - registry: registry, Class: SnippetOption, + registry: registry, }; }); diff --git a/addons/web_editor/static/src/js/translator.js b/addons/web_editor/static/src/js/translator.js index ead8ea8271c..4fd9825567f 100644 --- a/addons/web_editor/static/src/js/translator.js +++ b/addons/web_editor/static/src/js/translator.js @@ -4,18 +4,13 @@ odoo.define('web_editor.translate', function (require) { var core = require('web.core'); var Model = require('web.Model'); var ajax = require('web.ajax'); -var Class = require('web.Class'); var Widget = require('web.Widget'); var base = require('web_editor.base'); var rte = require('web_editor.rte'); var editor_widget = require('web_editor.widget'); -var qweb = core.qweb; var _t = core._t; -ajax.loadXML('/web_editor/static/src/xml/translator.xml', qweb); - - var translatable = !!$('html').data('translatable'); var edit_translations = !!$('html').data('edit_translations'); @@ -72,17 +67,21 @@ var RTE_Translate = rte.Class.extend({ }); var Translate_Modal = editor_widget.Dialog.extend({ - template: 'web_editor.translator.attributes', - init: function (parent, node) { - this._super(); + init: function (p, options, parent, node) { + this._super(p, _.extend({}, { + title: _t("Translate Attribute"), + buttons: [ + {text: _t("Close"), classes: "btn-primary o_save_button", close: true, click: this.save} + ] + }, options || {})); this.parent = parent; this.$target = $(node); this.translation = $(node).data('translation'); }, start: function () { var self = this; - this._super(); - var $group = this.$el.find('.form-group'); + var def = this._super.apply(this, arguments); + var $group = $("<div/>", {"class": "form-group"}).appendTo(this.$el); _.each(this.translation, function (node, attr) { var $node = $(node); var $label = $('<label class="control-label"></label>').text(attr); @@ -95,6 +94,7 @@ var Translate_Modal = editor_widget.Dialog.extend({ }); $group.append($label).append($input); }); + return def; } }); @@ -109,7 +109,7 @@ var Translate = Widget.extend({ this.ir_translation = new Model('ir.translation'); this.lang = lang || base.get_context().lang; this.setTarget($target); - this._super(); + this._super.apply(this, arguments); this.rte = new RTE_Translate(this, this.config); this.rte.on('change', this, this.rte_changed); @@ -211,7 +211,7 @@ var Translate = Widget.extend({ return; } - new Translate_Modal(event.data, event.target).appendTo('body'); + new Translate_Modal(null, {}, event.data, event.target).open(); }, markTranslatableNodes: function (node) { var self = this; @@ -267,7 +267,7 @@ var Translate = Widget.extend({ destroy: function () { this.cancel(); this.$el.remove(); - this._super(); + this._super.apply(this, arguments); }, config: function ($editable) { diff --git a/addons/web_editor/static/src/js/widgets.js b/addons/web_editor/static/src/js/widgets.js index a4ef757dbd5..aad71c06b6c 100644 --- a/addons/web_editor/static/src/js/widgets.js +++ b/addons/web_editor/static/src/js/widgets.js @@ -3,6 +3,7 @@ odoo.define('web_editor.widget', function (require) { var core = require('web.core'); var ajax = require('web.ajax'); +var Dialog = require('web.Dialog'); var Widget = require('web.Widget'); var base = require('web_editor.base'); var rte = require('web_editor.rte'); @@ -11,43 +12,35 @@ var QWeb = core.qweb; var range = $.summernote.core.range; var dom = $.summernote.core.dom; -////////////////////////////////////////////////////////////////////////////////////////////////////////// +var _t = core._t; -var Dialog = Widget.extend({ - events: { - 'hidden.bs.modal': 'destroy', - 'keydown.dismiss.bs.modal': 'stop_escape', - 'click button.save': 'save', - 'click button[data-dismiss="modal"]': 'cancel', - }, - init: function () { - this._super(); - }, - start: function () { - var sup = this._super(); - this.$el.modal({backdrop: 'static'}); - this.$('input:first').focus(); - return sup; +/** + * Extend Dialog class to handle save/cancel of edition components. + */ +Dialog = Dialog.extend({ + init: function (parent, options) { + options = options || {}; + this._super(parent, _.extend({}, { + buttons: [ + {text: options.save_text || _t("Save"), classes: "btn-primary o_save_button", close: true, click: this.save}, + {text: _t("Discard"), close: true} + ] + }, options)); + + this.destroyAction = "cancel"; + + var self = this; + this.opened().then(function () { + self.$('input:first').focus(); + }); + this.on("closed", this, function () { + this.trigger(this.destroyAction); + }); }, save: function () { + this.destroyAction = "saved"; this.close(); - this.trigger("saved"); }, - cancel: function () { - this.trigger("cancel"); - }, - close: function () { - this.$el.modal('hide'); - }, - destroy: function () { - this._super(); - $("body:has('> .modal:visible')").addClass('modal-open'); - }, - stop_escape: function (event) { - if($(".modal.in").length>0 && event.which == 27) { - event.stopPropagation(); - } - } }); /** @@ -55,24 +48,25 @@ var Dialog = Widget.extend({ */ var alt = Dialog.extend({ template: 'web_editor.dialog.alt', - init: function ($editable, media) { + init: function (parent, options, $editable, media) { + this._super(parent, _.extend({}, { + title: _t("Change media description and tooltip") + }, options)); this.$editable = $editable; this.media = media; this.alt = ($(this.media).attr('alt') || "").replace(/"/g, '"'); this.title = ($(this.media).attr('title') || "").replace(/"/g, '"'); - return this._super(); }, save: function () { - var self = this; - range.createFromNode(self.media).select(); + 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); - setTimeout(function () { - click_event(self.media, "mouseup"); - },0); - return this._super(); + _.defer((function () { + click_event(this.media, "mouseup"); + }).bind(this)); + return this._super.apply(this, arguments); }, }); @@ -86,7 +80,7 @@ var click_event = function (el, type) { * 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 */ var MediaDialog = Dialog.extend({ @@ -94,8 +88,10 @@ var MediaDialog = Dialog.extend({ events : _.extend({}, Dialog.prototype.events, { 'input input#icon-search': 'search', }), - init: function ($editable, media, options) { - this._super(); + init: function (parent, options, $editable, media) { + this._super(parent, _.extend({}, { + title: _t("Select a Media"), + }, options)); if ($editable) { this.$editable = $editable; this.rte = this.$editable.rte || this.$editable.data('rte'); @@ -105,6 +101,9 @@ var MediaDialog = Dialog.extend({ this.media = media; this.isNewMedia = !media; this.range = range.create(); + + this.$modal.addClass('note-image-dialog'); + this.$modal.find('.modal-dialog').addClass('o_select_media_dialog'); }, start: function () { var self = this; @@ -114,20 +113,22 @@ var MediaDialog = Dialog.extend({ this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden'); } - 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 (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")); @@ -142,11 +143,11 @@ var MediaDialog = Dialog.extend({ this.active = this.imageDialog; - $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) { + 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"); - } if ($(event.target).is('[href="#editor-media-document"]')) { + } 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"]')) { @@ -159,7 +160,7 @@ var MediaDialog = Dialog.extend({ } }); - return this._super(); + return this._super.apply(this, arguments); }, save: function () { if (this.options.select_images) { @@ -229,14 +230,14 @@ var MediaDialog = Dialog.extend({ }); /** - * ImageDialog widget. Lets users change an image, including uploading a - * new image in OpenERP or selecting the image style (if supported by + * 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 IMAGES_PER_ROW = 6; -var IMAGES_ROWS = 2; var ImageDialog = Widget.extend({ template: 'web_editor.dialog.image', + IMAGES_PER_ROW: 6, + IMAGES_ROWS: 2, events: _.extend({}, Dialog.prototype.events, { 'change .url-source': function (e) { this.changed($(e.target)); @@ -258,13 +259,12 @@ var ImageDialog = Widget.extend({ 'submit form': 'form_submit', 'change input.url': "change_input", 'keyup input.url': "change_input", - //'change select.image-style': 'preview_image', 'click .existing-attachments [data-src]': 'select_existing', 'click .o_existing_attachment_remove': 'try_remove', 'keydown.dismiss.bs.modal': function () {}, }), init: function (parent, media, options) { - this._super(); + 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']]]; @@ -277,8 +277,8 @@ var ImageDialog = Widget.extend({ start: function () { this.$preview = this.$('.preview-container').detach(); var self = this; - var res = this._super(); - var o = { url: null, alt: null }; + var res = this._super.apply(this, arguments); + var o = {url: null, alt: null}; if ($(this.media).is("img")) { o.url = this.media.getAttribute('src'); @@ -327,13 +327,14 @@ var ImageDialog = Widget.extend({ 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(this.media.tagName !== "IMG") { - var media = document.createElement('img'); + media = document.createElement('img'); $(this.media).replaceWith(media); this.media = media; } @@ -341,7 +342,7 @@ var ImageDialog = Widget.extend({ } else { if (this.media.tagName !== "A") { $('.note-control-selection').hide(); - var media = document.createElement('a'); + media = document.createElement('a'); $(this.media).replaceWith(media); this.media = media; } @@ -358,17 +359,11 @@ var ImageDialog = Widget.extend({ clear: function () { this.media.className = this.media.className.replace(/(^|\s+)((img(\s|$)|img-(?!circle|rounded|thumbnail))[^\s]*)/g, ' '); }, - cancel: function () { - this.trigger('cancel'); - }, change_input: function (e) { var $input = $(e.target); var $button = $input.parent().find("button"); - if ($input.val() === "") { - $button.addClass("btn-default").removeClass("btn-primary"); - } else { - $button.removeClass("btn-default").addClass("btn-primary"); - } + var emptyValue = ($input.val() === ""); + $button.toggleClass("btn-default", emptyValue).toggleClass("btn-primary", !emptyValue); }, search: function (needle) { var self = this; @@ -376,19 +371,15 @@ var ImageDialog = Widget.extend({ self.selected_existing(); }); }, - set_image: function (attachment, error) { - var self = this; + set_image: function (attachment) { this.push(attachment); this.$('input.url').val(''); - this.fetch_existing().then(function () { - self.selected_existing(); - }); + 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) { - var url = $form.find('input[name="url"]').val(); if (this.selected_existing().size()) { event.preventDefault(); return false; @@ -416,10 +407,11 @@ var ImageDialog = Widget.extend({ }; }, file_selection: function () { + var $form = this.$('form'); this.$el.addClass('nosave'); - this.$('form').removeClass('has-error').find('.help-block').empty(); + $form.removeClass('has-error').find('.help-block').empty(); this.$('button.filepicker').removeClass('btn-danger btn-success'); - this.$('form').submit(); + $form.submit(); }, file_selected: function (attachment, error) { var $button = this.$('button.filepicker'); @@ -427,14 +419,12 @@ var ImageDialog = Widget.extend({ $button.addClass('btn-success'); this.set_image(attachment); } else { - this.$('form').addClass('has-error') - .find('.help-block').text(error); + this.$('form').addClass('has-error').find('.help-block').text(error); $button.addClass('btn-danger'); } if (!this.options.select_images) { - // auto save and close popup - this.parent.save(); + this.parent.save(); // auto save and close popup } }, fetch_existing: function (needle) { @@ -448,15 +438,19 @@ var ImageDialog = Widget.extend({ args: [], kwargs: { domain: domain, - fields: ['name', 'mimetype', 'checksum', 'url'], // if we want to use /web/image/xxx with redirect for image url, remove 'url' + fields: ['name', 'mimetype', 'checksum', 'url', 'type'], order: 'id desc', context: base.get_context() } }).then(this.proxy('fetched_existing')); }, fetched_existing: function (records) { - this.records = records; - _.each(records, function (record) { + 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 || '/web/image/' + record.id; record.is_document = !(/gif|jpe|jpg|png/.test(record.mimetype)); }); @@ -464,22 +458,20 @@ var ImageDialog = Widget.extend({ }, display_attachments: function () { var self = this; - var per_screen = IMAGES_PER_ROW * IMAGES_ROWS; + 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 / IMAGES_PER_ROW); }) + .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.$('.existing-attachments').replaceWith(QWeb.render('web_editor.dialog.image.existing.content', {rows: rows})); this.parent.$('.pager') .find('li.previous').toggleClass('disabled', (from === 0)).end() .find('li.next').toggleClass('disabled', (from + per_screen >= records.length)); @@ -528,15 +520,20 @@ var ImageDialog = Widget.extend({ return; } $both.css({borderWidth: "", borderColor: ""}); - $help_block.replaceWith(QWeb.render( - 'web_editor.dialog.image.existing.error', { - views: prevented[id] - } - )); + $help_block.replaceWith(QWeb.render('web_editor.dialog.image.existing.error', { + views: prevented[id] + })); }); }, }); +/* list of font icons to load by editor. The icons are displayed in the media editor and + * identified like font and image (can be colored, spinned, resized with fa classes). + * To add font, push a new object {base, parser} + * - base: class who appear on all fonts (eg: fa fa-refresh) + * - parser: regular expression used to select all font in css style sheets + */ +var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}]; var cacheCssSelectors = {}; var getCssSelectors = function (filter) { @@ -596,7 +593,7 @@ var computeFonts = _.once(function () { }); rte.Class.include({ - init: function (EditorBar) { + init: function () { this._super.apply(this, arguments); computeFonts(); }, @@ -611,18 +608,9 @@ rte.Class.include({ }, }); -/* 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 - */ -var fontIcons = [{'base': 'fa', 'parser': /(?=^|\s)(\.fa-[0-9a-z_-]+::?before)/i}]; - - /** - * FontIconsDialog widget. Lets users change a font awsome, suport all - * font awsome loaded in the css files. + * FontIconsDialog widget. Lets users change a font awesome, support all + * font awesome loaded in the css files. */ var fontIconsDialog = Widget.extend({ template: 'web_editor.dialog.font-icons', @@ -638,28 +626,26 @@ var fontIconsDialog = Widget.extend({ 'keydown.dismiss.bs.modal': function () {}, }), - // extract list of font (like awsome) from the cheatsheet. - renderElement: function () { - this.iconsParser = fontIcons; - this.icons = _.flatten(_.map(fontIcons, function (data) { - return data.icons; - })); - this._super(); - }, - init: function (parent, media) { - this._super(); + this._super.apply(this, arguments); this.parent = parent; this.media = media; computeFonts(); }, start: function () { - return this._super().then(this.proxy('load_data')); + 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) { + return data.icons; + })); + this._super.apply(this, arguments); }, search: function (needle) { var iconsParser = this.iconsParser; if (needle) { - var iconsParser = []; + iconsParser = []; _.filter(this.iconsParser, function (data) { var cssData = _.filter(data.cssData, function (cssData) { return _.find(cssData[3], function (alias) { @@ -684,7 +670,6 @@ var fontIconsDialog = Widget.extend({ save: function () { var self = this; this.parent.trigger("save", this.media); - var icons = this.icons; 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)?$/; @@ -737,7 +722,7 @@ var fontIconsDialog = Widget.extend({ var classes = (this.media&&this.media.className||"").split(/\s+/); for (var i = 0; i < classes.length; i++) { var cls = classes[i]; - switch(cls) { + switch (cls) { case 'fa-1x':case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x': // size classes this.$('#fa-size').val(cls); @@ -765,8 +750,7 @@ var fontIconsDialog = Widget.extend({ } }, /** - * Serializes the dialog to an array of FontAwesome classes. Includes - * the base ``fa``. + * Serializes the dialog to an array of FontAwesome classes. Includes the base ``fa``. */ get_fa_classes: function () { var font = this.getFont(this.$('#fa-icon').val()); @@ -806,40 +790,52 @@ function createVideoNode(url) { var $video = $('<iframe>'); if (ytMatch && ytMatch[1].length === 11) { - var youtubeId = ytMatch[1]; - $video = $('<iframe>') - .attr('src', '//www.youtube.com/embed/' + youtubeId) - .attr('width', '640').attr('height', '360'); + var youtubeId = ytMatch[1]; + $video = $('<iframe>', { + src: '//www.youtube.com/embed/' + youtubeId, + width: '640', + height: '360' + }); } else if (igMatch && igMatch[0].length) { - $video = $('<iframe>') - .attr('src', igMatch[0] + '/embed/') - .attr('width', '612').attr('height', '710') - .attr('scrolling', 'no') - .attr('allowtransparency', 'true'); + $video = $('<iframe>', { + src: igMatch[0] + '/embed/', + width: '612', + height: '710', + scrolling: 'no', + allowtransparency: 'true' + }); } else if (vMatch && vMatch[0].length) { - $video = $('<iframe>') - .attr('src', vMatch[0] + '/embed/simple') - .attr('width', '600').attr('height', '600') - .attr('class', 'vine-embed'); + $video = $('<iframe>', { + src: vMatch[0] + '/embed/simple', + width: '600', + height: '600', + class: 'vine-embed' + }); } else if (vimMatch && vimMatch[3].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') - .attr('src', '//player.vimeo.com/video/' + vimMatch[3]) - .attr('width', '640').attr('height', '360'); + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + src: '//player.vimeo.com/video/' + vimMatch[3], + width: '640', + height: '360' + }); } else if (dmMatch && dmMatch[2].length) { - $video = $('<iframe>') - .attr('src', '//www.dailymotion.com/embed/video/' + dmMatch[2]) - .attr('width', '640').attr('height', '360'); + $video = $('<iframe>', { + src: '//www.dailymotion.com/embed/video/' + dmMatch[2], + width: '640', + height: '360' + }); } else if (youkuMatch && youkuMatch[1].length) { - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') - .attr('height', '498') - .attr('width', '510') - .attr('src', '//player.youku.com/embed/' + youkuMatch[1]); + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + height: '498', + width: '510', + src: '//player.youku.com/embed/' + youkuMatch[1] + }); } else { - // this is not a known video link. Now what, Cat? Now what? - $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>') - .attr('width', '640') - .attr('height', '360') - .attr('src', url); + // this is not a known video link. Now what, Cat? Now what? + $video = $('<iframe webkitallowfullscreen mozallowfullscreen allowfullscreen>', { + width: '640', + height: '360', + src: url + }); } $video.attr('frameborder', 0); @@ -863,7 +859,7 @@ var VideoDialog = Widget.extend({ 'keydown.dismiss.bs.modal': function () {}, }), init: function (parent, media) { - this._super(); + this._super.apply(this, arguments); this.parent = parent; this.media = media; }, @@ -876,7 +872,7 @@ var VideoDialog = Widget.extend({ this.$("input#urlvideo").val(src); this.get_video(); } - return this._super(); + return this._super.apply(this, arguments); }, change_input: function (e) { var $input = $(e.target); @@ -910,7 +906,6 @@ var VideoDialog = Widget.extend({ this.$("button.btn-primary").click(); video_id = this.$("#video_id").val(); } - var video_type = this.$("#video_type").val(); var $iframe = $( '<div class="media_iframe_video" data-src="'+this.$iframe.attr("src")+'">'+ '<div class="css_editable_mode_display"> </div>'+ @@ -932,7 +927,9 @@ var VideoDialog = Widget.extend({ }, }); -/* ----- EDITOR: LINK & MEDIA ---- */ +/** + * The Link Dialog allows to customize link content and style. + */ var LinkDialog = Dialog.extend({ template: 'web_editor.dialog.link', events: _.extend({}, Dialog.prototype.events, { @@ -947,8 +944,10 @@ var LinkDialog = Dialog.extend({ this.preview(); }, }), - init: function (editable, linkInfo) { - this._super(editable, linkInfo); + init: function (parent, options, editable, linkInfo) { + this._super(parent, _.extend({}, { + title: _t("Link to") + }, options || {})); this.editable = editable; this.data = linkInfo || {}; @@ -985,20 +984,20 @@ var LinkDialog = Dialog.extend({ } else if (eo !== ec.textContent.length) { ec.splitText(eo); } - + nodes = dom.listBetween(sc, ec); // browsers can't target a picture or void node if (dom.isVoid(sc) || dom.isImg(sc)) { - so = dom.listPrev(sc).length-1; - sc = sc.parentNode; + so = dom.listPrev(sc).length-1; + sc = sc.parentNode; } if (dom.isBR(ec)) { - eo = dom.listPrev(ec).length-1; - ec = ec.parentNode; + eo = dom.listPrev(ec).length-1; + ec = ec.parentNode; } else if (dom.isVoid(ec) || dom.isImg(sc)) { - eo = dom.listPrev(ec).length; - ec = ec.parentNode; + eo = dom.listPrev(ec).length; + ec = ec.parentNode; } this.data.range = range.create(sc, so, ec, eo); @@ -1034,7 +1033,7 @@ var LinkDialog = Dialog.extend({ start: function () { this.bind_data(); this.$('input.url-source:eq(1)').closest('.list-group-item').addClass('active'); - return this._super(); + return this._super.apply(this, arguments); }, get_data: function (test) { var self = this; @@ -1081,15 +1080,14 @@ var LinkDialog = Dialog.extend({ 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, ''); - - self.trigger("save", self.data); - }).then(_super); + 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, ''); + + self.trigger("save", self.data); + }).then(_super); }, bind_data: function () { var href = this.data.url; @@ -1110,8 +1108,8 @@ var LinkDialog = Dialog.extend({ } if (href) { - var match; - if(match = /mailto:(.+)/.exec(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); @@ -1144,12 +1142,11 @@ var LinkDialog = Dialog.extend({ }); return { - 'getCssSelectors': getCssSelectors, - 'Dialog': Dialog, - 'alt': alt, - 'MediaDialog': MediaDialog, - 'fontIcons': fontIcons, - 'LinkDialog': LinkDialog + Dialog: Dialog, + alt: alt, + MediaDialog: MediaDialog, + LinkDialog: LinkDialog, + getCssSelectors: getCssSelectors, + fontIcons: fontIcons, }; - }); diff --git a/addons/web_editor/static/src/less/web_editor.ui.components.less b/addons/web_editor/static/src/less/web_editor.ui.components.less index 6cf994e2f03..d89929bb13d 100644 --- a/addons/web_editor/static/src/less/web_editor.ui.components.less +++ b/addons/web_editor/static/src/less/web_editor.ui.components.less @@ -172,6 +172,7 @@ body .modal { &.o_webimage { width: 100%; height: auto; + background-image: none!important; } } &.o_selected .o_image { @@ -329,11 +330,4 @@ body .modal { } } } - - &.web_editor-dialog { - li > label { - width: 96%; - margin: 4px 2% 0; - } - } } 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 57caa2dda0e..6cbff90b572 100644 --- a/addons/web_editor/static/src/less/web_editor.ui.less +++ b/addons/web_editor/static/src/less/web_editor.ui.less @@ -180,6 +180,10 @@ html[lang] > body.editor_enable{ opacity: 0.8; } } + + &:nth-child(2n+1) { + clear: left; + } } } } @@ -495,7 +499,7 @@ html[lang] > body.editor_enable{ cursor: e-resize; width: @odoo-handle-edge-size; .o-position-absolute(@o-handles-offset-to-hide, auto, -@o-handles-offset-to-hide, 0); - + &:after { .o-position-absolute(@top: 50%, @left: -@odoo-handle-border-width); margin-top: -@o-handle-size/2; @@ -506,7 +510,7 @@ html[lang] > body.editor_enable{ cursor: w-resize; width: @odoo-handle-edge-size; .o-position-absolute(@o-handles-offset-to-hide, 0, -@o-handles-offset-to-hide, auto); - + &:after { .o-position-absolute(@top: 50%, @right: -@odoo-handle-border-width); margin-top: -@o-handle-size/2; @@ -657,7 +661,7 @@ html[lang] > body.editor_enable{ width: auto; text-align: center; box-shadow: none; - + .popover-content { height: @odoo-navbar-height; @@ -785,7 +789,7 @@ table.colorpicker { padding: 0; list-style: none; text-align: center; - + > li { display: block; float: none; @@ -944,7 +948,7 @@ body.editor_enable { padding-top: @odoo-navbar-height; > main { height: 100%; - #editable_area { + #editable_area { position: relative; overflow: hidden; max-width: 600px; diff --git a/addons/web_editor/static/src/xml/editor.xml b/addons/web_editor/static/src/xml/editor.xml index 49743987fc3..f4fb773c8c3 100644 --- a/addons/web_editor/static/src/xml/editor.xml +++ b/addons/web_editor/static/src/xml/editor.xml @@ -1,5 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <templates id="template" xml:space="preserve"> + <!--=================--> + <!-- Base components --> + <!--=================--> + + <!-- Editor top bar which contains the summernote tools and save/discard buttons --> <t t-name="web_editor.editorbar"> <div id="web_editor-top-edit" class="o_css_editor"> <div id="web_editor-toolbars"/> @@ -9,187 +14,72 @@ </form> </div> </t> - <t t-name="web_editor.dialog"> - <div class="modal o_website_modal web_editor-dialog" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title"> - <t t-esc="title"/> - </h3> - </div> - <div class="modal-body"> - <t t-raw="0"/> - </div> - <div class="modal-footer"> - <button class="btn btn-sm btn-primary save" t-if="!button" type="button">Save</button> - <button class="btn btn-sm btn-primary save" t-if="button" type="button"><t t-raw="button"/></button> - <button class="btn hidden wait" disabled="disabled" type="button"/> - <a class="btn btn-sm btn-default" data-dismiss="modal" href="#" aria-hidden="true">Discard</a> - </div> - </div> - </div> - </div> + + <!-- Custom checkbox (material-design-like toggle) --> + <t t-name="web_editor.components.switch"> + <label class="o_switch" t-att-for="id"> + <input type="checkbox" t-att-id="id"/> + <span/> + <t t-if="label"><t t-esc="label"/></t> + </label> </t> - <div t-name="web_editor.dialog.background_position" class="o_bg_img_opt_modal"> - <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="cover"/> Cover</label> - <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="contain"/> Contain</label> - <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="custom"/> Custom</label> - <div class="o_bg_img_opt" data-value="cover"> - <a class="help-control collapsed" role="button" data-toggle="collapse" href="#cover_help" aria-expanded="false" aria-controls="cover_help"><span class="fa fa-question-circle"/></a> - <div class="help collapse" id="cover_help"> - <div class="simulator cover_bg"> - <div class="bg"/> - <div class="el"/> - </div> - <p class="help-text">Scale the image to be as large as it needs to fill the whole content area. Part of the image may not be visible.</p> - </div> + <!--=================--> + <!-- Edition Dialogs --> + <!--=================--> - <p><strong>Assigning a focal point to ensure that the targeted area will be always visible</strong></p> - <div class="o_bg_img_opt_cover_edition"> - <h6>Click on the picture</h6> - <div class="o_bg_img_opt_object"> - <span class="grid grid-1"/> - <span class="grid grid-2"/> - <span class="grid grid-3"/> - <span class="grid grid-4"/> - <span class="o_focus_point"/> - <div class="o_bg_img_opt_ui_info">X: <span class="o_x"/> Y: <span class="o_y"/></div> - </div> + <!-- Alt Dialog (allows to change alt and title of page images) --> + <form t-name="web_editor.dialog.alt" action="#" class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-2 control-label" for="alt">Description</label> + <div class="col-sm-9"> + <input class="form-control" id="alt" required="required" t-att-value="widget.alt" type="text"/> </div> </div> - <div class="o_bg_img_opt" data-value="contain"> - <a class="help-control collapsed" role="button" data-toggle="collapse" href="#contain_help" aria-expanded="false" aria-controls="contain_help"><span class="fa fa-question-circle"/></a> - <div class="help collapse" id="contain_help"> - <div class="simulator contain_bg"> - <div class="bg"/> - <div class="el"/> - </div> - <p class="help-text">Scale the image to the largest size such that both its width and height can fit inside the content area</p> + <div class="form-group"> + <label class="col-sm-2 control-label" for="title">Tooltip</label> + <div class="col-sm-9"> + <input class="form-control" id="title" required="required" t-att-value="widget.title" type="text"/> </div> + </div> + </form> - <form class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-2 control-label" for="repeatInput">Repeat</label> - <div class="col-sm-5"> - <div class="checkbox"> - <label> - <input type="checkbox" id="o_bg_img_opt_contain_repeat"/> - </label> - </div> + <!-- Media Dialog (allows to choose an img/pictogram/video) --> + <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"> + <ul class="pager mb0 mt0"> + <li class="previous disabled"> + <a href="#">← Previous</a> + </li> + <li class="next disabled"> + <a href="#">Next →</a> + </li> + </ul> + </li> + <li class="search"> + <form action="#"> + <div class="form-group"> + <input class="form-control" id="icon-search" type="search"/> + <span class="fa fa-search"/> </div> - </div> - </form> + </form> + </li> + </ul> + <!-- Tab panes --> + <div class="tab-content"> + <div class="tab-pane fade in active" id="editor-media-image"/> + <div class="tab-pane fade" id="editor-media-document"/> + <div class="tab-pane fade" id="editor-media-icon"/> + <div class="tab-pane fade" id="editor-media-video"/> </div> - <form class="o_bg_img_opt form-horizontal" data-value="custom"> - <fieldset> - <legend>Background size</legend> - <p>Sets the width and height of the background image in percent of the parent element.</p> - <div class="form-group"> - <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_size_x">Background width</label> - <div class="col-sm-5"> - <div class="input-group"> - <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> - <div class="input-group-addon">%</div> - </div> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_size_y">Background height</label> - <div class="col-sm-5"> - <div class="input-group"> - <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> - <div class="input-group-addon">%</div> - </div> - </div> - </div> - </fieldset> - <fieldset> - <legend>Background position</legend> - <p>Set the starting position of the background image.</p> - <div class="form-group"> - <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_pos_x">Horizontal</label> - <div class="col-sm-5"> - <div class="input-group"> - <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> - <div class="input-group-addon">%</div> - </div> - </div> - </div> - <div class="form-group"> - <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_pos_y">Vertical</label> - <div class="col-sm-5"> - <div class="input-group"> - <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> - <div class="input-group-addon">%</div> - </div> - </div> - </div> - </fieldset> - <fieldset> - <legend>Background repeat</legend> - <p>Define if/how the background image will be repeated</p> - <select class="form-control" id="o_bg_img_opt_custom_repeat"> - <option value="">No repeat</option> - <option value="o_bg_img_opt_repeat">Repeat both</option> - <option value="o_bg_img_opt_repeat_x">Repeat x</option> - <option value="o_bg_img_opt_repeat_y">Repeat y</option> - </select> - </fieldset> - </form> </div> - <t t-name="web_editor.dialog.media"> - <div class="modal note-image-dialog o_website_modal" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog modal-lg o_select_media_dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Select a Media</h3> - </div> - <div class="modal-body"> - <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"> - <ul class="pager mb0 mt0"> - <li class="previous disabled"> - <a href="#">← Previous</a> - </li> - <li class="next disabled"> - <a href="#">Next →</a> - </li> - </ul> - </li> - <li class="search"> - <form action="#"> - <div class="form-group"> - <input class="form-control" id="icon-search" type="search"/> - <span class="fa fa-search"/> - </div> - </form> - </li> - </ul> - <!-- Tab panes --> - <div class="tab-content"> - <div class="tab-pane fade in active" id="editor-media-image"/> - <div class="tab-pane fade" id="editor-media-document"/> - <div class="tab-pane fade" id="editor-media-icon"/> - <div class="tab-pane fade" id="editor-media-video"/> - </div> - </div> - <div class="modal-footer"> - <button class="btn btn-sm btn-primary save" type="button">Save</button> - <button class="btn btn-sm hidden wait" disabled="disabled" type="button"/> - <a class="btn btn-sm btn-default" data-dismiss="modal" href="#" aria-hidden="true">Discard</a> - </div> - </div> - </div> - </div> - </t> + + <!-- Image choosing part of the Media Dialog --> <t t-name="web_editor.dialog.image"> <div> <form method="POST" @@ -276,45 +166,8 @@ </ul> </div> </t> - <t t-name="web_editor.discard"> - <div class="modal o_website_modal" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Discard edition</h3> - </div> - <div class="modal-body"> - <p>If you discard the current edition, <em>all</em> unsaved changes will be lost.</p> - <p>You can cancel to return to the edition mode.</p> - </div> - <div class="modal-footer"> - <button class="btn btn-sm btn-danger" type="button">Discard</button> - <a class="btn btn-sm btn-default" data-dismiss="modal" href="#" aria-hidden="true">Cancel</a> - </div> - </div> - </div> - </div> - </t> - <t t-name="web_editor.dialog.alt"> - <t t-call="web_editor.dialog"> - <t t-set="title">Change media description and tooltip</t> - <form action="#" class="form-horizontal"> - <div class="form-group"> - <label class="col-sm-2 control-label" for="alt">Description</label> - <div class="col-sm-9"> - <input class="form-control" id="alt" required="required" t-att-value="widget.alt" type="text"/> - </div> - </div> - <div class="form-group"> - <label class="col-sm-2 control-label" for="title">Tooltip</label> - <div class="col-sm-9"> - <input class="form-control" id="title" required="required" t-att-value="widget.title" type="text"/> - </div> - </div> - </form> - </t> - </t> + + <!-- Icon choosing part of the Media Dialog --> <t t-name="web_editor.dialog.font-icons"> <form action="#"> <input type="hidden" id="fa-icon"/> @@ -331,6 +184,8 @@ <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"/> </t> </t> + + <!-- Video choosing part of the Media Dialog --> <t t-name="web_editor.dialog.video"> <form action="#" class="form-inline"> <div class="well"> @@ -361,117 +216,221 @@ </div> </form> </t> - <t t-name="web_editor.dialog.link"> - <t t-call="web_editor.dialog"> - <t t-set="title">Link to</t> - <form> - <div class="list-group"> - <div class="form-group list-group-item active"> - <h3 class="list-group-item-heading"> - <label class="control-label" for="link-new">Link Label - </label> - </h3> - <input class="form-control" id="link-text" required="required" type="text"/> + + <!-- Link Dialog (allows to choose a style and content for a link on the page) --> + <form t-name="web_editor.dialog.link"> + <div class="list-group"> + <div class="form-group list-group-item active"> + <h3 class="list-group-item-heading"> + <label class="control-label" for="link-new">Link Label + </label> + </h3> + <input class="form-control" id="link-text" required="required" type="text"/> + </div> + </div> + <ul class="list-group"> + <li class="list-group-item form-group clearfix active"> + <div class="pull-right" style="display: none;"> + <label> + <input class="window-new" type="checkbox"/>Open in new window</label> + </div> + <h4 class="list-group-item-heading"> + <label class="control-label" for="link-external">URL or Email Address + </label> + </h4> + <input class="form-control url email-address url-source" id="link-external" placeholder="http://openerp.com" type="text"/> + </li> + </ul> + <div class="form-horizontal link-style"> + <div class="form-group"> + <label class="col-sm-2 control-label">Style + </label> + <div class="col-sm-4"> + <div class="dropdown" style="line-height: 2em;"> + <a class="btn btn-default" data-toggle="dropdown" href="#">Color Style + <span class="caret"/></a> + <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> + <li> + <label class="btn btn-link">Link + <input checked="checked" class="link-style" name="link-style-type" type="radio" value=""/></label> + </li> + <li> + <label class="btn btn-default">Basic + <input class="link-style" name="link-style-type" type="radio" value="btn-default"/></label> + </li> + <li> + <label class="btn btn-success">Success + <input class="link-style" name="link-style-type" type="radio" value="btn-success"/></label> + </li> + <li> + <label class="btn btn-primary">Primary + <input class="link-style" name="link-style-type" type="radio" value="btn-primary"/></label> + </li> + <li> + <label class="btn btn-info">Info + <input class="link-style" name="link-style-type" type="radio" value="btn-info"/></label> + </li> + <li> + <label class="btn btn-warning">Warning + <input class="link-style" name="link-style-type" type="radio" value="btn-warning"/></label> + </li> + <li> + <label class="btn btn-danger">Danger + <input class="link-style" name="link-style-type" type="radio" value="btn-danger"/></label> + </li> + </ul> + </div> + </div> + <div class="col-sm-4"> + <div class="dropdown" style="line-height: 2em;"> + <a class="btn btn-default" data-toggle="dropdown" href="#">Size + <span class="caret"/></a> + <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> + <li> + <label class="btn btn-default btn-xs">Extra Small + <input class="link-style" name="link-style-size" type="radio" value="btn-xs"/></label> + </li> + <li> + <label class="btn btn-default btn-sm">Small + <input class="link-style" name="link-style-size" type="radio" value="btn-sm"/></label> + </li> + <li> + <label class="btn btn-default">Default + <input checked="checked" class="link-style" name="link-style-size" type="radio" value=""/></label> + </li> + <li> + <label class="btn btn-default btn-lg">Large + <input class="link-style" name="link-style-size" type="radio" value="btn-lg"/></label> + </li> + </ul> </div> </div> - <ul class="list-group"> - <li class="list-group-item form-group clearfix active"> - <div class="pull-right" style="display: none;"> + </div> + </div> + <div class="form-horizontal link-style"> + <div class="form-group"> + <label class="col-sm-2 control-label">Preview + </label> + <div class="col-sm-10" style="line-height: 2em;"> + <div style="overflow: auto; max-width: 100%; max-height: 200px;"> + <a href="#" id="link-preview"/> + </div> + </div> + </div> + </div> + </form> + + <!--=================--> + <!-- Snippet options --> + <!--=================--> + + <!-- Background position option dialog --> + <div t-name="web_editor.dialog.background_position" class="o_bg_img_opt_modal"> + <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="cover"/> Cover</label> + <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="contain"/> Contain</label> + <label class="radio-inline"><input type="radio" name="o_bg_img_opt" value="custom"/> Custom</label> + + <div class="o_bg_img_opt" data-value="cover"> + <a class="help-control collapsed" role="button" data-toggle="collapse" href="#cover_help" aria-expanded="false" aria-controls="cover_help"><span class="fa fa-question-circle"/></a> + <div class="help collapse" id="cover_help"> + <div class="simulator cover_bg"> + <div class="bg"/> + <div class="el"/> + </div> + <p class="help-text">Scale the image to be as large as it needs to fill the whole content area. Part of the image may not be visible.</p> + </div> + + <p><strong>Assigning a focal point to ensure that the targeted area will be always visible</strong></p> + <div class="o_bg_img_opt_cover_edition"> + <h6>Click on the picture</h6> + <div class="o_bg_img_opt_object"> + <span class="grid grid-1"/> + <span class="grid grid-2"/> + <span class="grid grid-3"/> + <span class="grid grid-4"/> + <span class="o_focus_point"/> + <div class="o_bg_img_opt_ui_info">X: <span class="o_x"/> Y: <span class="o_y"/></div> + </div> + </div> + </div> + <div class="o_bg_img_opt" data-value="contain"> + <a class="help-control collapsed" role="button" data-toggle="collapse" href="#contain_help" aria-expanded="false" aria-controls="contain_help"><span class="fa fa-question-circle"/></a> + <div class="help collapse" id="contain_help"> + <div class="simulator contain_bg"> + <div class="bg"/> + <div class="el"/> + </div> + <p class="help-text">Scale the image to the largest size such that both its width and height can fit inside the content area</p> + </div> + + <form class="form-horizontal"> + <div class="form-group"> + <label class="col-sm-2 control-label" for="repeatInput">Repeat</label> + <div class="col-sm-5"> + <div class="checkbox"> <label> - <input class="window-new" type="checkbox"/>Open in new window</label> - </div> - <h4 class="list-group-item-heading"> - <label class="control-label" for="link-external">URL or Email Address + <input type="checkbox" id="o_bg_img_opt_contain_repeat"/> </label> - </h4> - <input class="form-control url email-address url-source" id="link-external" placeholder="http://openerp.com" type="text"/> - </li> - </ul> - <div class="form-horizontal link-style"> - <div class="form-group"> - <label class="col-sm-2 control-label">Style - </label> - <div class="col-sm-4"> - <div class="dropdown" style="line-height: 2em;"> - <a class="btn btn-default" data-toggle="dropdown" href="#">Color Style - <span class="caret"/></a> - <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> - <li> - <label class="btn btn-link">Link - <input checked="checked" class="link-style" name="link-style-type" type="radio" value=""/></label> - </li> - <li> - <label class="btn btn-default">Basic - <input class="link-style" name="link-style-type" type="radio" value="btn-default"/></label> - </li> - <li> - <label class="btn btn-success">Success - <input class="link-style" name="link-style-type" type="radio" value="btn-success"/></label> - </li> - <li> - <label class="btn btn-primary">Primary - <input class="link-style" name="link-style-type" type="radio" value="btn-primary"/></label> - </li> - <li> - <label class="btn btn-info">Info - <input class="link-style" name="link-style-type" type="radio" value="btn-info"/></label> - </li> - <li> - <label class="btn btn-warning">Warning - <input class="link-style" name="link-style-type" type="radio" value="btn-warning"/></label> - </li> - <li> - <label class="btn btn-danger">Danger - <input class="link-style" name="link-style-type" type="radio" value="btn-danger"/></label> - </li> - </ul> - </div> </div> - <div class="col-sm-4"> - <div class="dropdown" style="line-height: 2em;"> - <a class="btn btn-default" data-toggle="dropdown" href="#">Size - <span class="caret"/></a> - <ul aria-labelledby="dLabel" class="dropdown-menu" role="menu"> - <li> - <label class="btn btn-default btn-xs">Extra Small - <input class="link-style" name="link-style-size" type="radio" value="btn-xs"/></label> - </li> - <li> - <label class="btn btn-default btn-sm">Small - <input class="link-style" name="link-style-size" type="radio" value="btn-sm"/></label> - </li> - <li> - <label class="btn btn-default">Default - <input checked="checked" class="link-style" name="link-style-size" type="radio" value=""/></label> - </li> - <li> - <label class="btn btn-default btn-lg">Large - <input class="link-style" name="link-style-size" type="radio" value="btn-lg"/></label> - </li> - </ul> - </div> + </div> + </div> + </form> + </div> + <form class="o_bg_img_opt form-horizontal" data-value="custom"> + <fieldset> + <legend>Background size</legend> + <p>Sets the width and height of the background image in percent of the parent element.</p> + <div class="form-group"> + <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_size_x">Background width</label> + <div class="col-sm-5"> + <div class="input-group"> + <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_size_x" placeholder="auto"/> + <div class="input-group-addon">%</div> </div> </div> </div> - <div class="form-horizontal link-style"> - <div class="form-group"> - <label class="col-sm-2 control-label">Preview - </label> - <div class="col-sm-10" style="line-height: 2em;"> - <div style="overflow: auto; max-width: 100%; max-height: 200px;"> - <a href="#" id="link-preview"/> - </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_size_y">Background height</label> + <div class="col-sm-5"> + <div class="input-group"> + <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_size_y" placeholder="auto"/> + <div class="input-group-addon">%</div> </div> </div> </div> - </form> - </t> - </t> - <t t-name="web_editor.components.switch"> - <label class="o_switch" t-att-for="id"> - <input type="checkbox" t-att-id="id"/> - <span/> - <t t-if="label"><t t-esc="label"/></t> - </label> - </t> + </fieldset> + <fieldset> + <legend>Background position</legend> + <p>Set the starting position of the background image.</p> + <div class="form-group"> + <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_pos_x">Horizontal</label> + <div class="col-sm-5"> + <div class="input-group"> + <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_pos_x" placeholder="auto"/> + <div class="input-group-addon">%</div> + </div> + </div> + </div> + <div class="form-group"> + <label class="col-sm-3 control-label" for="o_bg_img_opt_custom_pos_y">Vertical</label> + <div class="col-sm-5"> + <div class="input-group"> + <input type="number" min="0" max="100" class="form-control input-sm" id="o_bg_img_opt_custom_pos_y" placeholder="auto"/> + <div class="input-group-addon">%</div> + </div> + </div> + </div> + </fieldset> + <fieldset> + <legend>Background repeat</legend> + <p>Define if/how the background image will be repeated</p> + <select class="form-control" id="o_bg_img_opt_custom_repeat"> + <option value="">No repeat</option> + <option value="o_bg_img_opt_repeat">Repeat both</option> + <option value="o_bg_img_opt_repeat_x">Repeat x</option> + <option value="o_bg_img_opt_repeat_y">Repeat y</option> + </select> + </fieldset> + </form> + </div> </templates> diff --git a/addons/web_editor/static/src/xml/translator.xml b/addons/web_editor/static/src/xml/translator.xml deleted file mode 100644 index 74b97905355..00000000000 --- a/addons/web_editor/static/src/xml/translator.xml +++ /dev/null @@ -1,23 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<templates id="template" xml:space="preserve"> - -<t t-name="web_editor.translator.attributes"> - <div class="modal web_editor-dialog" tabindex="-1" role="dialog" aria-hidden="true"> - <div class="modal-dialog"> - <div class="modal-content"> - <div class="modal-header"> - <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> - <h3 class="modal-title">Translate Attribute</h3> - </div> - <div class="modal-body"> - <div class="form-group"></div> - </div> - <div class="modal-footer"> - <button type="button" class="btn btn-primary save">Close</button> - </div> - </div> - </div> - </div> -</t> - -</templates> |
