[REF] web_editor, website, *: complete refactoring

* mass_mailing, payment, point_of_sale, portal, survey, web, web_tour,
  website_blog, website_crm_partner_assign, website_event,
  website_event_questions, website_form, website_forum, website_gengo,
  website_hr_recruitment, website_links, website_mail,
  website_mail_channel, website_mass_mailing, website_quote,
  website_sale, website_sale_options, website_slides, website_twitter

This commit reviews the whole "JS side" of the web_editor and website
apps. This is a first step to be able to improve them with new and
better functionnalities; this commit is not supposed to change any
visual behavior.

The main goal was to achieve a structure similar to the backend one.
Now, the frontend side also has a root widget (like the WebClient)
and all other widgets are attached to it one way or another. This allows
the benefits of using the 'trigger_up' functionnality for example.

As RPC are now mainly done with the `this._rpc` functionnality (being
possible thanks to the parent hierarchy), the frontend will also be
possible to test thanks to QUnit in a future update (besides the "text"
editor side which still requires a refactoring to be able to do that).

---

Here are some of the changes:

(-) conventions and documentation

The code has been updated to follow JS conventions and a lot of code has
been commented (around +2000 lines of comment). This also means that
lots of functions have been renamed to use camelCase or simply to make
their name understandable.
See https://github.com/odoo/odoo/wiki/Javascript-coding-guidelines.

(-) deprecated: web_editor.base

The "web_editor.base" module has been split and does not force the
modules which require it to wait for DOM ready anymore. This was indeed
slowing loading times, but also prevented to use some modules in some
contexts (see the LESS editor use in web_studio which is the subject of
another task).
Now the "editor context" can be got thanks to the "web_editor.context"
JS module with its "get" function.
The "web_editor.base" module should probably not be used anymore (see
its code and recent updates).

(-) new: web.dom_ready

If a JS module should wait for the DOM to be ready to be executed, a
new JS module has been created: "web.dom_ready". This should always
be used in a module which only want to instantiate stuff. Do not
extend (or worst, include) classes after DOM ready.

(-) website.website

The "website.website" module has been split. "website.website" does not
return anything useful anymore, it just initialize some miscellaneous
stuff, without waiting for the DOM to be ready. You might want to check
"website.utils", "website.content.compatibility" or `WebsiteRoot`. Also
`website.form` has been deleted (use `this._rpc`), so has been
`website.error`. `website.prompt` will be removed in a future update to
be replaced by `Dialog.prompt`.

(-) widgets are great

Many classes which were not widgets are now widgets. This allows them to
use the 'events', the 'xmlDependencies' and the 'this._rpc' features for
example. Here are some of the main ones:

- Snippet options: these were classes with a `$el` for the menu element
    and `$target` for the customized element. This is still the case
    but following standard `Widget` structure (one exception: using
    `this.$(...)` searches in the `$target` as before this update).

- Snippet animations: instead of class instances with a `$target`
    element which can be `start` and `stop`, these are now standard
    widgets which can be `start` and `destroy`. `this.$target` is
    an alias to `this.$el` for ease of compatibility.

- Snippet editors: instead of class instances in charge of an editor
    overlay, these are now widgets. Each "child" snippet editor is
    properly attached as a "child", which allows editors to communicate
    and to be properly destroyed.

(-) root widgets and website navbar

The frontend is different of the backend. In the backend, the page has
an empty <body/> element and all the components are instantiated from
parent to children (i.e. the `WebClient` is instantiated and is in
charge of instantiating the `ControlPanel`, etc). The frontend cannot
work like that on page loadings as they are way more frequent than in
the backend and we do not want them to flicker. A frontend page is
loaded as a <body/> element which already contains the website navbar
and its menus and the whole content page. JS code has to be "attached"
to these existing elements. This is possible thanks to the `RootWidget`
instances and the specialized `WebsiteRoot`, `IframeRoot` and
`WebsiteNavbar` (see code for details).

(-) lazy loading

No more (or at least a lot less) XML/JS has to be loaded on page
loading, thanks to the use of the `Widget.xmlDependencies` feature.
XML which have to be lazy loaded is loaded only on related Widget
instantiation if necessary, which allows to execute a lot of JS code
before the DOM is ready and to start many widgets on DOM ready (not
later). A visual benefit of this is clicking on the 'edit' button as
soon as it is possible: before this commit, this was sometimes not
doing anything as event handlers were not binded yet.

Still a possible exception: loading the session and locales. This may
be asynchronous stuff which is still required before widget
instantiations but this will be the subject of another task.

(-) deprecated code and code location

More than reviewing code and organizing it, many apparent dead code was
removed. More importantly, mislocated code was put in the right app.
This is the case for snippet animations which is a concept for website
apps but was defined in the web_editor app, or some translation concepts
which were part of website but should have been part of web_editor.

---

There are probably more things to say about this commit but I will let
the comments speak for those.
This commit is contained in:
qsm-odoo
2017-08-16 11:04:14 +02:00
parent 4f925af06f
commit 2972976962
122 changed files with 9879 additions and 7913 deletions
@@ -1,14 +1,14 @@
odoo.define('mass_mailing.editor', function (require) {
"use strict";
var ajax = require("web.ajax");
var core = require("web.core");
require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var rte = require('web_editor.rte');
var web_editor = require('web_editor.editor');
var options = require('web_editor.snippets.options');
var snippets_editor = require('web_editor.snippet.editor');
var $editable_area = $("#editable_area");
var $editable_area = $('#editable_area');
var odoo_top = window.top.odoo;
// Snippet option for resizing image and column width inline like excel
@@ -34,11 +34,10 @@ options.registry["width-x"] = options.Class.extend({
var $handle = $(this);
var compass = false;
_.each(['n', 's', 'e', 'w' ], function(handler) {
_.each(['n', 's', 'e', 'w' ], function (handler) {
if ($handle.hasClass(handler)) { compass = handler; }
});
if (self.is_image) { compass = "image"; }
self.buildingBlock.editor_busy = true;
$body.on("mousemove.mass_mailing_width_x", function (event) {
event.preventDefault();
@@ -60,16 +59,11 @@ options.registry["width-x"] = options.Class.extend({
self.change_width(event, self.$target, target_width, offset, true);
}
});
$body.on("mouseup.mass_mailing_width_x", function () {
$body.off('.mass_mailing_width_x');
self.buildingBlock.editor_busy = false;
self.$target.removeClass("resize_editor_busy");
});
});
},
change_width: function (event, target, target_width, offset, grow) {
target.css("width", grow ? (event.pageX - offset) : (offset + target_width - event.pageX));
this.buildingBlock.cover_target(this.$overlay, this.$target);
this.trigger_up('cover_update');
},
get_int_width: function (el) {
return parseInt($(el).css("width"), 10);
@@ -77,7 +71,7 @@ options.registry["width-x"] = options.Class.extend({
get_max_width: function ($el) {
return this.container_width - _.reduce(_.map($el.siblings(), this.get_int_width), function (memo, w) { return memo + w; });
},
on_focus: function () {
onFocus: function () {
this._super.apply(this, arguments);
if (this.$target.is("td, th")) {
@@ -87,34 +81,34 @@ options.registry["width-x"] = options.Class.extend({
});
options.registry.table_item = options.Class.extend({
on_clone: function ($clone) {
onClone: function () {
this._super.apply(this, arguments);
// If we cloned a td or th element...
if (this.$target.is("td, th")) {
// ... and that the td or th element was alone on its row ...
if (this.$target.siblings().length === 1) {
var $tr = $clone.parent();
$tr.clone().empty().insertAfter($tr).append($clone); // ... move the clone in a new row instead
var $tr = this.$target.parent();
$tr.clone().empty().insertAfter($tr).append(this.$target); // ... move the clone in a new row instead
return;
}
// ... if not, if the clone neighbor is an empty cell, remove this empty cell (like if the clone content had been put in that cell)
var $next = $clone.next();
var $next = this.$target.next();
if ($next.length && $next.text().trim() === "") {
$next.remove();
return;
}
// ... if not, insert an empty col in each other row, at the index of the clone
var width = $clone.width();
var width = this.$target.width();
var $trs = this.$target.closest("table").children("thead, tbody, tfoot").addBack().children("tr").not(this.$target.parent());
_.each($trs.children(":nth-child(" + this.$target.index() + ")"), function (col) {
$(col).after($("<td/>", {style: "width: " + width + "px;"}));
});
}
},
on_remove: function () {
onRemove: function () {
this._super.apply(this, arguments);
// If we are removing a td or th element which was not alone on its row ...
@@ -141,11 +135,7 @@ $.summernote.eventHandler.modules.popover.update = function ($popover, oStyle, i
ajax.loadXML("/mass_mailing/static/src/xml/mass_mailing.xml", core.qweb);
snippets_editor.Class.include({
_get_snippet_url: function () {
var url = (typeof snippets_url !== "undefined" ? window["snippets_url"] : this._super.apply(this, arguments));
return url;
},
compute_snippet_templates: function (html) {
_computeSnippetTemplates: function (html) {
var self = this;
var ret = this._super.apply(this, arguments);
@@ -362,9 +352,9 @@ snippets_editor.Class.include({
$old_layout.remove();
if (first_choice) {
self.add_default_snippet_text_classes($new_wrapper_content);
self._registerDefaultTexts($new_wrapper_content);
}
self.show_blocks();
self._disableUndroppableSnippets();
}
},
});
@@ -381,12 +371,12 @@ odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
var editor_enable = $('body').hasClass('editor_enable');
value = value || "";
if(value !==_val) {
if (value !==_val) {
if (editor_enable) {
if (value !== fields_values[field_name]) {
rte.history.recordUndo($editable);
}
snippets_editor.instance.make_active(false);
core.bus.trigger('deactivate_snippet');
}
if (value.indexOf('on_change_model_and_list') === -1) {
@@ -400,7 +390,7 @@ odoo_top[callback+"_updown"] = function (value, fields_values, field_name) {
}
}
if (fields_values.mailing_model && web_editor.editor_bar) {
if (fields_values.mailing_model && editor_enable) {
if (value.indexOf('on_change_model_and_list') !== -1) {
odoo_top[callback+"_downup"](_val);
}
@@ -1,11 +1,10 @@
odoo.define('payment.website_payment', function (require) {
"use strict";
require('web.dom_ready');
var ajax = require('web.ajax');
$(document).ready(function () {
if(!$('.o_website_payment').length) {
if (!$('.o_website_payment').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_payment'");
}
@@ -23,4 +22,3 @@ $(document).ready(function () {
});
});
});
});
@@ -1,7 +1,6 @@
odoo.define('point_of_sale.editor', function (require) {
'use strict';
var Editor = require('web_editor.snippet.editor').Editor;
var s_options = require('web_editor.snippets.options');
var core = require('web.core');
@@ -34,7 +33,7 @@ odoo.define('point_of_sale.editor', function (require) {
this.$('.pos-adv').append($('<div class="pos_adv_onsave_remove text-center" style="color:#666666; padding:10px;">' + _t('Set your customized advertisement here') + '</span>'));
},
clean_for_save: function() {
cleanForSave: function () {
this.$('.pos_adv_onsave_remove').remove();
}
});
@@ -48,4 +47,4 @@ odoo.define('point_of_sale.editor', function (require) {
}
});
});
});
+16 -16
View File
@@ -19,7 +19,7 @@
<link rel="apple-touch-icon" sizes="120x120" href="/point_of_sale/static/src/img/touch-icon-iphone-retina.png"/>
<link rel="apple-touch-icon" sizes="152x152" href="/point_of_sale/static/src/img/touch-icon-ipad-retina.png"/>
<style> body { background: #222; } </style>
<style> body { background: #222; } </style>
<link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
@@ -98,26 +98,26 @@
<div data-js='pos_company_logo' data-selector=".pos-company_logo"/>
<div data-js='pos_palette' data-selector=".pos-customer_facing_display">
<li data-select_class="pos-palette_01"><a href="#" class="pos-palette_01"/></li>
<li data-select_class="pos-palette_02"><a href="#" class="pos-palette_02"/></li>
<li data-select_class="pos-palette_03"><a href="#" class="pos-palette_03"/></li>
<li data-select_class="pos-palette_04"><a href="#" class="pos-palette_04"/></li>
<li data-select_class="pos-palette_05"><a href="#" class="pos-palette_05"/></li>
<li data-select_class="pos-palette_06"><a href="#" class="pos-palette_06"/></li>
<li data-select_class="pos-palette_07"><a href="#" class="pos-palette_07"/></li>
<li data-select_class="pos-palette_08"><a href="#" class="pos-palette_08"/></li>
<li data-select_class="pos-palette_09"><a href="#" class="pos-palette_09"/></li>
<li data-select_class="pos-palette_10"><a href="#" class="pos-palette_10"/></li>
<li data-select_class="pos-palette_11"><a href="#" class="pos-palette_11"/></li>
<li data-select_class="pos-palette_12"><a href="#" class="pos-palette_12"/></li>
<li data-select_class="pos-palette_13"><a href="#" class="pos-palette_13"/></li>
<li data-select-class="pos-palette_01"><a href="#" class="pos-palette_01"/></li>
<li data-select-class="pos-palette_02"><a href="#" class="pos-palette_02"/></li>
<li data-select-class="pos-palette_03"><a href="#" class="pos-palette_03"/></li>
<li data-select-class="pos-palette_04"><a href="#" class="pos-palette_04"/></li>
<li data-select-class="pos-palette_05"><a href="#" class="pos-palette_05"/></li>
<li data-select-class="pos-palette_06"><a href="#" class="pos-palette_06"/></li>
<li data-select-class="pos-palette_07"><a href="#" class="pos-palette_07"/></li>
<li data-select-class="pos-palette_08"><a href="#" class="pos-palette_08"/></li>
<li data-select-class="pos-palette_09"><a href="#" class="pos-palette_09"/></li>
<li data-select-class="pos-palette_10"><a href="#" class="pos-palette_10"/></li>
<li data-select-class="pos-palette_11"><a href="#" class="pos-palette_11"/></li>
<li data-select-class="pos-palette_12"><a href="#" class="pos-palette_12"/></li>
<li data-select-class="pos-palette_13"><a href="#" class="pos-palette_13"/></li>
</div>
<div data-js='pos_background' data-selector=".pos-company_logo, .pos-adv">
<li data-choose_image="choose_image"><a href="#"><b>Set Custom Image...</b></a></li>
<li data-choose-image="true" data-no-preview="true"><a href="#"><b>Set Custom Image...</b></a></li>
<li class="hidden pos-use_default_logo" data-background=""><a href="#">Default company logo</a></li>
<li class="divider"/>
<li data-toggle_class="pos-hidden"><a href="#"><i class="fa fa-eye-slash"/>Hide</a></li>
<li data-toggle-class="pos-hidden"><a href="#"><i class="fa fa-eye-slash"/>Hide</a></li>
</div>
</div>
</template>
+6 -7
View File
@@ -1,10 +1,10 @@
odoo.define('portal.portal', function(require) {
odoo.define('portal.portal', function (require) {
'use strict';
$(document).ready(function () {
require('web.dom_ready');
if(!$('.o_portal').length) {
if (!$('.o_portal').length) {
return $.Deferred().reject("DOM doesn't contain '.o_portal'");
}
@@ -21,14 +21,14 @@ $(document).ready(function () {
}
if ($('.o_portal_search_panel').length) {
$('.o_portal_search_panel .search-submit').click(function() {
$('.o_portal_search_panel .search-submit').click(function () {
var search = $.deparam(window.location.search.substring(1));
search.search_in = $(".o_portal_search_panel li.active a").attr("href").replace("#","");
search.search = $(".o_portal_search_panel input[name='search']").val();
window.location.search = $.param(search);
});
$('.o_portal_search_panel .dropdown-menu').find('a').click(function(e) {
$('.o_portal_search_panel .dropdown-menu').find('a').click(function (e) {
e.preventDefault();
$(this).parents('.dropdown-menu').find('li').removeClass('active');
$(this).parent('li').addClass('active');
@@ -46,4 +46,3 @@ $(document).ready(function () {
});
}
});
});
+10 -11
View File
@@ -1,15 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<template id="assets_frontend" inherit_id="web.assets_frontend" name="Portal Assets">
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.ui.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.wrapwrap.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/bootstrap.extend.less"/>
</xpath>
<xpath expr="//link[last()]" position="after">
<script type="text/javascript" src="/portal/static/src/js/portal.js"></script>
<script type="text/javascript" src="/portal/static/src/js/portal_tour.js"></script>
</xpath>
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.ui.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/website.wrapwrap.less"/>
<link rel="stylesheet" type="text/less" href="/portal/static/src/less/bootstrap.extend.less"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/portal/static/src/js/portal.js"></script>
<script type="text/javascript" src="/portal/static/src/js/portal_tour.js"></script>
</xpath>
</template>
</odoo>
</odoo>
+1
View File
@@ -1,6 +1,7 @@
odoo.define('survey.survey', function (require) {
'use strict';
require('web.dom_ready');
var core = require('web.core');
var time = require('web.time');
var ajax = require('web.ajax');
+1 -1
View File
@@ -1,7 +1,7 @@
odoo.define('survey.result', function (require) {
'use strict';
var website = require('website.website');
require('web.dom_ready');
if(!$('.js_surveyresult').length) {
return $.Deferred().reject("DOM doesn't contain '.js_surveyresult'");
+10 -1
View File
@@ -1,3 +1,13 @@
odoo.define('web.dom_ready', function (require) {
'use strict';
var def = $.Deferred();
$(def.resolve.bind(def));
return def;
});
//==============================================================================
odoo.define('web.dom', function (require) {
"use strict";
@@ -245,5 +255,4 @@ return {
return $container;
},
};
});
@@ -1,7 +1,6 @@
odoo.define('report.editor', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var utils = require('report.utils');
var editor = require('web_editor.editor');
@@ -16,23 +15,18 @@ var trusted_origin = utils.build_origin(trusted_protocol, trusted_host);
if (window.self !== window.top) {
$(document.body).addClass('o_in_iframe'); // in order to apply css rules
// As `reload` is called after `save` and `cancel`, we nullify this function
// if the editor in order to be able to chain some deferred to `save` and
// `cancel`.
editor.reload = function () {
return $.when();
};
// And now we chain some deferred to `save` and `cancel` in order to inform
// the report's client action that the actions are done.
editor.Class.include({
save: function () {
return this._super.apply(this, arguments).then(function () {
// Force to not reload
return this._super(false).then(function () {
window.parent.postMessage('report.editor:save_ok', trusted_origin);
});
},
cancel: function () {
return this._super.apply(this, arguments).then(function () {
// Force to not reload
return this._super(false).then(function () {
window.parent.postMessage('report.editor:discard_ok', trusted_origin);
});
},
@@ -54,12 +48,12 @@ if (window.self !== window.top) {
return;
}
switch(message) {
switch (message) {
case 'report.editor:ask_save':
editor.editor_bar.save();
core.bus.trigger('editor_save_request');
break;
case 'report.editor:ask_discard':
editor.editor_bar.cancel();
core.bus.trigger('editor_discard_request');
break;
default:
}
@@ -91,16 +85,19 @@ if (window.self !== window.top) {
}
options.registry.many2one.include({
select_record: function (li) {
var self = this;
this._super(li);
if (this.$target.data('oe-field') === "partner_id") {
var $img = $('.header .row img:first');
var css = window.getComputedStyle($img[0]);
$img.css("max-height", css.height+'px');
$img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
setTimeout(function () { $img.removeClass('o_dirty'); },0);
_selectRecord: function ($li) {
this._super.apply(this, arguments);
if (this.$target.data('oe-field') !== 'partner_id') {
return;
}
var $img = $('.header .row img:first');
var css = window.getComputedStyle($img[0]);
$img.css('max-height', css.height+'px');
$img.attr('src', '/web/image/res.partner/' + this.ID + '/image');
_.defer(function () {
$img.removeClass('o_dirty');
});
}
});
@@ -14,7 +14,7 @@
body.o_in_iframe {
margin-top: 40px!important;
div.o_css_editor {
div#web_editor-top-edit {
> form {
display: none;
}
@@ -7,7 +7,6 @@ var config = require('web.config');
var core = require('web.core');
var session = require('web.session');
var field_registry = require('web.field_registry');
var transcoder = require('web_editor.transcoder');
var TranslatableFieldMixin = basic_fields.TranslatableFieldMixin;
@@ -107,8 +106,8 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
*/
_getValue: function () {
if (this.nodeOptions['style-inline']) {
transcoder.class_to_style(this.$content);
transcoder.font_to_img(this.$content);
transcoder.classToStyle(this.$content);
transcoder.fontToImg(this.$content);
}
return this.$content.html();
},
@@ -125,7 +124,7 @@ var FieldTextHtmlSimple = basic_fields.DebouncedField.extend(TranslatableFieldMi
// trigger a mouseup to refresh the editor toolbar
this.$content.trigger('mouseup');
if (this.nodeOptions['style-inline']) {
transcoder.style_to_class(this.$content);
transcoder.styleToClass(this.$content);
}
// reset the history (otherwise clicking on undo before editing the
// value will empty the editor)
@@ -352,7 +351,7 @@ var FieldTextHtml = AbstractField.extend({
return;
}
if (this.mode === "edit") {
if(window.odoo[this.callback+"_updown"]) {
if (window.odoo[this.callback+"_updown"]) {
// FIXME
// window.odoo[this.callback+"_updown"](value, this.view.get_fields_values(), this.name);
this.resize();
@@ -391,7 +390,7 @@ var FieldTextHtml = AbstractField.extend({
var layoutInfo = this.editor.rte.editable().data('layoutInfo');
$.summernote.pluginEvents.codeview(undefined, undefined, layoutInfo, false);
}
this.editor.buildingBlock.clean_for_save();
this.editor.snippetsMenu.cleanForSave();
this._setValue(this.$content.html());
return this._super.apply(this, arguments);
},
@@ -405,5 +404,4 @@ return {
FieldTextHtmlSimple: FieldTextHtmlSimple,
FieldTextHtml: FieldTextHtml,
};
});
+51 -22
View File
@@ -1,35 +1,64 @@
odoo.define('web_editor.base', function (require) {
"use strict";
'use strict';
var ajax = require('web.ajax');
var session = require('web.session');
var get_context = function (dict) {
var domReady = $.Deferred();
$(domReady.resolve.bind(domReady));
return {
/**
* If a widget needs to be instantiated on page loading, it needs to wait
* for appropriate resources to be loaded. This function returns a Deferred
* which is resolved when the dom is ready, the session is bound
* (translations loaded) and the XML is loaded. This should however not be
* necessary anymore as widgets should not be parentless and should then be
* instantiated (directly or not) by the page main component (webclient,
* website root, editor bar, ...). The DOM will be ready then, the main
* component is in charge of waiting for the session and the XML can be
* lazy loaded thanks to the @see Widget.xmlDependencies key.
*
* @returns {Deferred}
*/
ready: function () {
return $.when(domReady, session.is_bound, ajax.loadXML());
},
};
});
//==============================================================================
odoo.define('web_editor.context', function (require) {
'use strict';
function getContext(context) {
var html = document.documentElement;
return _.extend({
'lang': (html.getAttribute('lang') || '').replace('-', '_'),
}, dict);
lang: (html.getAttribute('lang') || 'en_US').replace('-', '_'),
}, context || {});
}
function getExtraContext(context) {
var html = document.documentElement;
return _.extend(getContext(), {
editable: !!(html.dataset.editable || $('[data-oe-model]').length), // temporary hack, this should be done in python
translatable: !!html.dataset.translatable,
edit_translations: !!html.dataset.edit_translations,
}, context || {});
}
return {
get: getContext,
getExtra: getExtraContext,
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
var data = {
'get_context': get_context,
};
var dom_ready = $.Deferred();
$(document).ready(function () {
dom_ready.resolve(data);
// fix for ie
if ($.fn.placeholder) $('input, textarea').placeholder();
});
data.dom_ready = dom_ready;
// todo: remove and load a bundle of translated templates
data.ready = function () {
return $.when(dom_ready, session.is_bound, ajax.loadXML());
};
//==============================================================================
return dom_ready;
odoo.define('web_editor.ready', function (require) {
'use strict';
var base = require('web_editor.base');
return base.ready();
});
@@ -0,0 +1,33 @@
odoo.define('web_editor.BodyManager', function (require) {
'use strict';
var mixins = require('web.mixins');
var session = require('web.session');
var rootWidget = require('web_editor.root_widget');
/**
* Element which is designed to be unique and that will be the top-most element
* in the widget hierarchy. So, all other widgets will be indirectly linked to
* this Class instance. Its main role will be to retrieve RPC demands from its
* children and handle them.
*/
var BodyManager = rootWidget.RootWidget.extend(mixins.ServiceProvider, {
/**
* @constructor
*/
init: function () {
mixins.ServiceProvider.init.call(this);
this._super.apply(this, arguments);
},
/**
* @override
*/
willStart: function () {
return $.when(
this._super.apply(this, arguments),
session.is_bound
);
},
});
return BodyManager;
});
+181 -101
View File
@@ -1,86 +1,80 @@
odoo.define('web_editor.editor', function (require) {
"use strict";
'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');
var rte = require('web_editor.rte');
var snippetsEditor = require('web_editor.snippet.editor');
var qweb = core.qweb;
var _t = core._t;
var editor = {};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
editor.dummy = function () {return true;}; // used for snippets, options...
editor.editable = !!($('html').data('editable') || $("[data-oe-model]").length); // temporary hack, this should be done in python
ajax.loadXML('/web_editor/static/src/xml/editor.xml', qweb);
$(document).on('click', '.note-editable', function (ev) {
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();
}
});
//////////////////////////////////////////////////////////////////////////////////////////////////////////
editor.reload = function () {
location.hash = "scrollTop=" + window.document.body.scrollTop;
if (location.search.indexOf("enable_editor") > -1) {
window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
} else {
window.location.reload(true);
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* ----- TOP EDITOR BAR FOR ADMIN ---- */
base.ready().then(function () {
if (editor.editable && location.search.indexOf("enable_editor") >= 0) {
editor.editor_bar = new editor.Class();
editor.editor_bar.prependTo(document.body);
}
});
editor.Class = Widget.extend({
var EditorMenuBar = Widget.extend({
template: 'web_editor.editorbar',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
'click button[data-action=save]': 'save',
'click button[data-action=cancel]': 'cancel',
'click button[data-action=save]': '_onSaveClick',
'click button[data-action=cancel]': '_onCancelClick',
},
custom_events: {
request_history_undo_record: '_onHistoryUndoRecordRequest',
request_save: '_onSaveRequest',
},
/**
* Initializes RTE and snippets menu.
*
* @constructor
*/
init: function (parent) {
var self = this;
var res = this._super.apply(this, arguments);
this.parent = parent;
this.rte = new rte.Class(this);
this.rte.on('rte:start', this, function () {
self.trigger('rte:start');
});
// Snippets edition
var $editable = this.rte.editable();
this.snippetsMenu = new snippetsEditor.Class(this, $editable);
return res;
},
/**
* @override
*/
start: function () {
var self = this;
var defs = [this._super.apply(this, arguments)];
core.bus.on('editor_save_request', this, this._onSaveRequest);
core.bus.on('editor_discard_request', this, this._onDiscardRequest);
$('.dropdown-toggle').dropdown();
this.display_placeholder();
$(document).on('keyup', function (event) {
if ((event.keyCode === 8 || event.keyCode === 46)) {
var $target = $(event.target).closest('.o_editable');
if (!$target.is(':has(*:not(p):not(br))') && !$target.text().match(/\S/)) {
$target.empty();
}
}
});
$(document).on('click', '.note-editable', function (ev) {
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();
}
});
this.rte.on('change', this, this.proxy('rte_changed'));
this.rte.start();
var flag = false;
@@ -92,58 +86,144 @@ editor.Class = Widget.extend({
}
};
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]")
.filter(".oe_not_editable")
.filter(".oe_no_empty");
// Snippets menu
defs.push(this.snippetsMenu.insertAfter(this.$el));
this.rte.editable().find('*').off('mousedown mouseup click');
this.on('rte:start', this, function () {
$area.attr("data-oe-placeholder", _t("Write Your Text Here"));
});
this.on("snippets:ready", this, function () {
if ($("body").hasClass("editor_has_snippets")) {
$area.attr("data-oe-placeholder", _t("Write Your Text or Drag a Block Here"));
}
});
$(document).on("keyup", function (event) {
if((event.keyCode === 8 || event.keyCode === 46)) {
var $target = $(event.target).closest(".o_editable");
if(!$target.is(":has(*:not(p):not(br))") && !$target.text().match(/\S/)) {
$target.empty();
}
}
});
},
rte_changed: function () {},
save: function () {
return this.rte.save().then(function () {
editor.reload();
return $.when.apply($, defs).then(function () {
self.trigger_up('edit_mode');
});
},
/**
* Saves an RTE content, which always corresponds to a view section (?).
* @override
*/
save_without_reload: function () {
return this.rte.save();
destroy: function () {
this._super.apply(this, arguments);
core.bus.off('editor_save_request', this, this._onSaveRequest);
core.bus.off('editor_discard_request', this, this._onDiscardRequest);
},
cancel: function () {
return new $.Deferred(function (d) {
if (!rte.history.getEditableHasUndo().length) {
return d.resolve();
}
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)
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Asks the user if he really wants to discard its changes (if there are
* some of them), then simply reload the page if he wants to.
*
* @param {boolean} [reload=true]
* true if the page has to be reloaded when the user answers yes
* (do nothing otherwise but add this to allow class extension)
* @returns {Deferred}
*/
cancel: function (reload) {
var self = this;
var def = $.Deferred();
if (!rte.history.getEditableHasUndo().length) {
def.resolve();
} else {
var confirm = Dialog.confirm(this, _t("If you discard the current edition, all unsaved changes will be lost. You can cancel to return to the edition mode."), {
confirm_callback: def.resolve.bind(def),
});
confirm.on("closed", d, d.reject);
}).then(function () {
window.onbeforeunload = null;
editor.reload();
confirm.on('closed', def, def.reject);
}
return def.then(function () {
if (reload !== false) {
window.onbeforeunload = null;
return self._reload();
}
});
},
/**
* Asks the snippets to clean themself, then saves the page, then reloads it
* if asked to.
*
* @param {boolean} [reload=true]
* true if the page has to be reloaded after the save
* @returns {Deferred}
*/
save: function (reload) {
var self = this;
this.snippetsMenu.cleanForSave();
return this.rte.save().then(function () {
if (reload !== false) {
return self._reload();
}
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Reloads the page in non-editable mode, with the right scrolling.
*
* @private
* @returns {Deferred} (never resolved, the page is reloading anyway)
*/
_reload: function () {
window.location.hash = 'scrollTop=' + window.document.body.scrollTop;
if (window.location.search.indexOf('enable_editor') >= 0) {
window.location.href = window.location.href.replace(/&?enable_editor(=[^&]*)?/g, '');
} else {
window.location.reload(true);
}
return $.Deferred();
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "Discard" button is clicked -> discards the changes.
*
* @private
*/
_onCancelClick: function () {
this.cancel();
},
/**
* Called when an element askes to record an history undo -> records it.
*
* @private
* @param {OdooEvent} ev
*/
_onHistoryUndoRecordRequest: function (ev) {
this.rte.historyRecordUndo(ev.data.$target, ev.data.event);
},
/**
* Called when the "Save" button is clicked -> saves the changes.
*
* @private
*/
_onSaveClick: function () {
this.save();
},
/**
* Called when a discard request is received -> discard the page content
* changes.
*
* @private
* @param {OdooEvent} ev
*/
_onDiscardRequest: function (ev) {
this.cancel(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
},
/**
* Called when a save request is received -> saves the page content.
*
* @private
* @param {OdooEvent} ev
*/
_onSaveRequest: function (ev) {
ev.stopPropagation();
this.save(ev.data.reload).then(ev.data.onSuccess, ev.data.onFailure);
},
});
return editor;
return {
Class: EditorMenuBar,
};
});
+369 -267
View File
@@ -1,27 +1,24 @@
odoo.define('web_editor.rte', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var concurrency = require('web.concurrency');
var core = require('web.core');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var summernote = require('web_editor.summernote');
var base = require('web_editor.base');
var weWidgets = require('web_editor.widget');
var _t = core._t;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Summernote Lib (neek change to make accessible: method and object) */
// Summernote Lib (neek change to make accessible: method and object)
var dom = summernote.core.dom;
var range = summernote.core.range;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Change History to have a global History for all summernote instances */
var History = function History ($editable) {
// Change History to have a global History for all summernote instances
var History = function History($editable) {
var aUndo = [];
var pos = 0;
var toSnap;
this.makeSnap = function (event, rng) {
rng = rng || range.create();
@@ -42,27 +39,27 @@ var History = function History ($editable) {
var $editable = $(oSnap.editable);
if (document.documentMode) {
$editable.removeAttr("contentEditable").removeProp("contentEditable");
$editable.removeAttr('contentEditable').removeProp('contentEditable');
}
$editable.html(oSnap.contents).scrollTop(oSnap.scrollTop);
$(".oe_overlay").remove();
$(".note-control-selection").hide();
$('.oe_overlay').remove();
$('.note-control-selection').hide();
$editable.trigger("content_changed");
$editable.trigger('content_changed');
try {
var r = oSnap.editable.innerHTML === "" ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
var r = oSnap.editable.innerHTML === '' ? range.create(oSnap.editable, 0) : range.createFromBookmark(oSnap.editable, oSnap.bookmark);
r.select();
} catch(e) {
} catch (e) {
console.error(e);
return;
}
$(document).trigger("click");
$(".o_editable *").filter(function () {
$(document).trigger('click');
$('.o_editable *').filter(function () {
var $el = $(this);
if($el.data('snippet-editor')) {
if ($el.data('snippet-editor')) {
$el.removeData();
}
});
@@ -74,13 +71,13 @@ var History = function History ($editable) {
return;
}
$editable.trigger("applySnap");
$editable.trigger('applySnap');
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent("click", true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, target);
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);
$editable.trigger("keyup");
$editable.trigger('keyup');
});
};
@@ -101,24 +98,25 @@ var History = function History ($editable) {
pos--;
}
this.applySnap(aUndo[Math.max(--pos,0)]);
while (pos && (aUndo[pos].event === "blur" || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
while (pos && (aUndo[pos].event === 'blur' || (aUndo[pos+1].editable === aUndo[pos].editable && aUndo[pos+1].contents === aUndo[pos].contents))) {
this.applySnap(aUndo[--pos]);
}
};
this.hasUndo = function () {
return (toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo")) ||
return (toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo')) ||
!!_.find(aUndo.slice(0, pos+1), function (undo) {
return undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo";
return undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo';
});
};
this.getEditableHasUndo = function () {
var editable = [];
if ((toSnap && (toSnap.event !== "blur" && toSnap.event !== "activate" && toSnap.event !== "undo"))) {
if ((toSnap && (toSnap.event !== 'blur' && toSnap.event !== 'activate' && toSnap.event !== 'undo'))) {
editable.push(toSnap.editable);
}
_.each(aUndo.slice(0, pos+1), function (undo) {
if (undo.event !== "blur" && undo.event !== "activate" && undo.event !== "undo") {
if (undo.event !== 'blur' && undo.event !== 'activate' && undo.event !== 'undo') {
editable.push(undo.editable);
}
});
@@ -128,31 +126,31 @@ var History = function History ($editable) {
this.redo = function () {
if (!aUndo[pos+1]) { return; }
this.applySnap(aUndo[++pos]);
while (aUndo[pos+1] && aUndo[pos].event === "active") {
while (aUndo[pos+1] && aUndo[pos].event === 'active') {
this.applySnap(aUndo[pos++]);
}
};
this.hasRedo = function () {
return aUndo.length > pos+1;
};
var toSnap;
this.recordUndo = function ($editable, event, internal_history) {
var self = this;
if (!$editable) {
var rng = range.create();
if(!rng) return;
$editable = $(rng.sc).closest(".o_editable");
if (!rng) return;
$editable = $(rng.sc).closest('.o_editable');
}
if (aUndo[pos] && (event === "applySnap" || event === "activate")) {
if (aUndo[pos] && (event === 'applySnap' || event === 'activate')) {
return;
}
if (!internal_history) {
if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === "activate") { // don't trigger change for all keypress
if (!event || !toSnap || !aUndo[pos-1] || toSnap.event === 'activate') { // don't trigger change for all keypress
setTimeout(function () {
$editable.trigger("content_changed");
$editable.trigger('content_changed');
},0);
}
}
@@ -199,11 +197,9 @@ var History = function History ($editable) {
};
var history = new History();
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// add focusIn to jQuery to allow to move caret into a div of a contentEditable area
$.extend($.expr[':'],{
o_editable: function (node,i,m) {
// jQuery extensions
$.extend($.expr[':'], {
o_editable: function (node, i, m) {
while (node) {
if (node.className && _.isString(node.className)) {
if (node.className.indexOf('o_not_editable')!==-1 ) {
@@ -216,9 +212,8 @@ $.extend($.expr[':'],{
node = node.parentNode;
}
return false;
}
},
});
$.fn.extend({
focusIn: function () {
if (this.length) {
@@ -239,18 +234,16 @@ $.fn.extend({
range.create(dom.firstChild(this[0]), 0, next, next.textContent.length).select();
}
return this;
}
},
});
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* ----- RICH TEXT EDITOR ---- */
var RTE = Widget.extend({
init: function (EditorBar, config) {
// RTE
var RTEWidget = Widget.extend({
/**
* @constructor
*/
init: function (parent, getConfig) {
var self = this;
this.EditorBar = EditorBar;
data.rte = this;
this._super.apply(this, arguments);
this.init_bootstrap_carousel = $.fn.carousel;
@@ -262,32 +255,12 @@ var RTE = Widget.extend({
return res;
};
if (config) {
this.config = config;
}
this._getConfig = getConfig || this._getDefaultConfig;
weWidgets.computeFonts();
},
/**
* Add a record undo to history
* @param {DOM} target where the dom is changed is editable zone
*/
historyRecordUndo: function ($target, event, internal_history) {
var rng = range.create();
var $editable = $(rng && rng.sc).closest(".o_editable");
if (!rng || !$editable.length) {
$editable = $($target).closest(".o_editable");
rng = range.create($target.closest("*")[0],0);
} else {
rng = $editable.data('range') || rng;
}
rng.select();
history.recordUndo($editable, event, internal_history);
},
/**
* Makes the page editable
*
* @param {Boolean} [restart=false] in case the edition was already set
* up once and is being re-enabled.
* @returns {$.Deferred} deferred indicating when the RTE is ready
* @override
*/
start: function () {
var self = this;
@@ -296,11 +269,14 @@ var RTE = Widget.extend({
$.fn.carousel = this.edit_bootstrap_carousel;
$(document).on("keydown.rte", this, this.onKeydown.bind(this));
$(document).on("mousedown.rte activate.rte", this, this.onMousedown.bind(this));
$(document).on("mouseup.rte", this, this.onMouseup.bind(this));
core.bus.on('alt_dialog_demand', this, this._onAltDialogDemand);
core.bus.on('link_dialog_demand', this, this._onLinkDialogDemand);
core.bus.on('media_dialog_demand', this, this._onMediaDialogDemand);
$('.o_not_editable').attr("contentEditable", false);
$(document).on('mousedown.rte activate.rte', this, this._onMousedown.bind(this));
$(document).on('mouseup.rte', this, this._onMouseup.bind(this));
$('.o_not_editable').attr('contentEditable', false);
var $editable = this.editable();
@@ -310,28 +286,27 @@ var RTE = Widget.extend({
var $node = $(this);
// add class to display inline-block for empty t-field
if (window.getComputedStyle(this).display === "inline" && $node.data('oe-type') !== "image") {
if (window.getComputedStyle(this).display === 'inline' && $node.data('oe-type') !== 'image') {
$node.addClass('o_is_inline_editable');
}
$node.data('initInnerHTML', $node.html());
});
// start element observation
$(document).on('content_changed', '.o_editable', function (event) {
self.trigger('change', this);
self.trigger_up('rte_change', {target: event.target});
$(this).addClass('o_dirty');
});
$('#wrapwrap, .o_editable').on('click.rte', '*', this, this.onClick.bind(this));
$('#wrapwrap, .o_editable').on('click.rte', '*', this, this._onClick.bind(this));
$('body').addClass("editor_enable");
$('body').addClass('editor_enable');
$(document)
.tooltip({
selector: '[data-oe-readonly]',
container: 'body',
trigger: 'hover',
delay: { "show": 1000, "hide": 100 },
delay: { 'show': 1000, 'hide': 100 },
placement: 'bottom',
title: _t("Readonly field")
})
@@ -341,113 +316,25 @@ var RTE = Widget.extend({
$(document).trigger('mousedown');
this.trigger('rte:start');
return this._super.apply(this, arguments);
},
/**
* @override
*/
destroy: function () {
this.cancel();
this._super.apply(this, arguments);
},
save: function (context) {
var self = this;
this.__saved = {}; // list of allready saved views and data
var defs = $('.o_dirty')
.removeAttr('contentEditable')
.removeClass('o_dirty oe_carlos_danger o_is_inline_editable')
.map(function () {
var $el = $(this);
$el.find('[class]').filter(function () {
if (!this.className.match(/\S/)) {
this.removeAttribute("class");
}
});
// TODO: Add a queue with concurrency limit in webclient
// https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
return self.saving_mutex.exec(function () {
return self.saveElement($el, context)
.then(undefined, function (thing, response) {
// because ckeditor regenerates all the dom,
// we can't just setup the popover here as
// everything will be destroyed by the DOM
// regeneration. Add markings instead, and
// returns a new rejection with all relevant
// info
var id = _.uniqueId('carlos_danger_');
$el.addClass('o_dirty oe_carlos_danger ' + id);
return $.Deferred().reject({
id: id,
error: response.data,
});
});
});
}).get();
return $.when.apply(null, defs).then(function () {
window.onbeforeunload = null;
}, function (failed) {
// If there were errors, re-enable edition
self.stop();
self.start();
// jquery's deferred being a pain in the ass
if (!_.isArray(failed)) { failed = [failed]; }
_(failed).each(function (failure) {
var html = failure.error.exception_type === "except_osv";
if (html) {
var msg = $("<div/>").text(failure.error.message).html();
var data = msg.substring(3,msg.length-2).split(/', u'/);
failure.error.message = '<b>' + data[0] + '</b>' + data[1];
}
$('.o_editable.' + failure.id)
.removeClass(failure.id)
.popover({
html: html,
trigger: 'hover',
content: failure.error.message,
placement: 'auto top',
})
// Force-show popovers so users will notice them.
.popover('show');
});
});
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Get HTML cloned element with text nodes escaped for XML storage
* Stops the RTE.
*/
getEscapedElement: function ($el) {
var escaped_el = $el.clone();
var to_escape = escaped_el.find('*').addBack();
to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
to_escape.contents().each(function () {
if(this.nodeType === 3) {
this.nodeValue = $('<div />').text(this.nodeValue).html();
}
});
return escaped_el;
},
saveElement: function ($el, context) {
// remove multi edition
if ($el.data('oe-model')) {
var key = $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression');
if (this.__saved[key]) return $.when();
this.__saved[key] = true;
}
var markup = this.getEscapedElement($el).prop('outerHTML');
return ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'ir.ui.view',
method: 'save',
args: [
$el.data('oe-id'),
markup,
$el.data('oe-xpath') || null,
_.omit(context || base.get_context(), 'lang')
],
});
},
stop: function () {
cancel: function () {
if (this.$last) {
this.$last.destroy();
this.$last = null;
@@ -455,47 +342,299 @@ var RTE = Widget.extend({
$.fn.carousel = this.init_bootstrap_carousel;
$(document).off(".rte");
core.bus.off('alt_dialog_demand', this, this._onAltDialogDemand);
core.bus.off('link_dialog_demand', this, this._onLinkDialogDemand);
core.bus.off('media_dialog_demand', this, this._onMediaDialogDemand);
$(document).off('.rte');
$('#wrapwrap, .o_editable').off('.rte');
$('.o_not_editable').removeAttr("contentEditable");
$('.o_not_editable').removeAttr('contentEditable');
$(document).off('content_changed').removeClass('o_is_inline_editable').removeData('rte');
$(document).tooltip('destroy');
$('body').removeClass("editor_enable");
$('body').removeClass('editor_enable');
this.trigger('rte:stop');
},
cancel: function () {
$('.o_editable').each(function () {
var $node = $(this);
$node.data('initInnerHTML', $node.html());
});
this.stop();
/**
* Returns the editable areas on the page.
*
* @returns {jQuery}
*/
editable: function () {
return $('#wrapwrap [data-oe-model]')
.not('.o_not_editable')
.filter(function () {
return !$(this).closest('.o_not_editable').length;
})
.not('link, script')
.not('[data-oe-readonly]')
.not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
.not('.oe_snippet_editor')
.add('.o_editable');
},
onClick: function (e) {
e.preventDefault();
/**
* Records the current state of the given $target to be able to undo future
* changes.
*
* @see History.recordUndo
* @param {jQuery} $target
* @param {string} event
* @param {boolean} internal_history
*/
historyRecordUndo: function ($target, event, internal_history) {
var rng = range.create();
var $editable = $(rng && rng.sc).closest('.o_editable');
if (!rng || !$editable.length) {
$editable = $($target).closest('.o_editable');
rng = range.create($target.closest('*')[0],0);
} else {
rng = $editable.data('range') || rng;
}
rng.select();
history.recordUndo($editable, event, internal_history);
},
/**
* Searches all the dirty element on the page and saves them one by one. If
* one cannot be saved, this notifies it to the user and restarts rte
* edition.
*
* @param {Object} [context] - the context to use for saving rpc, default to
* the editor context found on the page
* @return {Deferred} rejected if the save cannot be done
*/
save: function (context) {
var self = this;
// handler for cancel editor
onKeydown: function (event) {
if (event.keyCode === 27 && !$('.modal-content:visible').length) {
_.defer(function () {
$('#editor-top-navbar [data-action="cancel"]').click();
var $modal = $('.modal-content > .modal-body').parents(".modal:first");
$modal.off('keyup.dismiss.bs.modal');
_.delay(function () {
$modal.on('keyup.dismiss.bs.modal', function () {
$(this).modal('hide');
});
}, 500);
var $dirty = $('.o_dirty');
$dirty
.removeAttr('contentEditable')
.removeClass('o_dirty oe_carlos_danger o_is_inline_editable');
var defs = _.map($dirty, function (el) {
var $el = $(el);
$el.find('[class]').filter(function () {
if (!this.className.match(/\S/)) {
this.removeAttribute('class');
}
});
// TODO: Add a queue with concurrency limit in webclient
// https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
return self.saving_mutex.exec(function () {
return self._saveElement($el, context || weContext.get())
.then(function () {
$el.removeClass('o_dirty');
}, function (response) {
// because ckeditor regenerates all the dom, we can't just
// setup the popover here as everything will be destroyed by
// the DOM regeneration. Add markings instead, and returns a
// new rejection with all relevant info
var id = _.uniqueId('carlos_danger_');
$el.addClass('o_dirty oe_carlos_danger ' + id);
var html = (response.data.exception_type === 'except_osv');
if (html) {
var msg = $('<div/>', {text: response.data.message}).html();
var data = msg.substring(3, msg.length -2).split(/', u'/);
response.data.message = '<b>' + data[0] + '</b>' + data[1];
}
$('.o_editable.' + id)
.removeClass(id)
.popover({
html: html,
trigger: 'hover',
content: response.data.message,
placement: 'auto top',
})
.popover('show');
});
});
});
return $.when.apply($, defs).then(function () {
window.onbeforeunload = null;
}, function (failed) {
// If there were errors, re-enable edition
self.cancel();
self.start();
});
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* When the users clicks on an editable element, this function allows to add
* external behaviors.
*
* @private
* @param {jQuery} $editable
*/
_enableEditableArea: function ($editable) {
if ($editable.data('oe-type') === "monetary") {
$editable.attr('contenteditable', false);
$editable.find('.oe_currency_value').attr('contenteditable', true);
}
if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
$editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
}
},
/**
* When an element enters edition, summernote is initialized on it. This
* function returns the default configuration for the summernote instance.
*
* @see _getConfig
* @private
* @param {jQuery} $editable
* @returns {Object}
*/
_getDefaultConfig: function ($editable) {
return {
'airMode' : true,
'focus': false,
'airPopover': [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture']],
['history', ['undo', 'redo']],
],
'styleWithSpan': false,
'inlinemedia' : ['p'],
'lang': 'odoo',
'onChange': function (html, $editable) {
$editable.trigger('content_changed');
}
};
},
/**
* Gets jQuery cloned element with internal text nodes escaped for XML
* storage.
*
* @private
* @param {jQuery} $el
* @return {jQuery}
*/
_getEscapedElement: function ($el) {
var escaped_el = $el.clone();
var to_escape = escaped_el.find('*').addBack();
to_escape = to_escape.not(to_escape.filter('object,iframe,script,style,[data-oe-model][data-oe-model!="ir.ui.view"]').find('*').addBack());
to_escape.contents().each(function () {
if (this.nodeType === 3) {
this.nodeValue = $('<div />').text(this.nodeValue).html();
}
});
return escaped_el;
},
/**
* Saves one (dirty) element of the page.
*
* @private
* @param {jQuery} $el - the element to save
* @param {Object} context - the context to use for the saving rpc
* @param {boolean} [withLang=false]
* false if the lang must be omitted in the context (saving "master"
* page element)
*/
_saveElement: function ($el, context, withLang) {
return this._rpc({
model: 'ir.ui.view',
method: 'save',
args: [
$el.data('oe-id'),
this._getEscapedElement($el).prop('outerHTML'),
$el.data('oe-xpath') || null,
withLang ? context : _.omit(context, 'lang')
],
});
},
// activate editor
onMousedown: function (event) {
var $target = $(event.target);
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when any editable element is clicked -> Prevents default browser
* action for the element.
*
* @private
* @param {Event} e
*/
_onClick: function (e) {
e.preventDefault();
},
/**
* Called when a demand to open a alt dialog is received on the bus.
*
* @private
* @param {Object} data
*/
_onAltDialogDemand: function (data) {
var altDialog = new weWidgets.alt(this,
data.options || {},
data.$editable,
data.media
);
if (data.onSave) {
altDialog.on('save', this, data.onSave);
}
if (data.onCancel) {
altDialog.on('cancel', this, data.onCancel);
}
altDialog.open();
},
/**
* Called when a demand to open a link dialog is received on the bus.
*
* @private
* @param {Object} data
*/
_onLinkDialogDemand: function (data) {
var linkDialog = new weWidgets.LinkDialog(this,
data.options || {},
data.$editable,
data.linkInfo
);
if (data.onSave) {
linkDialog.on('save', this, data.onSave);
}
if (data.onCancel) {
linkDialog.on('cancel', this, data.onCancel);
}
linkDialog.open();
},
/**
* Called when a demand to open a media dialog is received on the bus.
*
* @private
* @param {Object} data
*/
_onMediaDialogDemand: function (data) {
var mediaDialog = new weWidgets.MediaDialog(this,
data.options || {},
data.$editable,
data.media
);
if (data.onSave) {
mediaDialog.on('save', this, data.onSave);
}
if (data.onCancel) {
mediaDialog.on('cancel', this, data.onCancel);
}
mediaDialog.open();
},
/**
* Called when the mouse is pressed on the document -> activate element
* edition.
*
* @private
* @param {Event} ev
*/
_onMousedown: function (ev) {
var $target = $(ev.target);
var $editable = $target.closest('.o_editable');
if (!$editable.length) {
@@ -534,8 +673,8 @@ var RTE = Widget.extend({
this.$last = null;
}
if ($editable.length && (!this.$last || this.$last[0] !== $editable[0]) &&
($target.closest('[contenteditable]').attr('contenteditable') || "").toLowerCase() !== 'false') {
$editable.summernote(this.config($editable));
($target.closest('[contenteditable]').attr('contenteditable') || '').toLowerCase() !== 'false') {
$editable.summernote(this._getConfig($editable));
$editable.data('NoteHistory', history);
this.$last = $editable;
@@ -545,7 +684,7 @@ var RTE = Widget.extend({
document.execCommand('enableObjectResizing', false, false);
document.execCommand('enableInlineTableEditing', false, false);
document.execCommand('2D-position', false, false);
} catch (e) {}
} catch (e) { /* */ }
document.body.addEventListener('resizestart', function (evt) {evt.preventDefault(); return false;});
document.body.addEventListener('movestart', function (evt) {evt.preventDefault(); return false;});
document.body.addEventListener('dragstart', function (evt) {evt.preventDefault(); return false;});
@@ -558,15 +697,17 @@ var RTE = Widget.extend({
$target.trigger('mousedown'); // for activate selection on picture
}
this.onEnableEditableArea($editable);
this._enableEditableArea($editable);
}
},
onEnableEditableArea: function ($editable) {
},
onMouseup: function (event) {
var $target = $(event.target);
/**
* Called when the mouse is unpressed on the document.
*
* @private
* @param {Event} ev
*/
_onMouseup: function (ev) {
var $target = $(ev.target);
var $editable = $target.closest('.o_editable');
if (!$editable.length) {
@@ -582,56 +723,17 @@ var RTE = Widget.extend({
// whitespace characters (so Tripleclicking then typing text will remove
// the whole paragraph instead of its content).
// http://stackoverflow.com/questions/38467334/why-does-google-chrome-always-add-space-after-selected-text
if ($.browser.chrome === true && event.originalEvent.detail === 3) {
if ($.browser.chrome === true && ev.originalEvent.detail === 3) {
var currentSelection = range.create();
if (currentSelection.sc.parentNode === currentSelection.ec) {
range.create(currentSelection.sc, currentSelection.so, currentSelection.sc, currentSelection.sc.length).select();
}
}
},
editable: function () {
return $('#wrapwrap [data-oe-model]')
.not('.o_not_editable')
.filter(function () {
return !$(this).closest('.o_not_editable').length;
})
.not('link, script')
.not('[data-oe-readonly]')
.not('img[data-oe-field="arch"], br[data-oe-field="arch"], input[data-oe-field="arch"]')
.not('.oe_snippet_editor')
.add('.o_editable');
},
config: function ($editable) {
return {
'airMode' : true,
'focus': false,
'airPopover': [
['style', ['style']],
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['para', ['ul', 'ol', 'paragraph']],
['table', ['table']],
['insert', ['link', 'picture']],
['history', ['undo', 'redo']],
],
'styleWithSpan': false,
'inlinemedia' : ['p'],
'lang': "odoo",
'onChange': function (html, $editable) {
$editable.trigger("content_changed");
}
};
},
});
var data = {
return {
Class: RTEWidget,
history: history,
Class: RTE
};
return data;
});
@@ -3,39 +3,32 @@ odoo.define('web_editor.rte.summernote', function (require) {
var core = require('web.core');
var ajax = require('web.ajax');
var base = require('web_editor.base');
var weContext = require('web_editor.context');
var widgets = require('web_editor.widget');
var rte = require('web_editor.rte');
var QWeb = core.qweb;
var _t = core._t;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
ajax.jsonRpc('/web/dataset/call', 'call', {
'model': 'ir.ui.view',
'method': 'read_template',
'args': ['web_editor.colorpicker', base.get_context()]
'args': ['web_editor.colorpicker', weContext.get()]
}).done(function (data) {
QWeb.add_template(data);
});
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Summernote Lib (neek change to make accessible: method and object) */
// Summernote Lib (neek change to make accessible: method and object)
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var eventHandler = $.summernote.eventHandler;
var renderer = $.summernote.renderer;
// var options = $.summernote.options;
var tplButton = renderer.getTemplate().button;
var tplIconButton = renderer.getTemplate().iconButton;
var tplDropdown = renderer.getTemplate().dropdown;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* update and change the popovers content, and add history button */
// Update and change the popovers content, and add history button
var fn_createPalette = renderer.createPalette;
renderer.createPalette = function ($container, options) {
fn_createPalette.call(this, $container, options);
@@ -322,10 +315,8 @@ eventHandler.modules.handle.update = function ($handle, oStyle, isAirMode) {
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* hack for image and link editor */
function getImgTarget ($editable) {
// Hack for image and link editor
function getImgTarget($editable) {
var $handle = $editable ? dom.makeLayoutInfo($editable).handle() : undefined;
return $(".note-control-selection", $handle).data('target');
}
@@ -375,18 +366,18 @@ eventHandler.modules.linkDialog.showLinkDialog = function ($editable, $dialog, l
$editable.data('range').select();
$editable.data('NoteHistory').recordUndo();
var editor = new widgets.LinkDialog(null, {}, $editable, linkInfo).open();
var def = new $.Deferred();
editor.on("save", this, function (linkInfo) {
linkInfo.range.select();
$editable.data('range', linkInfo.range);
def.resolve(linkInfo);
$editable.trigger('keyup');
$('.note-popover .note-link-popover').show();
});
editor.on("cancel", this, function () {
def.reject();
core.bus.trigger('link_dialog_demand', {
$editable: $editable,
linkInfo: linkInfo,
onSave: function (linkInfo) {
linkInfo.range.select();
$editable.data('range', linkInfo.range);
def.resolve(linkInfo);
$editable.trigger('keyup');
$('.note-popover .note-link-popover').show();
},
onCancel: def.reject.bind(def),
});
return def;
};
@@ -395,18 +386,22 @@ eventHandler.modules.imageDialog.showImageDialog = function ($editable) {
if (r.sc.tagName && r.sc.childNodes.length) {
r.sc = r.sc.childNodes[r.so];
}
new widgets.MediaDialog(null, {}, $editable, dom.isImg(r.sc) ? r.sc : null).open();
core.bus.trigger('media_dialog_demand', {
$editable: $editable,
media: dom.isImg(r.sc) ? r.sc : null,
});
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(null, {}, $editable, media).open();
core.bus.trigger('alt_dialog_demand', {
$editable: $editable,
media: $selection.data('target'),
});
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
// Utils
var fn_is_void = dom.isVoid || function () {};
dom.isVoid = function (node) {
return fn_is_void(node) || dom.isImgFont(node) || (node && node.className && node.className.match(/(^|\s)media_iframe_video(\s|$)/i));
@@ -440,13 +435,11 @@ dom.isFont = function (node) {
return fn_is_font(node) || dom.isImgFont(node);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
var fn_visible = $.summernote.pluginEvents.visible;
$.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
var res = fn_visible.apply(this, arguments);
var rng = range.create();
if(!rng) return res;
if (!rng) return res;
var $node = $(dom.node(rng.sc));
if (($node.is('[data-oe-type="html"]') || $node.is('[data-oe-field="arch"]')) &&
$node.hasClass("o_editable") &&
@@ -582,11 +575,12 @@ $.summernote.pluginEvents.codeview = function (event, editor, layoutInfo, enable
}
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* fix ie and re-range to don't break snippet*/
// Fix ie and re-range to don't break snippet
var last_div;
var last_div_change;
var last_editable;
var initial_data = {};
function reRangeSelectKey (event) {
function reRangeSelectKey(event) {
initial_data.range = null;
if (event.shiftKey && event.keyCode >= 37 && event.keyCode <= 40 && !$(event.target).is("input, textarea, select")) {
var r = range.create();
@@ -598,7 +592,7 @@ function reRangeSelectKey (event) {
}
}
}
function reRangeSelect (event, dx, dy) {
function reRangeSelect(event, dx, dy) {
var r = range.create();
if (!r || r.isCollapsed()) return;
@@ -615,7 +609,7 @@ function reRangeSelect (event, dx, dy) {
$(data.sc).closest('.o_editable').data('range', r);
return r;
}
function summernote_mouseup (event) {
function summernote_mouseup(event) {
if ($(event.target).closest("#web_editor-top-navbar, .note-popover").length) {
return;
}
@@ -638,7 +632,7 @@ function summernote_mouseup (event) {
}
}
var remember_selection;
function summernote_mousedown (event) {
function summernote_mousedown(event) {
rte.history.splitNext();
var $editable = $(event.target).closest(".o_editable, .note-editor");
@@ -663,19 +657,19 @@ function summernote_mousedown (event) {
var r_editable = editables.has((r||{}).sc);
if (!r_editable.closest('.note-editor').is($editable) && !r_editable.filter('.o_editable').is(editables)) {
var saved_editable = editables.has((remember_selection||{}).sc);
if($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
if ($editable.length && !saved_editable.closest('.o_editable, .note-editor').is($editable)) {
remember_selection = range.create(dom.firstChild($editable[0]), 0);
} else if(!saved_editable.length) {
} else if (!saved_editable.length) {
remember_selection = undefined;
}
if(remember_selection) {
if (remember_selection) {
try {
remember_selection.select();
} catch (e) {
console.warn(e);
}
}
} else if(r_editable.length) {
} else if (r_editable.length) {
remember_selection = r;
}
@@ -691,18 +685,15 @@ function summernote_mousedown (event) {
}
}
var last_div;
var last_div_change;
var last_editable;
function summernote_ie_fix (event, pred) {
function summernote_ie_fix(event, pred) {
var editable;
var div;
var node = event.target;
while(node.parentNode) {
while (node.parentNode) {
if (!div && pred(node)) {
div = node;
}
if(last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
if (last_div !== node && (node.getAttribute('contentEditable')==='false' || node.className && (node.className.indexOf('o_not_editable') !== -1))) {
break;
}
if (node.className && node.className.indexOf('o_editable') !== -1) {
@@ -772,13 +763,13 @@ eventHandler.attach = function (oLayoutInfo, options) {
eventHandler.modules.linkDialog.show(oLayoutInfo);
});
if(oLayoutInfo.editor().is('[data-oe-model][data-oe-type="image"]')) {
if (oLayoutInfo.editor().is('[data-oe-model][data-oe-type="image"]')) {
oLayoutInfo.editor().on('click', 'img', function (event) {
$(event.target).trigger("dblclick");
});
}
oLayoutInfo.editable().on('mousedown', function (e) {
if(dom.isImg(e.target)) {
if (dom.isImg(e.target)) {
range.createFromNode(e.target).select();
}
});
@@ -875,9 +866,7 @@ eventHandler.detach = function (oLayoutInfo, options) {
$(document).off("keyup", reRangeSelectKey);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Translation for odoo */
// Translation for odoo
$.summernote.lang.odoo = {
font: {
bold: _t('Bold'),
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -6,25 +6,21 @@ require('summernote/summernote'); // wait that summernote is loaded
var _t = core._t;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Summernote Lib (neek hack to make accessible: method and object) */
var agent = $.summernote.core.agent;
// Summernote Lib (neek hack to make accessible: method and object)
// var agent = $.summernote.core.agent;
var dom = $.summernote.core.dom;
var range = $.summernote.core.range;
var list = $.summernote.core.list;
var key = $.summernote.core.key;
var eventHandler = $.summernote.eventHandler;
var editor = eventHandler.modules.editor;
// var editor = eventHandler.modules.editor;
var renderer = $.summernote.renderer;
var options = $.summernote.options;
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* Add method to Summernote*/
// Add methods to summernote
dom.hasContentAfter = function (node) {
var next;
if(dom.isEditable(node)) return;
if (dom.isEditable(node)) return;
while (node.nextSibling) {
next = node.nextSibling;
if (next.tagName || dom.isVisibleText(next) || dom.isBR(next)) return next;
@@ -33,7 +29,7 @@ dom.hasContentAfter = function (node) {
};
dom.hasContentBefore = function (node) {
var prev;
if(dom.isEditable(node)) return;
if (dom.isEditable(node)) return;
while (node.previousSibling) {
prev = node.previousSibling;
if (prev.tagName || dom.isVisibleText(prev) || dom.isBR(prev)) return prev;
@@ -97,12 +93,12 @@ dom.isEqual = function (prev, cur) {
}
var att, att2;
loop_prev:
for(var a in prev.attributes) {
for (var a in prev.attributes) {
att = prev.attributes[a];
for(var b in cur.attributes) {
for (var b in cur.attributes) {
att2 = cur.attributes[b];
if (att.name === att2.name) {
if (strip(att.value) != strip(att2.value)) return false;
if (strip(att.value) !== strip(att2.value)) return false;
continue loop_prev;
}
}
@@ -110,9 +106,6 @@ dom.isEqual = function (prev, cur) {
}
return true;
};
/**
* Checks that the node only has a @style, not e.g. @class or whatever
*/
dom.hasOnlyStyle = function (node) {
for (var i = 0; i < node.attributes.length; i++) {
var attr = node.attributes[i];
@@ -199,7 +192,7 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
if (!begin) {
begin = node;
while(begin.firstChild) {begin = begin.firstChild;}
while (begin.firstChild) {begin = begin.firstChild;}
so = 0;
} else if (begin.tagName && begin.childNodes[so]) {
begin = begin.childNodes[so];
@@ -207,10 +200,8 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
}
if (!end) {
end = node;
while(end.lastChild) {end = end.lastChild;}
while (end.lastChild) {end = end.lastChild;}
eo = end.textContent.length-1;
} else if (end.tagName) {
} else if (end.tagName && end.childNodes[so]) {
end = end.childNodes[so];
so = 0;
@@ -225,7 +216,7 @@ dom.merge = function (node, begin, so, end, eo, mergeFilter, all) {
eo = 0;
}
function __merge (node) {
function __merge(node) {
var merged = false;
var prev;
for (var k=0; k<node.childNodes.length; k++) {
@@ -330,7 +321,8 @@ dom.autoMerge = function (target, previous) {
while (node) {
nodes.push(node);
if (temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node))) {
temp = (previous ? dom.hasContentBefore(node) : dom.hasContentAfter(node));
if (temp) {
if (!dom.isText(node) && !dom.isMergable(node) && temp.tagName !== node.tagName) {
nodes = [];
}
@@ -369,7 +361,7 @@ dom.removeSpace = function (node, begin, so, end, eo) {
};
}
(function __remove_space (node) {
(function __remove_space(node) {
if (!node) return;
var t_begin, t_end;
for (var k=0; k<node.childNodes.length; k++) {
@@ -389,14 +381,14 @@ dom.removeSpace = function (node, begin, so, end, eo) {
removed = true;
if (cur === begin) {
t_begin = dom.hasContentBefore(dom.ancestorHavePreviousSibling(cur));
if(t_begin) {
if (t_begin) {
so = 0;
begin = dom.lastChild(t_begin);
}
}
if (cur === end) {
t_end = dom.hasContentAfter(dom.ancestorHaveNextSibling(cur));
if(t_end) {
if (t_end) {
eo = 1;
end = dom.firstChild(t_end);
if (dom.isText(end)) {
@@ -414,19 +406,21 @@ dom.removeSpace = function (node, begin, so, end, eo) {
// convert HTML space
if (dom.isText(cur)) {
var text;
var temp;
var _temp;
var exp1 = /[\t\n\r ]+/g;
var exp2 = /(?!([ ]|\u00A0)|^)\u00A0(?!([ ]|\u00A0)|$)/g;
if (cur === begin) {
var temp = cur.textContent.substr(0, so);
var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
temp = cur.textContent.substr(0, so);
_temp = temp.replace(exp1, ' ').replace(exp2, ' ');
so -= temp.length - _temp.length;
}
if (cur === end) {
var temp = cur.textContent.substr(0, eo);
var _temp = temp.replace(exp1, ' ').replace(exp2, ' ');
temp = cur.textContent.substr(0, eo);
_temp = temp.replace(exp1, ' ').replace(exp2, ' ');
eo -= temp.length - _temp.length;
}
var text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
text = cur.textContent.replace(exp1, ' ').replace(exp2, ' ');
removed = removed || cur.textContent.length !== text.length;
cur.textContent = text;
}
@@ -441,10 +435,8 @@ dom.removeSpace = function (node, begin, so, end, eo) {
eo: dom.isBR(end) ? 0 : eo
};
};
dom.node = function (node) {
return node.tagName ? node : node.parentNode;
};
dom.removeBetween = function (sc, so, ec, eo, towrite) {
var text;
if (ec.tagName) {
if (ec.childNodes[eo]) {
ec = ec.childNodes[eo];
@@ -529,22 +521,19 @@ dom.removeBetween = function (sc, so, ec, eo, towrite) {
so = 0;
$(ancestor).prepend(sc);
} else if (dom.isText(sc)) {
var text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
text = sc.textContent.replace(/[ \t\n\r]+$/, '\u00A0');
so = Math.min(so, text.length);
sc.textContent = text;
}
} else {
var text = ancestor.textContent;
text = ancestor.textContent;
ancestor.textContent = text.slice(0, so) + text.slice(eo, Infinity).replace(/^[ \t\n\r]+/, '\u00A0');
}
eo = so;
if(!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
if (!dom.isBR(sc) && !dom.isVisibleText(sc) && !dom.isText(dom.hasContentBefore(sc)) && !dom.isText(dom.hasContentAfter(sc))) {
ancestor = dom.node(sc);
var text = document.createTextNode('\u00A0');
text = document.createTextNode('\u00A0');
$(sc).before(text);
sc = text;
so = 0;
@@ -570,7 +559,7 @@ dom.outdent = function (node) {
return margin;
};
dom.scrollIntoViewIfNeeded = function (node) {
var node = dom.node(node);
node = dom.node(node);
var $span;
if (dom.isBR(node)) {
@@ -593,7 +582,7 @@ dom.scrollIntoViewIfNeeded = function (node) {
// get if a parent have a scrollbar
parent = node.parentNode;
while (parent != offsetParent &&
while (parent !== offsetParent &&
(parent.tagName === "BODY" || ["auto", "scroll"].indexOf(dom.getComputedStyle(parent).overflowY) === -1)) {
parent = parent.parentNode;
}
@@ -659,14 +648,9 @@ dom.isNotBreakable = function (node) {
// avoid triple click => crappy dom
return !dom.isText(node) && !dom.isBR(dom.firstChild(node)) && dom.isVoid(dom.firstChild(node));
};
dom.isContentEditable = function (node) {
return $(node).closest('[contenteditable]').is('[contenteditable="true"]');
};
dom.isPre = function (node) {
return node.tagName === "PRE";
};
dom.isFont = function (node) {
var nodeName = node && node.nodeName.toUpperCase();
return node && (nodeName === "FONT" ||
@@ -678,19 +662,10 @@ dom.isFont = function (node) {
dom.isVisibleText = function (textNode) {
return !!textNode.textContent.match(/\S|\u00A0/);
};
/* avoid thinking HTML comment are visible */
var old_isVisiblePoint = dom.isVisiblePoint;
dom.isVisiblePoint = function (point) {
return point.node.nodeType !== 8 && old_isVisiblePoint.apply(this, arguments);
};
/**
* order the style of the node to compare 2 nodes and remove attribute if empty
*
* @param {Node} node
* @return {String} className
*/
dom.orderStyle = function (node) {
var style = node.getAttribute('style');
if (!style) return null;
@@ -705,12 +680,6 @@ dom.orderStyle = function (node) {
node.setAttribute('style', style);
return style;
};
/**
* order the class of the node to compare 2 nodes and remove attribute if empty
*
* @param {Node} node
* @return {String} className
*/
dom.orderClass = function (node) {
var className = node.getAttribute && node.getAttribute('class');
if (!className) return null;
@@ -725,21 +694,9 @@ dom.orderClass = function (node) {
node.setAttribute('class', className);
return className;
};
/**
* return the current node (if text node, return parentNode)
*
* @param {Node} node
* @return {Node} node
*/
dom.node = function (node) {
return dom.isText(node) ? node.parentNode : node;
};
/**
* move all childNode to an other node
*
* @param {Node} from
* @param {Node} to
*/
dom.moveContent = function (from, to) {
if (from === to) {
return;
@@ -749,18 +706,16 @@ dom.moveContent = function (from, to) {
dom.insertAfter(from.lastChild, from);
}
} else {
while (from.firstChild && from.firstChild != to) {
while (from.firstChild && from.firstChild !== to) {
to.appendChild(from.firstChild);
}
}
};
dom.getComputedStyle = function (node) {
return node.nodeType == Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
return node.nodeType === Node.COMMENT_NODE ? {} : window.getComputedStyle(node);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
range.WrappedRange.prototype.reRange = function (keep_end, isNotBreakable) {
var sc = this.sc;
@@ -894,7 +849,7 @@ range.WrappedRange.prototype.isContentEditable = function () {
return dom.isContentEditable(this.sc) && (this.sc === this.ec || dom.isContentEditable(this.ec));
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
renderer.tplButtonInfo.fontsize = function (lang, options) {
var items = options.fontSizes.reduce(function (memo, v) {
@@ -910,8 +865,7 @@ renderer.tplButtonInfo.fontsize = function (lang, options) {
});
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* add some text commands */
//::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::::
key.nameFromCode[46] = 'DELETE';
key.nameFromCode[27] = 'ESCAPE';
@@ -960,7 +914,7 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable, 'tab');
var r = range.create();
var outdent = outdent || false;
outdent = outdent || false;
event.preventDefault();
if (r && (dom.ancestor(r.sc, dom.isCell) || dom.ancestor(r.ec, dom.isCell))) {
@@ -986,11 +940,12 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
$.summernote.pluginEvents.indent(event, editor, layoutInfo);
}
} else {
var next;
if (!outdent) {
if(dom.isText(r.sc)) {
var next = r.sc.splitText(r.so);
if (dom.isText(r.sc)) {
next = r.sc.splitText(r.so);
} else {
var next = document.createTextNode('')
next = document.createTextNode('');
$(r.sc.childNodes[r.so]).before(next);
}
editor.typing.insertTab($editable, r, options.tabsize);
@@ -1000,7 +955,7 @@ $.summernote.pluginEvents.tab = function (event, editor, layoutInfo, outdent) {
} else {
r = dom.merge(r.sc.parentNode, r.sc, r.so, r.ec, r.eo, null, true);
r = range.create(r.sc, r.so, r.ec, r.eo);
var next = r.sc.splitText(r.so);
next = r.sc.splitText(r.so);
r.sc.textContent = r.sc.textContent.replace(/(\u00A0)+$/g, '');
next.textContent = next.textContent.replace(/^(\u00A0)+/g, '');
range.create(r.sc, r.sc.textContent.length, r.sc, r.sc.textContent.length).select();
@@ -1062,27 +1017,30 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
var br = $("<br/>")[0];
var node;
var $node;
var $clone;
var contentBefore = r.sc.textContent.slice(0,r.so).match(/\S|\u00A0/);
if (!contentBefore && dom.isText(r.sc)) {
var node = r.sc.previousSibling;
node = r.sc.previousSibling;
while (!contentBefore && node && dom.isText(node)) {
contentBefore = dom.isVisibleText(node);
node = node.previousSibling;
}
}
var node = dom.node(r.sc);
node = dom.node(r.sc);
var exist = r.sc.childNodes[r.so] || r.sc;
exist = dom.isVisibleText(exist) || dom.isBR(exist) ? exist : dom.hasContentAfter(exist) || (dom.hasContentBefore(exist) || exist);
// table: add a tr
var td = dom.ancestor(node, dom.isCell);
if (td && !dom.nextElementSibling(node) && !dom.nextElementSibling(td) && !dom.nextElementSibling(td.parentNode) && (!dom.isText(r.sc) || !r.sc.textContent.slice(r.so).match(/\S|\u00A0/))) {
var $node = $(td.parentNode);
var $clone = $node.clone();
$node = $(td.parentNode);
$clone = $node.clone();
$clone.children().html(dom.blank);
$node.after($clone);
var node = dom.firstElementChild($clone[0]) || $clone[0];
node = dom.firstElementChild($clone[0]) || $clone[0];
range.create(node, 0, node, 0).select();
dom.scrollIntoViewIfNeeded(br);
event.preventDefault();
@@ -1125,8 +1083,8 @@ $.summernote.pluginEvents.enter = function (event, editor, layoutInfo) {
if (dom.isBR(last)) {
last = last.parentNode;
}
var $node = $(last);
var $clone = $node.clone().text("");
$node = $(last);
$clone = $node.clone().text("");
$node.after($clone);
node = dom.node(dom.firstElementChild($clone[0]) || $clone[0]);
$(node).html(br);
@@ -1197,22 +1155,7 @@ $.summernote.pluginEvents.visible = function (event, editor, layoutInfo) {
return true;
};
function summernote_keydown_clean (field) {
setTimeout(function () {
var r = range.create();
if (!r) return;
var node = r[field];
while (dom.isText(node)) {node = node.parentNode;}
node = node.parentNode;
var data = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
data = dom.removeSpace(node.parentNode, data.sc, data.so, data.sc, data.so);
range.create(data.sc, data.so, data.sc, data.so).select();
},0);
}
function remove_table_content(r) {
var ancestor = r.commonAncestor();
var nodes = dom.listBetween(r.sc, r.ec);
if (dom.isText(r.sc)) {
r.sc.textContent = r.sc.textContent.slice(0, r.so);
@@ -1226,12 +1169,12 @@ function remove_table_content(r) {
continue;
} else if (dom.isCell(node)) {
$(node).html("<br/>");
} else if(node.parentNode) {
} else if (node.parentNode) {
do {
var parent = node.parentNode;
parent.removeChild(node);
node = parent;
} while(!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
} while (!dom.isVisibleText(node) && !dom.firstElementChild(node) &&
!dom.isCell(node) &&
node.parentNode && !$(node.parentNode).hasClass('o_editable'));
}
@@ -1282,12 +1225,12 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
var content = target.textContent.replace(/[ \t\r\n]+$/, '');
var temp;
var temp2;
var next;
// media
if (dom.isImg(node) || (!contentAfter && dom.isImg(dom.hasContentAfter(node)))) {
var parent;
var index;
var r;
if (!dom.isImg(node)) {
node = dom.hasContentAfter(node);
}
@@ -1295,7 +1238,7 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
parent = node.parentNode;
index = dom.position(node);
if (index>0) {
var next = node.previousSibling;
next = node.previousSibling;
r = range.create(next, next.textContent.length);
} else {
r = range.create(parent, 0);
@@ -1315,7 +1258,7 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
return false;
}
var before = false;
var next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
next = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
if (!dom.isContentEditable(next)) {
before = true;
next = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
@@ -1343,9 +1286,9 @@ $.summernote.pluginEvents.delete = function (event, editor, layoutInfo) {
!dom.isNotBreakable(temp) &&
!dom.isNotBreakable(temp2)) {
dom.autoMerge(target, false);
var next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
next = dom.firstChild(dom.hasContentAfter(dom.ancestorHaveNextSibling(target)));
if (dom.isBR(next)) {
if(dom.position(next) == 0) {
if (dom.position(next) === 0) {
range.create(next.parentNode, 0).select();
}
else {
@@ -1426,6 +1369,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
var content = target.textContent.replace(/[ \t\r\n]+$/, '');
var temp;
var temp2;
var prev;
// delete media
if (dom.isImg(node) || (!contentBefore && dom.isImg(dom.hasContentBefore(node)))) {
@@ -1450,7 +1394,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
node = dom.lastChild(dom.hasContentBefore(dom.ancestorHavePreviousSibling(tr)) || $editable.get(0));
}
$(tr).empty();
if(!$(tr).closest('table').has('td, th').length) {
if (!$(tr).closest('table').has('td, th').length) {
$(tr).closest('table').remove();
}
$(tr).remove();
@@ -1472,7 +1416,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
return false;
}
var before = true;
var prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
prev = dom.hasContentBefore(dom.ancestorHavePreviousSibling(node));
if (!dom.isContentEditable(prev)) {
before = false;
prev = dom.hasContentAfter(dom.ancestorHaveNextSibling(node));
@@ -1499,7 +1443,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
(temp.tagName !== "LI" || !$('ul,ol', temp).length) && (temp2.tagName !== "LI" || !$('ul,ol', temp2).length) && // protect li
!dom.isNotBreakable(temp) &&
!dom.isNotBreakable(temp2)) {
var prev = dom.firstChild(target);
prev = dom.firstChild(target);
dom.autoMerge(target, true);
range.create(prev, 0).select();
}
@@ -1521,7 +1465,7 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
}
}
var r = range.create();
r = range.create();
if (r) {
$(dom.node(r.sc)).trigger("click"); // trigger click to disable and reanable editor and image handler
dom.scrollIntoViewIfNeeded(r.sc.parentNode.previousElementSibling || r.sc);
@@ -1531,9 +1475,6 @@ $.summernote.pluginEvents.backspace = function (event, editor, layoutInfo) {
return false;
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* add list command (create a uggly dom for chrome) */
function isFormatNode(node) {
return node.tagName && options.styleTags.indexOf(node.tagName.toLowerCase()) !== -1;
}
@@ -1603,13 +1544,13 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI
return;
}
var parent = p0.parentNode;
var ul = document.createElement(sorted ? "ol" : "ul");
parent = p0.parentNode;
ul = document.createElement(sorted ? "ol" : "ul");
parent.insertBefore(ul, p0);
var childNodes = parent.childNodes;
var brs = [];
var begin = false;
for (var i=0; i<childNodes.length; i++) {
for (i = 0; i < childNodes.length; i++) {
if (begin && dom.isBR(childNodes[i])) {
parent.removeChild(childNodes[i]);
i--;
@@ -1631,7 +1572,7 @@ $.summernote.pluginEvents.insertUnorderedList = function (event, editor, layoutI
parent.removeChild(childNodes[i]);
}
for (var i=0; i<brs.length; i++) {
for (i = 0; i < brs.length ; i++) {
parent.removeChild(brs[i]);
}
r.clean().select();
@@ -1648,11 +1589,10 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
if (!r) return;
var flag = false;
function indentUL (UL, start, end) {
function indentUL(UL, start, end) {
var next;
var tagName = UL.tagName;
var node = UL.firstChild;
var parent = UL.parentNode;
var ul = document.createElement(tagName);
var li = document.createElement("li");
li.style.listStyle = "none";
@@ -1686,15 +1626,14 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
li = prev;
li.parentNode.removeChild(dom.nextElementSibling(li));
}
var next = dom.nextElementSibling(li);
next = dom.nextElementSibling(li);
if (next && next.tagName === "LI" && (temp = dom.firstElementChild(next)) && temp.tagName === tagName && (dom.firstElementChild(li) !== dom.firstElementChild(next))) {
dom.doMerge(dom.firstElementChild(li), dom.firstElementChild(next));
li.parentNode.removeChild(dom.nextElementSibling(li));
}
}
function outdenttUL (UL, start, end) {
function outdenttUL(UL, start, end) {
var next;
var tagName = UL.tagName;
var node = UL.firstChild;
var parent = UL.parentNode;
var li = UL.parentNode.tagName === "LI" ? UL.parentNode : UL;
@@ -1733,7 +1672,7 @@ $.summernote.pluginEvents.indent = function (event, editor, layoutInfo, outdent)
dom.merge(parent, start, 0, end, 1, null, true);
}
function indentOther (p, start, end) {
function indentOther(p, start, end) {
if (p === start || $.contains(p, start) || $.contains(start, p)) {
flag = true;
}
@@ -1815,8 +1754,6 @@ $.summernote.pluginEvents.outdent = function (event, editor, layoutInfo) {
return $.summernote.pluginEvents.indent(event, editor, layoutInfo, true);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
$.summernote.pluginEvents.formatBlock = function (event, editor, layoutInfo, sTagName) {
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable);
@@ -1858,11 +1795,11 @@ $.summernote.pluginEvents.removeFormat = function (event, editor, layoutInfo, va
var $editable = layoutInfo.editable();
$editable.data('NoteHistory').recordUndo($editable);
var r = range.create();
if(!r) return;
if (!r) return;
var node = range.create().sc.parentNode;
document.execCommand('removeFormat');
document.execCommand('removeFormat');
var r = range.create();
r = range.create();
if (!r) return;
r = dom.merge(node, r.sc, r.so, r.ec, r.eo, null, true);
range.create(r.sc, r.so, r.ec, r.eo).select();
@@ -1875,15 +1812,16 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
var $recentcolor = $.find('.note-recent-color i');
var $recentcolorbtn = $.find('.note-recent-color');
//find last used color for fonts or for icons
//find last used color for fonts or for icons
//set this color into recentcolor button of both font and icon
for (var i in $recentcolor) {
var $font = $recentcolor[i];
var $button = $($recentcolorbtn[i]);
var className = $font.className.split(/\s+/);
var k;
if (sEvent === "foreColor") {
//set class for forecolor to recentcolor button font-icon
for (var k=2; k<className.length; k++) {
for (k=2; k<className.length; k++) {
if (className[k].length && className[k].slice(0,5) === "text-") {
className.splice(k,1);
k--;
@@ -1898,7 +1836,7 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
}
} else {
//set class for backcolor to recentcolor button font-icon
for (var k=2; k<className.length; k++) {
for (k=2; k<className.length; k++) {
if (className[k].length && className[k].slice(0,3) === "bg-") {
className.splice(k,1);
k--;
@@ -1925,17 +1863,17 @@ eventHandler.modules.toolbar.button.updateRecentColor = function (elBtn, sEvent,
$(document).on('click keyup', function () {
var $popover = $((range.create()||{}).sc).closest('[contenteditable]');
var popover_history = ($popover.data()||{}).NoteHistory;
if(!popover_history || popover_history == history) return;
if (!popover_history || popover_history === history) return;
var editor = $popover.parent('.note-editor');
$('button[data-event="undo"]', editor).attr('disabled', !popover_history.hasUndo());
$('button[data-event="redo"]', editor).attr('disabled', !popover_history.hasRedo());
});
eventHandler.modules.editor.undo = function ($popover) {
if(!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
if (!$popover.attr('disabled')) $popover.data('NoteHistory').undo();
};
eventHandler.modules.editor.redo = function ($popover) {
if(!$popover.attr('disabled')) $popover.data('NoteHistory').redo();
if (!$popover.attr('disabled')) $popover.data('NoteHistory').redo();
};
// use image toolbar if current range is on image
@@ -1943,19 +1881,19 @@ var fn_editor_currentstyle = eventHandler.modules.editor.currentStyle;
eventHandler.modules.editor.currentStyle = function (target) {
var styleInfo = fn_editor_currentstyle.apply(this, arguments);
// with our changes for inline editor, the targeted element could be a button of the editor
if(!styleInfo.image || !dom.isEditable(styleInfo.image)) {
if (!styleInfo.image || !dom.isEditable(styleInfo.image)) {
styleInfo.image = undefined;
var r = range.create();
if(r)
if (r)
styleInfo.image = r.isOnImg();
}
return styleInfo;
}
};
options.fontSizes = [_t('Default'), 8, 9, 10, 11, 12, 14, 18, 24, 36, 48, 62];
$.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color, bgcolor, size) {
var r = range.create();
if(!r) return;
if (!r) return;
var startPoint = r.getStartPoint();
var endPoint = r.getEndPoint();
@@ -1978,13 +1916,15 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// get first and last point
if (endPoint.offset && endPoint.offset != dom.nodeLength(endPoint.node)) {
var ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
var ancestor;
var node;
if (endPoint.offset && endPoint.offset !== dom.nodeLength(endPoint.node)) {
ancestor = dom.ancestor(endPoint.node, dom.isFont) || endPoint.node;
dom.splitTree(ancestor, endPoint);
}
if (startPoint.offset && startPoint.offset != dom.nodeLength(startPoint.node)) {
var ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
var node = dom.splitTree(ancestor, startPoint);
if (startPoint.offset && startPoint.offset !== dom.nodeLength(startPoint.node)) {
ancestor = dom.ancestor(startPoint.node, dom.isFont) || startPoint.node;
node = dom.splitTree(ancestor, startPoint);
if (endPoint.node === startPoint.node) {
endPoint.node = node;
endPoint.offset = dom.nodeLength(node);
@@ -1999,7 +1939,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
var node = point.node;
if (((dom.isText(node) && dom.isVisibleText(node)) ||
(dom.isFont(node) && !dom.isVisibleText(node))) &&
(node != endPoint.node || endPoint.offset)) {
(node !== endPoint.node || endPoint.offset)) {
nodes.push(point.node);
@@ -2013,9 +1953,10 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// apply font: foreColor, backColor, size (the color can be use a class text-... or bg-...)
var node, font, $font, fonts = [], className;
var font, $font, fonts = [], className;
var i;
if (color || bgcolor || size) {
for (var i=0; i<nodes.length; i++) {
for (i=0; i<nodes.length; i++) {
node = nodes[i];
font = dom.ancestor(node, dom.isFont);
@@ -2033,8 +1974,9 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
className = font.className.split(/\s+/);
var k;
if (color) {
for (var k=0; k<className.length; k++) {
for (k=0; k<className.length; k++) {
if (className[k].length && className[k].slice(0,5) === "text-") {
className.splice(k,1);
k--;
@@ -2050,7 +1992,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
}
if (bgcolor) {
for (var k=0; k<className.length; k++) {
for (k=0; k<className.length; k++) {
if (className[k].length && className[k].slice(0,3) === "bg-") {
className.splice(k,1);
k--;
@@ -2077,7 +2019,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
// remove empty values
// we must remove the value in 2 steps (applay inherit then remove) because some
// browser like chrome have some time an error for the rendering and/or keep inherit
for (var i=0; i<fonts.length; i++) {
for (i=0; i<fonts.length; i++) {
font = fonts[i];
if (font.style.backgroundColor === "inherit") {
font.style.backgroundColor = "";
@@ -2100,13 +2042,13 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
}
// select nodes to clean (to remove empty font and merge same nodes)
var nodes = [];
nodes = [];
dom.walkPoint(startPoint, endPoint, function (point) {
nodes.push(point.node);
});
nodes = list.unique(nodes);
function remove (node, to) {
function remove(node, to) {
if (node === endPoint.node) {
endPoint = dom.prevPoint(endPoint);
}
@@ -2118,8 +2060,8 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
// remove node without attributes (move content), and merge the same nodes
if (!dom.isImgFont(node)) {
var className2, style, style2, $prev;
for (var i=0; i<nodes.length; i++) {
var className2, style, style2;
for (i=0; i<nodes.length; i++) {
node = nodes[i];
if ((dom.isText(node) || dom.isBR(node)) && !dom.isVisibleText(node)) {
@@ -2150,7 +2092,7 @@ $.summernote.pluginEvents.applyFont = function (event, editor, layoutInfo, color
if (i>0 && (font = dom.ancestor(nodes[i-1], dom.isFont))) {
className2 = font.getAttribute('class');
style2 = font.getAttribute('style');
if (node !== font && className == className2 && style == style2) {
if (node !== font && className === className2 && style === style2) {
remove(node, font);
nodes.splice(i,1);
i--;
@@ -2169,7 +2111,6 @@ $.summernote.pluginEvents.fontSize = function (event, editor, layoutInfo, value)
editor.afterCommand($editable);
};
$.summernote.pluginEvents.color = function (event, editor, layoutInfo, sObjColor) {
var $editable = layoutInfo.editable();
var oColor = JSON.parse(sObjColor);
var foreColor = oColor.foreColor, backColor = oColor.backColor;
@@ -2200,8 +2141,6 @@ $.summernote.pluginEvents.backColor = function (event, editor, layoutInfo, backC
editor.afterCommand($editable);
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
options.onCreateLink = function (sLinkUrl) {
if (sLinkUrl.indexOf('mailto:') === 0) {
// pass
@@ -2213,16 +2152,13 @@ options.onCreateLink = function (sLinkUrl) {
return sLinkUrl;
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
/* table */
function summernote_table_scroll (event) {
function summernote_table_scroll(event) {
var r = range.create();
if (r && r.isOnCell()) {
$('.o_table_handler').remove();
}
}
function summernote_table_update (oStyle) {
function summernote_table_update(oStyle) {
var r = range.create();
if (!oStyle.range || !r || !r.isOnCell() || !r.isOnCellFirst()) {
$('.o_table_handler').remove();
@@ -2334,15 +2270,15 @@ function summernote_table_update (oStyle) {
var fn_popover_update = eventHandler.modules.popover.update;
eventHandler.modules.popover.update = function ($popover, oStyle, isAirMode) {
fn_popover_update.call(this, $popover, oStyle, isAirMode);
if(!!(isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
if ((isAirMode ? $popover : $popover.parent()).find('.note-table').length) {
summernote_table_update(oStyle);
}
};
// override summernote clipboard functionality
eventHandler.modules.clipboard.attach = function(layoutInfo) {
eventHandler.modules.clipboard.attach = function (layoutInfo) {
var $editable = layoutInfo.editable();
$editable.on('paste', function(e) {
$editable.on('paste', function (e) {
var clipboardData = ((e.originalEvent || e).clipboardData || window.clipboardData);
// Change nothing if pasting html (copy from text editor / web / ...) or
// if clipboardData is not available (IE / ...)
@@ -2359,8 +2295,6 @@ eventHandler.modules.clipboard.attach = function(layoutInfo) {
});
};
//////////////////////////////////////////////////////////////////////////////////////////////////////////
var fn_attach = eventHandler.attach;
eventHandler.attach = function (oLayoutInfo, options) {
var $editable = oLayoutInfo.editor().hasClass('note-editable') ? oLayoutInfo.editor() : oLayoutInfo.editor().find('.note-editable');
@@ -2379,5 +2313,4 @@ options.icons.image.image = "file-image-o";
$.summernote.lang['en-US'].image.image = "File / Image";
return $.summernote;
});
@@ -4,39 +4,49 @@ odoo.define('web_editor.transcoder', function (require) {
var widget = require('web_editor.widget');
var rulesCache = [];
var getMatchedCSSRules = function (a) {
/**
* Returns the css rules which applies on an element, tweaked so that they are
* browser/mail client ok.
*
* @param {DOMElement} a
* @returns {Object} css property name -> css property value
*/
function getMatchedCSSRules(a) {
var i, r, k;
if (!rulesCache.length) {
var sheets = document.styleSheets;
for(var i = sheets.length-1; i >= 0 ; i--) {
for (i = sheets.length-1 ; i >= 0 ; i--) {
var rules;
if (sheets[i].rules) {
rules = sheets[i].rules;
} else {
//try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
// try...catch because Firefox not able to enumerate
// document.styleSheets[].cssRules[] for cross-domain sheets.
try {
rules = sheets[i].cssRules;
} catch(e) {
} catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
rules = sheets[i].cssRules;
}
if (rules) {
for(var r = rules.length-1; r >= 0; r--) {
for (r = rules.length-1; r >= 0; r--) {
var selectorText = rules[r].selectorText;
if (selectorText &&
rules[r].cssText &&
selectorText !== "*" &&
selectorText.indexOf(":hover") === -1 &&
selectorText.indexOf(":before") === -1 &&
selectorText.indexOf(":after") === -1 &&
selectorText.indexOf(":active") === -1 &&
selectorText.indexOf(":link") === -1 &&
selectorText.indexOf("::") === -1 &&
selectorText.indexOf("\"") === -1 &&
selectorText !== '*' &&
selectorText.indexOf(':hover') === -1 &&
selectorText.indexOf(':before') === -1 &&
selectorText.indexOf(':after') === -1 &&
selectorText.indexOf(':active') === -1 &&
selectorText.indexOf(':link') === -1 &&
selectorText.indexOf('::') === -1 &&
selectorText.indexOf('"') === -1 &&
selectorText.indexOf("'") === -1) {
var st = selectorText.split(/\s*,\s*/);
for (var k=0; k<st.length; k++) {
for (k = 0 ; k < st.length ; k++) {
rulesCache.push({ 'selector': st[k], 'style': rules[r].style });
}
}
@@ -47,13 +57,15 @@ var getMatchedCSSRules = function (a) {
}
var css = [];
var style;
a.matches = a.matches || a.webkitMatchesSelector || a.mozMatchesSelector || a.msMatchesSelector || a.oMatchesSelector;
for(var r = 0; r < rulesCache.length; r++) {
for (r = 0; r < rulesCache.length; r++) {
if (a.matches(rulesCache[r].selector)) {
var style = rulesCache[r].style;
style = rulesCache[r].style;
if (style.parentRule) {
var style_obj = {};
for (var k=0, len=style.length; k<len; k++) {
var len;
for (k = 0, len = style.length ; k < len ; k++) {
if (style[k].indexOf('animation') !== -1) {
continue;
}
@@ -68,19 +80,19 @@ var getMatchedCSSRules = function (a) {
}
}
function specificity (selector) {
function specificity(selector) {
// http://www.w3.org/TR/css3-selectors/#specificity
var a = 0;
selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ""; });
selector = selector.replace(/#[a-z0-9_-]+/gi, function () { a++; return ''; });
var b = 0;
selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ""; });
selector = selector.replace(/(\.[a-z0-9_-]+)|(\[.*?\])/gi, function () { b++; return ''; });
var c = 0;
selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if(a.indexOf(':not(')===-1) c++; return ""; });
selector = selector.replace(/(^|\s+|:+)[a-z0-9_-]+/gi, function (a) { if (a.indexOf(':not(')===-1) c++; return ''; });
return a*100 + b*10 + c;
}
css.sort(function (a, b) { return specificity(a[0]) - specificity(b[0]); });
var style = {};
style = {};
_.each(css, function (v,k) {
_.each(v[1], function (v,k) {
if (v && _.isString(v) && k.indexOf('-webkit') === -1 && (!style[k] || style[k].indexOf('important') === -1 || v.indexOf('important') !== -1)) {
@@ -99,7 +111,7 @@ var getMatchedCSSRules = function (a) {
delete style.display;
}
_.each(['margin', 'padding'], function(p) {
_.each(['margin', 'padding'], function (p) {
if (style[p+'-top'] || style[p+'-right'] || style[p+'-bottom'] || style[p+'-left']) {
if (style[p+'-top'] === style[p+'-right'] && style[p+'-top'] === style[p+'-bottom'] && style[p+'-top'] === style[p+'-left']) {
// keep => property: [top/right/bottom/left value];
@@ -132,103 +144,125 @@ var getMatchedCSSRules = function (a) {
}
return style;
};
}
// convert font awsome into image
var font_to_img = function ($editable) {
$editable.find(".fa").each(function () {
/**
* Converts font icons to images.
*
* @param {jQuery} $editable - the element in which the font icons have to be
* converted to images
*/
function fontToImg($editable) {
$editable.find('.fa').each(function () {
var $font = $(this);
var icon, content;
_.find(widget.fontIcons, function (font) {
return _.find(widget.getCssSelectors(font.parser), function (css) {
if ($font.is(css[0].replace(/::?before/g, ''))) {
icon = css[2].split("-").shift();
icon = css[2].split('-').shift();
content = css[1].match(/content:\s*['"]?(.)['"]?/)[1];
return true;
}
});
});
if (content) {
var color = $font.css("color").replace(/\s/g, '');
$font.replaceWith($("<img/>", {
"src": _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, $font.height())),
"data-class": $font.attr("class"),
"data-style": $font.attr("style"),
"class": $font.attr("class").replace(new RegExp("(^|\\s+)" + icon + "(-[^\\s]+)?", "gi"), ''), // remove inline font-awsome style
"style": $font.attr("style")
}).css({height: "auto", width: "auto"}));
var color = $font.css('color').replace(/\s/g, '');
$font.replaceWith($('<img/>', {
src: _.str.sprintf('/web_editor/font_to_img/%s/%s/%s', content.charCodeAt(0), window.encodeURI(color), Math.max(1, $font.height())),
'data-class': $font.attr('class'),
'data-style': $font.attr('style'),
class: $font.attr('class').replace(new RegExp('(^|\\s+)' + icon + '(-[^\\s]+)?', 'gi'), ''), // remove inline font-awsome style
style: $font.attr('style'),
}).css({height: 'auto', width: 'auto'}));
} else {
$font.remove();
}
});
};
// convert image into font awsome
var img_to_font = function ($editable) {
$editable.find("img[src*='/web_editor/font_to_img/']").each(function () {
}
/**
* Converts images which were the result of a font icon convertion to a font
* icon again.
*
* @param {jQuery} $editable - the element in which the images will be converted
* back to font icons
*/
function imgToFont($editable) {
$editable.find('img[src*="/web_editor/font_to_img/"]').each(function () {
var $img = $(this);
$img.replaceWith($("<span/>", {
"class": $img.data("class"),
"style": $img.data("style")
$img.replaceWith($('<span/>', {
class: $img.data('class'),
style: $img.data('style')
}));
});
};
}
// convert class into inline style to send by mail
var class_to_style = function ($editable) {
/**
* Converts css style to inline style (leave the classes on elements but forces
* the style they give as inline style).
*
* @param {jQuery} $editable
*/
function classToStyle($editable) {
if (!rulesCache.length) {
getMatchedCSSRules($editable[0]);
}
$editable.find('*').each(function () {
var $target = $(this);
var css = getMatchedCSSRules(this);
var style = $target.attr("style") || "";
var style = $target.attr('style') || '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
style = k+":"+v+";"+style;
style = k+':'+v+';'+style;
}
});
if (_.isEmpty(style)) {
$target.removeAttr("style");
$target.removeAttr('style');
} else {
$target.attr("style", style);
$target.attr('style', style);
}
});
};
// convert style into inline class from mail
var style_to_class = function ($editable) {
}
/**
* Removes the inline style which is not necessary (because, for example, a
* class on an element will induce the same style).
*
* @param {jQuery} $editable
*/
function styleToClass($editable) {
getMatchedCSSRules($editable[0]);
var $c = $('<span/>').appendTo("body");
var $c = $('<span/>').appendTo(document.body);
$editable.find('*').each(function () {
var $target = $(this);
var css = getMatchedCSSRules(this);
var style = "";
var style = '';
_.each(css, function (v,k) {
if (!(new RegExp('(^|;)\s*' + k).test(style))) {
style = k+":"+v+";"+style;
style = k+':'+v+';'+style;
}
});
css = ($c.attr("style", style).attr("style") || "").split(/\s*;\s*/);
style = $target.attr("style") || "";
css = ($c.attr('style', style).attr('style') || '').split(/\s*;\s*/);
style = $target.attr('style') || '';
_.each(css, function (v) {
style = style.replace(v, '');
});
style = style.replace(/;+(\s;)*/g, ';').replace(/^;/g, '');
if (style !== '') {
$target.attr("style", style);
$target.attr('style', style);
} else {
$target.removeAttr("style");
$target.removeAttr('style');
}
});
$c.remove();
};
}
return {
'font_to_img': font_to_img,
'img_to_font': img_to_font,
'class_to_style': class_to_style,
'style_to_class': style_to_class,
fontToImg: fontToImg,
imgToFont: imgToFont,
classToStyle: classToStyle,
styleToClass: styleToClass,
};
});
@@ -2,70 +2,65 @@ odoo.define('web_editor.translate', function (require) {
'use strict';
var core = require('web.core');
var ajax = require('web.ajax');
var Dialog = require('web.Dialog');
var localStorage = require('web.local_storage');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var weContext = require('web_editor.context');
var rte = require('web_editor.rte');
var editor_widget = require('web_editor.widget');
var weWidgets = require('web_editor.widget');
var _t = core._t;
var translatable = !!$('html').data('translatable');
var edit_translations = !!$('html').data('edit_translations');
var localStorageNoDialogKey = 'website_translator_nodialog';
var RTE_Translate = rte.Class.extend({
saveElement: function ($el, context) {
// remove multi edition
var key = $el.data('oe-translation-id') ? 'translation:'+$el.data('oe-translation-id') :
($el.data('oe-model') ? $el.data('oe-model')+":"+$el.data('oe-id')+":"+$el.data('oe-field')+":"+$el.data('oe-type')+":"+$el.data('oe-expression') : false);
if (!key || this.__saved[key]) return true;
this.__saved[key] = true;
var RTETranslatorWidget = rte.Class.extend({
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* If the element holds a translation, saves it. Otherwise, fallback to the
* standard saving but with the lang kept.
*
* @override
*/
_saveElement: function ($el, context, withLang) {
if ($el.data('oe-translation-id')) {
var translation_content = this.getEscapedElement($el).html();
return ajax.jsonRpc('/web/dataset/call', 'call', {
return this._rpc({
model: 'ir.translation',
method: 'save_html',
args: [
[+$el.data('oe-translation-id')],
translation_content,
context || base.get_context()
],
});
} else {
var markup = this.getEscapedElement($el).prop('outerHTML');
return ajax.jsonRpc('/web/dataset/call', 'call', {
model: 'ir.ui.view',
method: 'save',
args: [
$el.data('oe-id'),
markup,
$el.data('oe-xpath') || null,
context || base.get_context()
this._getEscapedElement($el).html(),
context || weContext.get()
],
});
}
return this._super($el, context, withLang === undefined ? true : withLang);
},
});
var Translate_Modal = editor_widget.Dialog.extend({
init: function (p, options, parent, node) {
this._super(p, _.extend({}, {
var AttributeTranslateDialog = weWidgets.Dialog.extend({
/**
* @constructor
*/
init: function (parent, options, node) {
this._super(parent, _.extend({
title: _t("Translate Attribute"),
buttons: [
{text: _t("Close"), classes: "btn-primary o_save_button", close: true, click: this.save}
]
{text: _t("Close"), classes: 'btn-primary o_save_button', click: this.save}
],
}, options || {}));
this.parent = parent;
this.$target = $(node);
this.translation = $(node).data('translation');
},
/**
* @override
*/
start: function () {
var self = this;
var def = this._super.apply(this, arguments);
var $group = $("<div/>", {"class": "form-group"}).appendTo(this.$el);
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);
@@ -74,42 +69,68 @@ var Translate_Modal = editor_widget.Dialog.extend({
var value = $input.val();
$node.html(value).trigger('change', node);
$node.data('$node').attr($node.data('attribute'), value).trigger('translate');
self.parent.rte_changed(node);
self.trigger_up('rte_change', {target: node});
});
$group.append($label).append($input);
});
return def;
return this._super.apply(this, arguments);
}
});
var Translate = Widget.extend({
events: {
'click [data-action="save"]': 'save_and_reload',
'click [data-action="cancel"]': 'cancel',
var TranslatorInfoDialog = Dialog.extend({
template: 'web_editor.TranslatorInfoDialog',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/translator.xml']
),
/**
* @constructor
*/
init: function (parent, options) {
this._super(parent, _.extend({
title: _t("Translation Info"),
buttons: [
{text: _t("Ok, never show me this again"), classes: 'btn-primary', close: true, click: this._onStrongOk.bind(this)},
{text: _t("Ok"), close: true}
],
}, options || {}));
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "strong" ok is clicked -> adapt localstorage to make sure
* the dialog is never displayed again.
*
* @private
*/
_onStrongOk: function () {
localStorage.setItem(localStorageNoDialogKey, true);
},
});
var TranslatorMenuBar = Widget.extend({
template: 'web_editor.editorbar',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
'click [data-action="save"]': '_onSaveClick',
'click [data-action="cancel"]': '_onCancelClick',
},
custom_events: {
'rte_change': '_onRTEChange',
},
/**
* @constructor
*/
init: function (parent, $target, lang) {
this.parent = parent;
this.lang = lang || base.get_context().lang;
this.setTarget($target);
this._super.apply(this, arguments);
this.rte = new RTE_Translate(this, this.config);
this.rte.on('change', this, this.rte_changed);
},
start: function () {
this._super.apply(this, arguments);
this.$('#web_editor-toolbars').remove();
return this.edit();
},
setTarget: function ($target) {
var $edit = $target.find('[data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field]');
$edit.filter(':has([data-oe-translation-id], [data-oe-model][data-oe-id][data-oe-field])').attr('data-oe-readonly', true);
this.$target = $edit.not('[data-oe-readonly]');
// attributes
var attrs = ['placeholder', 'title', 'alt'];
_.each(attrs, function (attr) {
$target.find('['+attr+'*="data-oe-translation-id="]').filter(':empty, input, select, textarea, img').each(function () {
@@ -135,39 +156,99 @@ var Translate = Widget.extend({
});
this.$target_attr = $target.find('.o_translatable_attribute');
this.$target_attribute = $('.o_editable_translatable_attribute');
this.lang = lang || weContext.get().lang;
this.rte = new RTETranslatorWidget(this, this._getRTEConfig);
},
find: function (selector) {
return selector ? this.$target.find(selector).addBack().filter(selector) : this.$target;
},
edit: function () {
/**
* @override
*/
start: function () {
this.$('#web_editor-toolbars').remove();
var flag = false;
window.onbeforeunload = function (event) {
if ($('.o_editable.o_dirty').length && !flag) {
flag = true;
setTimeout(function () {flag=false;},0);
setTimeout(function () {
flag = false;
}, 0);
return _t('This document is not saved!');
}
};
this.$target.addClass("o_editable");
this.$target.addClass('o_editable');
this.rte.start();
this.translations = [];
this.markTranslatableNodes();
this.onTranslateReady();
this._markTranslatableNodes();
this.$el.show(); // TODO seems useless
this.trigger('edit');
if (!localStorage.getItem(localStorageNoDialogKey)) {
new TranslatorInfoDialog(this).open();
}
return this._super.apply(this, arguments);
},
onTranslateReady: function () {
this.$el.show();
this.trigger("edit");
/**
* @override
*/
destroy: function () {
this._cancel();
this._super.apply(this, arguments);
},
rte_changed: function (node) {
var $node = $(node);
$node.find("p").each(function () { // remove <p/> element which might have been inserted because of copy-paste
var $p = $(this);
$p.after($p.html()).remove();
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Leaves translation mode by hiding the translator bar.
*
* @todo should be destroyed?
* @private
*/
_cancel: function () {
var self = this;
this.rte.cancel();
this.$target.each(function () {
$(this).html(self._getTranlationObject(this).value);
});
var trans = this.getTranlationObject($node[0]);
$node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
this._unmarkTranslatableNode();
this.trigger('cancel');
this.$el.hide();
window.onbeforeunload = null;
},
getTranlationObject: function (node) {
/**
* Returns the RTE summernote configuration for translation mode.
*
* @private
* @param {jQuery} $editable
*/
_getRTEConfig: function ($editable) {
return {
airMode : true,
focus: false,
airPopover: $editable.data('oe-model') ? [
['history', ['undo', 'redo']],
] : [
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['history', ['undo', 'redo']],
],
styleWithSpan: false,
inlinemedia : ['p'],
lang: 'odoo',
onChange: function (html, $editable) {
$editable.trigger('content_changed');
},
};
},
/**
* @private
*/
_getTranlationObject: function (node) {
var $node = $(node);
var id = +$node.data('oe-translation-id');
if (!id) {
@@ -181,33 +262,23 @@ var Translate = Widget.extend({
}
return trans;
},
__unbind_click: function (event) {
if (event.ctrlKey || !$(event.target).is(':o_editable')) {
return;
}
event.preventDefault();
event.stopPropagation();
},
__translate_attribute: function (event) {
if (event.ctrlKey) {
return;
}
event.preventDefault();
event.stopPropagation();
if (event.type !== 'mousedown') {
return;
}
new Translate_Modal(null, {}, event.data, event.target).open();
},
markTranslatableNodes: function (node) {
/**
* @private
*/
_markTranslatableNodes: function () {
var self = this;
this.$target.add(this.$target_attribute).each(function () {
var $node = $(this);
var trans = self.getTranlationObject(this);
var trans = self._getTranlationObject(this);
trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
});
this.$target.parent().prependEvent('click.translator', this, this.__unbind_click);
this.$target.parent().prependEvent('click.translator', function (ev) {
if (ev.ctrlKey || !$(ev.target).is(':o_editable')) {
return;
}
ev.preventDefault();
ev.stopPropagation();
});
// attributes
@@ -215,105 +286,86 @@ var Translate = Widget.extend({
var $node = $(this);
var translation = $node.data('translation');
_.each(translation, function (node, attr) {
var trans = self.getTranlationObject(node);
var trans = self._getTranlationObject(node);
trans.value = (trans.value ? trans.value : $node.html() ).replace(/[ \t\n\r]+/, ' ');
$node.attr('data-oe-translation-state', (trans.state || 'to_translate'));
});
});
this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', this, this.__translate_attribute);
this.$target_attr.prependEvent('mousedown.translator click.translator mouseup.translator', function (ev) {
if (ev.ctrlKey) {
return;
}
ev.preventDefault();
ev.stopPropagation();
if (ev.type !== 'mousedown') {
return;
}
console.info('Click on CTRL when you click in an translatable area to have the default behavior');
new AttributeTranslateDialog(self, {}, ev.target).open();
});
},
unarkTranslatableNode: function () {
/**
* Saves the translation and reloads the page to leave edit mode.
*
* @private
* @returns {Deferred} (never resolved as the page is reloading anyway)
*/
_save: function () {
return this.rte.save(weContext.get({lang: this.lang})).then(function () {
window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
return $.Deferred();
});
},
/**
* @private
*/
_unmarkTranslatableNode: function () {
this.$target.removeClass('o_editable').removeAttr('contentEditable');
this.$target.parent().off('.translator');
this.$target_attr.off('.translator');
},
save_and_reload: function () {
return this.save().then(function () {
window.location.href = window.location.href.replace(/&?edit_translations(=[^&]*)?/g, '');
});
},
save: function () {
var context = base.get_context();
context.lang = this.lang;
return this.rte.save(context);
},
cancel: function () {
var self = this;
this.rte.cancel();
this.$target.each(function () {
$(this).html(self.getTranlationObject(this).value);
});
this.unarkTranslatableNode();
this.trigger("cancel");
this.$el.hide();
window.onbeforeunload = null;
},
destroy: function () {
this.cancel();
this.$el.remove();
this._super.apply(this, arguments);
},
config: function ($editable) {
if ($editable.data('oe-model')) {
return {
'airMode' : true,
'focus': false,
'airPopover': [
['history', ['undo', 'redo']],
],
'styleWithSpan': false,
'inlinemedia' : ['p'],
'lang': "odoo",
'onChange': function (html, $editable) {
$editable.trigger("content_changed");
}
};
}
return {
'airMode' : true,
'focus': false,
'airPopover': [
['font', ['bold', 'italic', 'underline', 'clear']],
['fontsize', ['fontsize']],
['color', ['color']],
['history', ['undo', 'redo']],
],
'styleWithSpan': false,
'inlinemedia' : ['p'],
'lang': "odoo",
'onChange': function (html, $editable) {
$editable.trigger("content_changed");
}
};
}
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "cancel" button is clicked -> undo changes and leaves
* edition.
*
* @private
*/
_onCancelClick: function () {
this._cancel();
},
/**
* Called when text is edited -> make sure text is not messed up and mark
* the element as dirty.
*
* @private
* @param {OdooEvent} ev
*/
_onRTEChange: function (ev) {
var $node = $(ev.data.target);
$node.find('p').each(function () { // remove <p/> element which might have been inserted because of copy-paste
var $p = $(this);
$p.after($p.html()).remove();
});
var trans = this._getTranlationObject($node[0]);
$node.toggleClass('o_dirty', trans.value !== $node.html().replace(/[ \t\n\r]+/, ' '));
},
/**
* Called when the "save" button is clicked -> saves the translations.
*
* @private
*/
_onSaveClick: function () {
this._save();
},
});
if (edit_translations) {
base.ready().then(function () {
data.instance = new Translate(this, $('#wrapwrap'));
data.instance.prependTo(document.body);
$('a[href*=edit_translations]').each(function () {
this.href = this.href.replace(/[$?]edit_translations[^&?]+/, '');
});
$('form[action*=edit_translations]').each(function () {
this.action = this.action.replace(/[$?]edit_translations[^&?]+/, '');
});
$('title').html($('title').html().replace(/&lt;span data-oe-model.+?&gt;(.+?)&lt;\/span&gt;/, '\$1'));
});
}
var data = {
'translatable': translatable,
'edit_translations': edit_translations,
'Class': Translate,
return {
Class: TranslatorMenuBar,
};
return data;
});
+100 -36
View File
@@ -1,29 +1,29 @@
odoo.define('web_editor.iframe', function (require) {
'use strict';
var ajax = require("web.ajax");
var core = require("web.core");
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');
var callback = window ? window["callback"] : undefined;
window.top.odoo[callback+"_updown"] = function (value, fields_values, field_name) {
var $editable = $("#editable_area");
if(value !== $editable.prop("innerHTML")) {
if ($('body').hasClass('editor_enable')) {
if (value !== fields_values[field_name]) {
rte.history.recordUndo($editable);
}
snippet_editor.instance.make_active(false);
var callback = window ? window['callback'] : undefined;
window.top.odoo[callback + '_updown'] = function (value, fields_values, field_name) {
var $editable = $('#editable_area');
if (value === $editable.prop('innerHTML')) {
return;
}
if ($('body').hasClass('editor_enable')) {
if (value !== fields_values[field_name]) {
rte.history.recordUndo($editable);
}
core.bus.trigger('deactivate_snippet');
}
$editable.html(value);
$editable.html(value);
if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
$editable.trigger("content_changed");
}
if ($('body').hasClass('editor_enable') && value !== fields_values[field_name]) {
$editable.trigger('content_changed');
}
};
@@ -32,33 +32,26 @@ editor.Class.include({
this.on('rte:start', this, function () {
this.$('form').hide();
if (window.top.odoo[callback+"_editor"]) {
window.top.odoo[callback+"_editor"](this);
if (window.top.odoo[callback + '_editor']) {
window.top.odoo[callback + '_editor'](this);
}
var $editable = $("#editable_area");
var $editable = $('#editable_area');
setTimeout(function () {
$($editable.find("*").filter(function () {return !this.children.length;}).first()[0] || $editable)
.focusIn().trigger("mousedown").trigger("keyup");
$($editable.find('*').filter(function () {return !this.children.length;}).first()[0] || $editable)
.focusIn().trigger('mousedown').trigger('keyup');
},0);
$editable.on('content_changed', this, function () {
if (window.top.odoo[callback+"_downup"]) {
window.top.odoo[callback+"_downup"]($editable.prop('innerHTML'));
if (window.top.odoo[callback + '_downup']) {
window.top.odoo[callback + '_downup']($editable.prop('innerHTML'));
}
});
});
this.on("snippets:ready", this, function () {
$(window.top).trigger("resize");
return this._super.apply(this, arguments).then(function () {
$(window.top).trigger('resize'); // TODO check, probably useless
});
return this._super.apply(this, arguments);
}
});
snippet_editor.Class.include({
_get_snippet_url: function () {
return snippets_url;
}
});
@@ -69,18 +62,89 @@ rte.Class.include({
config.airPopover.splice(7, 0, ['view', ['codeview']]);
}
return config;
}
},
});
translator.Class.include({
start: function () {
var res = this._super.apply(this, arguments);
$('button[data-action=save]').hide();
if (window.top.odoo[callback+"_editor"]) {
window.top.odoo[callback+"_editor"](this);
if (window.top.odoo[callback + '_editor']) {
window.top.odoo[callback + '_editor'](this);
}
return res;
}
},
});
});
//==============================================================================
odoo.define('web_editor.IframeRoot.instance', function (require) {
'use strict';
require('web.dom_ready');
var iframeRootData = require('web_editor.IframeRoot');
var iframeRoot = new iframeRootData.IframeRoot(null);
return iframeRoot.attachTo(document.body).then(function () {
return iframeRoot;
});
});
//==============================================================================
odoo.define('web_editor.IframeRoot', function (require) {
'use strict';
var BodyManager = require('web_editor.BodyManager');
var weContext = require('web_editor.context');
var editor = require('web_editor.editor');
var rootWidget = require('web_editor.root_widget');
var translate = require('web_editor.translate');
var iframeRootRegistry = new rootWidget.RootWidgetRegistry();
var IframeRoot = BodyManager.extend({
/**
* @override
* @todo this is somehow a duplicate of website features
*/
start: function () {
var defs = [this._super.apply(this, arguments)];
var ctx = weContext.getExtra();
if (ctx.editable && window.location.search.indexOf('enable_editor') >= 0) {
var editorInstance = new (editor.Class)(this);
defs.push(editorInstance.prependTo(this.$el));
}
if (ctx.edit_translations) {
var translator = new (translate.Class)(this, this.$('#wrapwrap'));
defs.push(translator.prependTo(this.$el));
}
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* As the IframeRoot instance is designed to be unique, the associated
* registry has been instantiated outside of the class and is simply
* returned here.
*
* @override
*/
_getRegistry: function () {
return iframeRootRegistry;
},
});
return {
IframeRoot: IframeRoot,
iframeRootRegistry: iframeRootRegistry,
};
});
+26 -25
View File
@@ -1,6 +1,7 @@
odoo.define('web_editor.inline', function (require) {
'use strict';
var core = require('web.core');
var editor = require('web_editor.editor');
var rte = require('web_editor.rte');
var widget = require('web_editor.widget');
@@ -9,22 +10,22 @@ var snippet_editor = require('web_editor.snippet.editor');
widget.MediaDialog.include({
start: function () {
this._super.apply(this, arguments);
this.$('[href="#editor-media-video"]').addClass('hidden');
}
return this._super.apply(this, arguments);
},
});
editor.Class.include({
start: function () {
if (location.search.indexOf("enable_editor") !== -1) {
if (window.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")) {
$('#wrapwrap').on('click', function (event) {
if ($(event.target).is('#wrapwrap') || $(event.target).is('#editable_area:empty')) {
_.defer(function () {
var node = $("#editable_area *")
var node = $('#editable_area *')
.filter(function () { return this.textContent.match(/\S|\u00A0/); })
.add($("#editable_area"))
.add($('#editable_area'))
.last()[0];
$.summernote.core.range.create(node, $.summernote.core.dom.nodeLength(node)).select();
});
@@ -38,21 +39,22 @@ editor.Class.include({
snippet_editor.Class.include({
start: function () {
this._super.apply(this, arguments);
setTimeout(function () {
var $editable = $("#editable_area");
transcoder.img_to_font($editable);
transcoder.style_to_class($editable);
_.defer(function () {
var $editable = $('#editable_area');
transcoder.imgToFont($editable);
transcoder.styleToClass($editable);
// fix outlook image rendering bug
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
});
return this._super.apply(this, arguments);
},
clean_for_save: function () {
cleanForSave: function () {
this._super.apply(this, arguments);
var $editable = $("#editable_area");
transcoder.font_to_img($editable);
transcoder.class_to_style($editable);
var $editable = $('#editable_area');
transcoder.fontToImg($editable);
transcoder.classToStyle($editable);
// fix outlook image rendering bug
_.each(['width', 'height'], function (attribute) {
@@ -65,24 +67,23 @@ snippet_editor.Class.include({
},
});
window.top.odoo[callback+"_updown"] = function (value, fields_values) {
var $editable = $("#editable_area");
value = value || "";
var callback = window ? window['callback'] : undefined;
window.top.odoo[callback + '_updown'] = function (value, fields_values) {
var $editable = $('#editable_area');
value = value || '';
if (value.indexOf('on_change_model_and_list') === -1 && value !== $editable.html()) {
rte.history.recordUndo($editable, null, true);
if (snippet_editor.instance) {
snippet_editor.instance.make_active(false);
}
core.bus.trigger('deactivate_snippet');
$editable.html(value);
transcoder.img_to_font($editable);
transcoder.style_to_class($editable);
transcoder.imgToFont($editable);
transcoder.styleToClass($editable);
// fix outlook image rendering bug
$editable.find('img[style*="width"], img[style*="height"]').removeAttr('height width');
} else {
$editable.trigger("content_changed");
$editable.trigger('content_changed');
}
};
});
@@ -0,0 +1,155 @@
odoo.define('web_editor.root_widget', function (require) {
'use strict';
var Class = require('web.Class');
var dom = require('web.dom');
var mixins = require('web.mixins');
var Widget = require('web.Widget');
/**
* Specialized Widget which automatically instantiates child widgets to attach
* to internal DOM elements once it is started. The widgets to instantiate are
* known thanks to a linked registry which contains info about the widget
* classes and jQuery selectors to use to find the elements to attach them to.
*/
var RootWidget = Widget.extend({
custom_events: _.extend({}, Widget.prototype.custom_events || {}, {
registry_update: '_onRegistryUpdate',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this._widgets = [];
this._listenToUpdates = false;
this._getRegistry().setParent(this);
},
/**
* @override
* @see _attachComponents
*/
start: function () {
var defs = [this._super.apply(this, arguments)];
defs.push(this._attachComponents());
this._listenToUpdates = true;
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Instantiates a child widget according to the given registry data.
*
* @private
* @param {Object} childInfo
* @param {function} childInfo.Widget - the widget class to instantiate
* @param {string} childInfo.selector
* the jQuery selector to use to find the internal DOM element which
* needs to be attached to the instantiated widget
* @param {jQuery} [$from] - only check DOM elements which are descendant of
* the given one. If not given, use this.$el.
* @returns {Deferred}
*/
_attachComponent: function (childInfo, $from) {
var self = this;
var $elements = dom.cssFind($from || this.$el, childInfo.selector);
var defs = _.map($elements, function (element) {
var w = new childInfo.Widget(self);
self._widgets.push(w);
return w.attachTo(element);
});
return $.when.apply($, defs);
},
/**
* Instantiates the child widgets that need to be according to the linked
* registry.
*
* @private
* @param {jQuery} [$from] - only check DOM elements which are descendant of
* the given one. If not given, use this.$el.
* @returns {Deferred}
*/
_attachComponents: function ($from) {
var self = this;
var childInfos = this._getRegistry().get();
var defs = _.map(childInfos, function (childInfo) {
return self._attachComponent(childInfo, $from);
});
return $.when.apply($, defs);
},
/**
* Returns the `RootWidgetRegistry` instance that is linked to this
* `RootWidget` instance.
*
* @abstract
* @private
* @returns {RootWidgetRegistry}
*/
_getRegistry: function () {},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the linked registry is updated after this `RootWidget`
*
* @private
* @param {OdooEvent} ev
*/
_onRegistryUpdate: function (ev) {
ev.stopPropagation();
if (this._listenToUpdates) {
this._attachComponent(ev.data);
}
},
});
var RootWidgetRegistry = Class.extend(mixins.EventDispatcherMixin, {
/**
* @constructor
*/
init: function () {
mixins.EventDispatcherMixin.init.call(this);
this._registry = [];
},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Adds an element to the registry (info of what and how to instantiate).
*
* @param {function} Widget - the widget class to instantiate
* @param {string} selector
* the jQuery selector to use to find the internal DOM element which
* needs to be attached to the instantiated widget
*/
add: function (Widget, selector) {
var registryInfo = {
Widget: Widget,
selector: selector,
};
this._registry.push(registryInfo);
this.trigger_up('registry_update', registryInfo);
},
/**
* Retrieves all the registry elements.
*/
get: function () {
return this._registry;
},
});
return {
RootWidget: RootWidget,
RootWidgetRegistry: RootWidgetRegistry,
};
});
@@ -1,119 +0,0 @@
odoo.define('web_editor.snippets.animation', function (require) {
'use strict';
var Class = require('web.Class');
var base = require('web_editor.base');
var registry = {};
var ready = [];
/**
* The Animation class provides a way for executing code once a website DOM element is loaded in the dom
* and handle the case where the website edit mode is triggered.
*/
var Animation = Class.extend({
/**
* The selector attribute, if defined, allows to automatically create an instance of this animation on
* page load for each DOM element which matches this selector. The Animation $target element will then
* be that particular DOM element. This should be the main way of instantiating Animation elements.
*/
selector: false,
/**
* The $ method is a helper to search for DOM elements inside the animation DOM element target.
* @param selector of the element to search in the DOM element target
*/
$: function () {
return this.$el.find.apply(this.$el, arguments);
},
/**
* The init method starts the animation, waiting for its dependencies to be initialized.
* The init method is called when instantiating the Animation thanks to the "new" keyword.
* @param dom - the DOM element the animation is targeting
* @param editable_mode - A boolean which is true if the page is in edition mode
*/
init: function (dom, editable_mode) {
this.$el = this.$target = $(dom);
this._start(editable_mode);
},
/**
* The willStart method initializes the resources the start method will need to properly work.
* @param editable_mode - A boolean which is true if the page is in edition mode
* @return A deferred which is resolved once all the resources are loaded
*/
willStart: function (editable_mode) {
return $.when();
},
/**
* The abstract start method is intended to initialize the animation.
* The method should not be called directly as called automatically on animation initialization and
* on restart (@see Animation.init, @see Animation.restart).
* @param editable_mode - A boolean which is true if the page is in edition mode
* @param next argument will be the resources given by the willStart deferred (@see Animation.willStart)
*/
start: function (editable_mode) {},
/**
* The abstract stop method is intended to destroy the animation and should basically restore the
* target to the way it was before the start method was called.
* Note: this method should be callable even if the start method was not called earlier.
*/
stop: function () {},
/**
* The restart method stops the animation then starts it again, waiting for the eventual willStart
* deferred.
* @param editable_mode - A boolean which is true if the page is in edition mode
* @return A deferred which is resolved once the Animation resources/elements are (re)loaded/(re)initialized.
*/
restart: function (editable_mode) {
return $.when(this.stop()).then(this._start.bind(this, editable_mode));
},
/**
* The private _start method basically calls the start method after waiting for the end
* of the willStart method.
* @param editable_mode - A boolean which is true if the page is in edition mode
* @return A deferred which is resolved once the Animation resources/elements are loaded/initialized.
*/
_start: function (editable_mode) {
return this.willStart(editable_mode).then(this.start.bind(this, editable_mode));
},
});
/**
* Start animations when the website loading is finished
*/
base.ready().always(function () {
_.defer(start);
});
return {
Class: Animation,
registry: registry,
start: start,
};
/**
* The start function allows to create an Animation instance for each registered one for each DOM element
* which matches the "selector" key of these registered animations (@see Animation.selector).
* @param editable_mode - A boolean which is true if the page is in edition mode
* @param $init_target (optional) - a jQuery DOM element ; if given, only initialize the animations whose
* "selector" matches it
*/
function start(editable_mode, $init_target) {
_.each(registry, function (Animation) {
var selector = Animation.prototype.selector || "";
var $target = $init_target ? $init_target.filter(selector) : $(selector);
$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.restart(editable_mode);
}
});
});
}
});
+322 -316
View File
@@ -1,322 +1,328 @@
odoo.define('web_editor.tour', function (require) {
"use strict";
'use strict';
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var snippet_editor = require('web_editor.snippet.editor');
var core = require('web.core');
var tour = require('web_tour.tour');
var base = require('web_editor.base');
var click_event = function (el, type) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
};
/**
* Simulates a click event of given type on the given element.
*
* @param {DOMElement} el
* @param {string} type - 'click', 'mouseup', ...
*/
function simulateClickEvent(el, type) {
var evt = document.createEvent('MouseEvents');
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
}
tour.register("rte", {
url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<section>\n'+
' <div class="container" style="width: 600px;">\n'+
' <div class="row">\n'+
' <div class="col-md-6 mt16">\n'+
'<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
'<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
' </div>\n'+
' <div class="col-md-6 mt16">\n'+
' <img class="img img-responsive shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
' </div>\n'+
' </div>\n'+
'</section>\n';
this.$anchor.html(html);
}
}, {
content: "simulate triple click and change text bg-color",
trigger: '#editable_area > section .row > div:first',
run: function () {
var $h1 = $('h1', this.$anchor);
$.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
click_event($h1[0], 'mouseup');
}
}, {
content: "change text bg-color after triple click",
trigger: '.note-popover button[data-event="color"]',
extra_trigger: '#editable_area > section .row > div:first',
}, {
content: "change selection to change text color",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
run: function () {
$.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
click_event(this.$anchor[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text color",
trigger: ".btn-group.open button[data-event=foreColor]:first",
}, {
content: "change selection to change text bg-color again",
trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
click_event(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text backColor again",
trigger: "button[data-event=backColor]:visible:first",
}, {
content: "change selection (h1 and p) to change text color with class",
trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
click_event(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text color again",
trigger: "button[data-event=foreColor]:visible:eq(3)",
}, {
content: "delete selection",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first p font',
run: "keydown 46", // delete
}, {
content: "create an other selection to delete",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
$.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
click_event(this.$anchor.find('font:last')[0], 'mouseup');
},
}, {
content: "clean and delete (backspace) an other selection",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
click_event(this.$anchor.find('font:first')[0], 'mouseup');
},
}, {
content: "delete an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
run: "keydown 46",
}, {
content: "undo",
trigger: '.note-image-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
}, {
content: "undo again",
trigger: '.note-air-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
}, {
content: "delete (backspace) after undo",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "click on image",
trigger: '#editable_area > section .row > div:first img[style*="25%"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
}, {
content: "Click on resize half",
trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
}, {
content: "Click on edit picture",
trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
}, {
content: "Click on pictogram tab",
trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
extra_trigger: '#editor-media-image',
}, {
content: "select a pictogram",
trigger: '#editor-media-icon.active span.fa:first',
}, {
content: "save pictogram",
trigger: 'button.o_save_button',
extra_trigger: '#editor-media-icon.active span.o_selected',
}, {
content: "select a size for the pictogram",
trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
}, {
content: "click on float right",
trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
extra_trigger: '#wrapwrap span.fa-3x',
}, {
content: "click on create link",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
}, {
content: "click on color style",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
}, {
content: "insert a link url",
trigger: '#o_link_dialog_url_input',
extra_trigger: 'a#link-preview.btn',
run: "text http://www.odoo.com",
}, {
content: "change text label",
trigger: '#o_link_dialog_label_input',
run: "text ABC[IMG] DEF",
}, {
content: "save link",
trigger: 'button.o_save_button',
extra_trigger: 'a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
}, {
content: "click on other picture",
trigger: '#editable_area > section .row > div:last img',
extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
}, {
content: "click on create link again",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
}, {
content: "click on color style again",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
}, {
content: "insert an email",
trigger: '#o_link_dialog_url_input',
extra_trigger: 'a#link-preview.btn',
run: "text test@test.test",
}, {
content: "save link",
trigger: 'button.o_save_button',
extra_trigger: 'a#link-preview.btn[href="mailto:test@test.test"]',
}, {
content: "select for triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
run: function () {
var p = this.$anchor[0].firstChild;
$.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
click_event(p, 'mouseup');
},
}, {
content: "triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
}, {
content: "add ul content",
trigger: '#editable_area > section .row > div:first',
extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
run: function () {
var html = ' <ul> '+
'\n <li> <p>Batnae municipium. </p></li>'+
'\n <li> Seleucia praeter.</li>'+
'\n <li><p>Et hanc quidem.</p></li>'+
'\n </ul>';
this.$anchor.append(html);
var node = this.$anchor.find('ul li p:last')[0].firstChild;
$.summernote.core.range.create(node, 6).select();
click_event(node, 'mouseup');
}
}, {
content: "click on style dropdown",
trigger: '.note-air-popover .note-style button.dropdown-toggle',
extra_trigger: '#editable_area > section .row > div:first ul li p:first',
}, {
content: "select h3",
trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
}, {
content: "select h3",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: function () {
var node = this.$anchor[0].firstChild;
$.summernote.core.range.create(node, 0).select();
click_event(node, 'mouseup');
}
}, {
content: "double tabulation",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: "keydown 9,9", // tabulation
}, {
content: "click on order list",
trigger: '.note-air-popover button[data-event="insertOrderedList"]',
extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
}, {
content: "select for enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: function () {
this.$anchor[0].firstChild.textContent += "";
$.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
click_event(this.$anchor[0], 'mouseup');
}
}, {
content: "enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: "keydown 66,13", // enter
}, {
trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
content: "backspace in list",
run: "keydown 8",
}, {
content: "end",
trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
}]);
tour.register('rte', {
url: '/web_editor/field/html?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<section>\n'+
' <div class="container" style="width: 600px;">\n'+
' <div class="row">\n'+
' <div class="col-md-6 mt16">\n'+
'<h1 id="text_content_id">Batnae municipium in Anthemusia</h1> \n'+
' <p>Batnae municipium in Anthemusia conditum Macedonum manu priscorum ab Euphrate flumine brevi spatio disparatur, refertum mercatoribus opulentis, ubi annua sollemnitate prope Septembris initium mensis ad.</p>\n'+
' <p> Quam <img style="width: 25%" src="/web/static/src/img/logo.png"/> quidem <span class="fa fa-flag fa-2x"></span> partem accusationis admiratus sum et moleste tuli potissimum esse Atratino datam. Neque enim decebat neque aetas.</p>\n'+
' <p>Et hanc quidem praeter oppida multa duae civitates exornant Seleucia opus Seleuci regis, et Claudiopolis quam deduxit coloniam Claudius Caesar. Isaura enim antehac nimium potens, olim subversa ut rebellatrix.</p>'+
'<p>Harum trium sententiarum nulli prorsus assentior.</p>\n'+
' </div>\n'+
' <div class="col-md-6 mt16">\n'+
' <img class="img img-responsive shadow mb16" src="/web/static/src/img/logo.png" alt="Odoo text and image block">\n'+
' </div>\n'+
' </div>\n'+
'</section>\n';
this.$anchor.html(html);
}
}, {
content: "simulate triple click and change text bg-color",
trigger: '#editable_area > section .row > div:first',
run: function () {
var $h1 = $('h1', this.$anchor);
$.summernote.core.range.create($h1[0].firstChild, 0, $('p', this.$anchor)[0], 0).select();
simulateClickEvent($h1[0], 'mouseup');
}
}, {
content: "change text bg-color after triple click",
trigger: '.note-popover button[data-event="color"]',
extra_trigger: '#editable_area > section .row > div:first',
}, {
content: "change selection to change text color",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1 font',
run: function () {
$.summernote.core.range.create(this.$anchor[0].firstChild, 5, this.$anchor[0].firstChild, 10).select();
simulateClickEvent(this.$anchor[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text color",
trigger: ".btn-group.open button[data-event=foreColor]:first",
}, {
content: "change selection to change text bg-color again",
trigger: '#editable_area > section .row > div:first h1 font:eq(2)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor[0].firstChild, 10).select();
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text backColor again",
trigger: "button[data-event=backColor]:visible:first",
}, {
content: "change selection (h1 and p) to change text color with class",
trigger: '#editable_area > section .row > div:first h1 font:eq(4)',
run: function () {
$.summernote.core.range.create(this.$anchor.prev()[0].firstChild, 3, this.$anchor.parent("h1").next("p")[0].firstChild, 30).select();
simulateClickEvent(this.$anchor.prev()[0], 'mouseup');
}
}, {
content: "open color dropdown",
trigger: ".note-color button.dropdown-toggle",
}, {
content: "change text color again",
trigger: "button[data-event=foreColor]:visible:eq(3)",
}, {
content: "delete selection",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first p font',
run: "keydown 46", // delete
}, {
content: "create an other selection to delete",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.createFromNode(this.$anchor.next("p")[0]).clean();
$.summernote.core.range.create(this.$anchor.find('font:containsExact(ici)')[0].firstChild, 1, this.$anchor.next().next()[0].firstChild, 5).select();
simulateClickEvent(this.$anchor.find('font:last')[0], 'mouseup');
},
}, {
content: "clean and delete (backspace) an other selection",
trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
extra_trigger: '.o_editable.note-editable.o_dirty',
run: function () {
$.summernote.core.range.create(this.$anchor.find('font:first')[0].firstChild, 3, this.$anchor.next("p")[0].childNodes[2], 8).select();
simulateClickEvent(this.$anchor.find('font:first')[0], 'mouseup');
},
}, {
content: "delete an other selection",
trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) ) h1',
run: "keydown 46",
}, {
content: "undo",
trigger: '.note-image-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(Bat) )',
}, {
content: "undo again",
trigger: '.note-air-popover button[data-event="undo"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) )',
}, {
content: "delete (backspace) after undo",
trigger: '.o_editable.note-editable.o_dirty',
extra_trigger: '#editable_area > section .row > div:first:not(:has(p font)) h1',
run: "keydown 8", // backspace
}, {
content: "click on image",
trigger: '#editable_area > section .row > div:first img[style*="25%"]',
extra_trigger: '#editable_area > section .row > div:first:has( font:last:containsExact(i) ):has( p:first:containsRegex(/^uam/) )',
}, {
content: "Click on resize half",
trigger: '.note-image-popover:visible button[data-event="resize"][data-value="0.5"]',
}, {
content: "Click on edit picture",
trigger: '.note-image-popover:visible button[data-event="showImageDialog"]',
extra_trigger: '#editable_area > section .row > div:first img[style*="50%"]',
}, {
content: "Click on pictogram tab",
trigger: 'a[data-toggle="tab"]:contains(Pictogram)',
extra_trigger: '#editor-media-image',
}, {
content: "select a pictogram",
trigger: '#editor-media-icon.active span.fa:first',
}, {
content: "save pictogram",
trigger: 'button.o_save_button',
extra_trigger: '#editor-media-icon.active span.o_selected',
}, {
content: "select a size for the pictogram",
trigger: '.note-image-popover button[data-event="resizefa"][data-value="3"]',
}, {
content: "click on float right",
trigger: '.note-image-popover:visible button[data-event="floatMe"][data-value="right"]',
extra_trigger: '#wrapwrap span.fa-3x',
}, {
content: "click on create link",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
}, {
content: "click on color style",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<span [^>]+><\\/span>$/) > span.fa.fa-3x.pull-right',
}, {
content: "insert a link url",
trigger: '#o_link_dialog_url_input',
extra_trigger: 'a#link-preview.btn',
run: "text http://www.odoo.com",
}, {
content: "change text label",
trigger: '#o_link_dialog_label_input',
run: "text ABC[IMG] DEF",
}, {
content: "save link",
trigger: 'button.o_save_button',
extra_trigger: 'a#link-preview.btn:containsRegex(/^ABC<span [^>]+><\\/span> DEF$/)',
}, {
content: "click on other picture",
trigger: '#editable_area > section .row > div:last img',
extra_trigger: 'body:not(:has(#link-preview)) a.btn[href^="http://"]:has(span.fa.fa-3x.pull-right)',
}, {
content: "click on create link again",
trigger: '.note-image-popover:visible button[data-event="showLinkDialog"]',
extra_trigger: '#editable_area > section .row > div:first span.fa.pull-right',
}, {
content: "click on color style again",
trigger: '.o_link_dialog_color > .o_link_dialog_color_item.btn-success',
extra_trigger: 'a#link-preview:containsRegex(/^<img [^>]+>$/) img',
}, {
content: "insert an email",
trigger: '#o_link_dialog_url_input',
extra_trigger: 'a#link-preview.btn',
run: "text test@test.test",
}, {
content: "save link",
trigger: 'button.o_save_button',
extra_trigger: 'a#link-preview.btn[href="mailto:test@test.test"]',
}, {
content: "select for triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
extra_trigger: 'body:not(:has(#link-preview)) #editable_area > section .row > div:eq(1) > a > img',
run: function () {
var p = this.$anchor[0].firstChild;
$.summernote.core.range.create(p, p.textContent.length, p, p.textContent.length).select();
simulateClickEvent(p, 'mouseup');
},
}, {
content: "triple enter then double backspace",
trigger: '#editable_area > section .row > div:first p:eq(2)',
run: "keydown 66,13,66,13,13,8,8", // B enter B enter enter backspace backspace
}, {
content: "add ul content",
trigger: '#editable_area > section .row > div:first',
extra_trigger: 'body:not(:has(#editable_area > section .row > div:first p:eq(5), #editable_area > section .row > div:eq(3))) #editable_area > section .row > div:first p:eq(3)',
run: function () {
var html = ' <ul> '+
'\n <li> <p>Batnae municipium. </p></li>'+
'\n <li> Seleucia praeter.</li>'+
'\n <li><p>Et hanc quidem.</p></li>'+
'\n </ul>';
this.$anchor.append(html);
var node = this.$anchor.find('ul li p:last')[0].firstChild;
$.summernote.core.range.create(node, 6).select();
simulateClickEvent(node, 'mouseup');
}
}, {
content: "click on style dropdown",
trigger: '.note-air-popover .note-style button.dropdown-toggle',
extra_trigger: '#editable_area > section .row > div:first ul li p:first',
}, {
content: "select h3",
trigger: '.note-air-popover .note-style ul:visible a[data-value="h3"]',
}, {
content: "select h3",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: function () {
var node = this.$anchor[0].firstChild;
$.summernote.core.range.create(node, 0).select();
simulateClickEvent(node, 'mouseup');
}
}, {
content: "double tabulation",
trigger: '#editable_area > section .row > div:first > ul > li > h3',
run: "keydown 9,9", // tabulation
}, {
content: "click on order list",
trigger: '.note-air-popover button[data-event="insertOrderedList"]',
extra_trigger: '#editable_area > section .row > div:first ul > li > ul > li > ul > li > h3',
}, {
content: "select for enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: function () {
this.$anchor[0].firstChild.textContent += "";
$.summernote.core.range.create(this.$anchor[0].firstChild, 7).select();
simulateClickEvent(this.$anchor[0], 'mouseup');
}
}, {
content: "enter in ul",
trigger: '#editable_area > section .row > div:first ul li > p:last',
run: "keydown 66,13", // enter
}, {
trigger: '#editable_area > section .row > div:first ul li > p:eq(1):containsRegex(/^municipium./)',
content: "backspace in list",
run: "keydown 8",
}, {
content: "end",
trigger: '#editable_area > section .row > div:first ul li p:eq(0):containsRegex(/^Batnae Bmunicipium.$/)',
}]);
tour.register('rte_inline', {
url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<div>\n'+
' <table cellspacing="0" cellpadding="0" width="100%">\n'+
' <tbody>\n'+
' <tr>\n'+
' <td valign="center" width="270">\n'+
' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
' </td>\n'+
' <td valign="center" width="270">\n'+
' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
' </td>\n'+
' </tr>\n'+
' </tbody>\n'+
' </table>\n'+
'</div>';
this.$anchor.html(html);
}
}, {
content: "call clean for save",
trigger: '#wrapwrap table',
run: function () {
snippet_editor.instance.clean_for_save();
}
}, {
content: "check the image style",
trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
}, {
content: "check the font image src",
trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(51,122,183)/28"]',
}, {
content: "check the font class to css",
trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
}, {
content: "check the second font class to css",
trigger: '#wrapwrap img:eq(2)[style*="float: right"]',
}]);
tour.register('rte_inline', {
url: '/web_editor/field/html/inline?callback=FieldTextHtml_0&enable_editor=1&datarecord=%7B%7D',
test: true,
wait_for: base.ready(),
}, [{
content: "Change html for this test",
trigger: "#editable_area",
run: function () {
var html = '\n'+
'<div>\n'+
' <table cellspacing="0" cellpadding="0" width="100%">\n'+
' <tbody>\n'+
' <tr>\n'+
' <td valign="center" width="270">\n'+
' <img src="/logo.png" alt="Your Logo" class="img-circle img-thumbnail">\n'+
' </td>\n'+
' <td valign="center" width="270">\n'+
' <a href="https://www.facebook.com/Odoo"><span class="fa fa-facebook-square fa-2x text-primary"></span></a>\n'+
' <span style="color: rgb(255, 0, 0);" class="fa fa-4x fa-google-plus-square pull-right"></span>\n'+
' </td>\n'+
' </tr>\n'+
' </tbody>\n'+
' </table>\n'+
'</div>';
this.$anchor.html(html);
}
}, {
content: "call clean for save",
trigger: '#wrapwrap table',
run: function () {
core.bus.trigger('snippet_editor_clean_for_save');
}
}, {
content: "check the image style",
trigger: '#wrapwrap img:first[width][height][style*="-radius"][style*="1px"][style*="padding"]',
}, {
content: "check the font image src",
trigger: '#wrapwrap img:eq(1)[src^="/web_editor/font_to_img/"][src$="/rgb(51,122,183)/28"]',
}, {
content: "check the font class to css",
trigger: '#wrapwrap img:eq(1)[height]:not([class*="fa"])',
}, {
content: "check the second font class to css",
trigger: '#wrapwrap img:eq(2)[style*="float: right"]',
}]);
});
File diff suppressed because it is too large Load Diff
+181 -180
View File
@@ -2,11 +2,9 @@ odoo.define('web_editor.widget', function (require) {
'use strict';
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');
var weContext = require("web_editor.context");
var QWeb = core.qweb;
var range = $.summernote.core.range;
@@ -48,6 +46,10 @@ Dialog = Dialog.extend({
*/
var alt = Dialog.extend({
template: 'web_editor.dialog.alt',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
init: function (parent, options, $editable, media) {
this._super(parent, _.extend({}, {
title: _t("Change media description and tooltip")
@@ -70,158 +72,11 @@ var alt = Dialog.extend({
},
});
var click_event = function (el, type) {
function click_event(el, type) {
var evt = document.createEvent("MouseEvents");
evt.initMouseEvent(type, true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
};
/**
* 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({
template: 'web_editor.dialog.media',
events : _.extend({}, Dialog.prototype.events, {
'input input#icon-search': 'search',
}),
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');
}
this.options = options || {};
this.old_media = media;
this.media = media;
this.isNewMedia = !media;
this.range = range.create();
},
start: function () {
var self = this;
this.$modal.addClass('note-image-dialog');
this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image"));
if (this.only_images) {
this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
}
this.opened((function () {
if (this.media) {
if (this.media.nodeName === "IMG") {
this.$('[href="#editor-media-image"]').tab('show');
} else if ($(this.media).is('a.o_image')) {
this.$('[href="#editor-media-document"]').tab('show');
} else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
this.$('[href="#editor-media-video"]').tab('show');
} else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
this.media = this.media.parentNode;
this.$('[href="#editor-media-video"]').tab('show');
} else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
this.$('[href="#editor-media-icon"]').tab('show');
}
}
}).bind(this));
this.imageDialog = new ImageDialog(this, this.media, this.options);
this.imageDialog.appendTo(this.$("#editor-media-image"));
this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
this.documentDialog.appendTo(this.$("#editor-media-document"));
if (!this.only_images) {
this.iconDialog = new fontIconsDialog(this, this.media, this.options);
this.iconDialog.appendTo(this.$("#editor-media-icon"));
this.videoDialog = new VideoDialog(this, this.media, this.options);
this.videoDialog.appendTo(this.$("#editor-media-video"));
}
this.active = this.imageDialog;
this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
if ($(event.target).is('[href="#editor-media-image"]')) {
self.active = self.imageDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
} else if ($(event.target).is('[href="#editor-media-document"]')) {
self.active = self.documentDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
} else if ($(event.target).is('[href="#editor-media-icon"]')) {
self.active = self.iconDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
} else if ($(event.target).is('[href="#editor-media-video"]')) {
self.active = self.videoDialog;
self.$('.nav-tabs li.search').addClass("hidden");
}
});
return this._super.apply(this, arguments);
},
save: function () {
if (this.options.select_images) {
this.final_data = this.active.save();
this._super.apply(this, arguments);
return;
}
if(this.rte) {
this.range.select();
this.rte.historyRecordUndo(this.media);
}
var self = this;
if (self.media) {
this.media.innerHTML = "";
if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
this.imageDialog.clear();
}
// if not mode only_images
if (this.iconDialog && this.active !== this.iconDialog) {
this.iconDialog.clear();
}
if (this.videoDialog && this.active !== this.videoDialog) {
this.videoDialog.clear();
}
} else {
this.media = document.createElement("img");
this.range.insertNode(this.media, true);
this.active.media = this.media;
}
this.active.save();
if (this.active.add_class) {
$(this.active.media).addClass(this.active.add_class);
}
var media = this.active.media;
this.final_data = [media, self.old_media];
$(document.body).trigger("media-saved", this.final_data);
$(self.old_media).trigger("save", this.final_data);
// Update editor bar after image edition (in case the image change to icon or other)
_.defer(function () {
if (!media.parentNode) return;
range.createFromNode(media).select();
click_event(media, "mousedown");
click_event(media, "mouseup");
});
this._super.apply(this, arguments);
},
searchTimer: null,
search: function () {
var self = this;
var needle = this.$("input#icon-search").val();
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(function () {
self.active.search(needle || "");
},250);
}
});
}
/**
* ImageDialog widget. Let users change an image, including uploading a
@@ -230,6 +85,7 @@ var MediaDialog = Dialog.extend({
*/
var ImageDialog = Widget.extend({
template: 'web_editor.dialog.image',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
IMAGES_PER_ROW: 6,
IMAGES_ROWS: 2,
events: _.extend({}, Dialog.prototype.events, {
@@ -284,7 +140,7 @@ var ImageDialog = Widget.extend({
o.id = +o.url.match(/\/web\/content\/([0-9]*)/, '')[1];
}
this.parent.$(".pager > li").click(function (e) {
if(!self.$el.is(':visible')) {
if (!self.$el.is(':visible')) {
return;
}
e.preventDefault();
@@ -331,7 +187,7 @@ var ImageDialog = Widget.extend({
this.add_class = "pull-left";
this.style = {"width": "100%"};
}
if(this.media.tagName !== "IMG") {
if (this.media.tagName !== "IMG") {
media = document.createElement('img');
$(this.media).replaceWith(media);
this.media = media;
@@ -431,15 +287,15 @@ var ImageDialog = Widget.extend({
if (needle && needle.length) {
domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
}
return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
return this._rpc({
model: 'ir.attachment',
method: 'search_read',
args: [],
kwargs: {
domain: domain,
fields: ['name', 'mimetype', 'checksum', 'url', 'type'],
order: 'id desc',
context: base.get_context()
order: [{name: 'id', asc: false}],
context: weContext.get(),
}
}).then(this.proxy('fetched_existing'));
},
@@ -516,7 +372,12 @@ var ImageDialog = Widget.extend({
$both.css({borderWidth: "5px", borderColor: "#f00"});
return ajax.jsonRpc('/web_editor/attachment/remove', 'call', {'ids': [id]}).then(function (prevented) {
return this._rpc({
route: '/web_editor/attachment/remove',
params: {
ids: [id],
},
}).then(function (prevented) {
if (_.isEmpty(prevented)) {
self.records = _.without(self.records, attachment);
self.display_attachments();
@@ -545,7 +406,7 @@ var getCssSelectors = function (filter) {
return cacheCssSelectors[filter];
}
var sheets = document.styleSheets;
for(var i = 0; i < sheets.length; i++) {
for (var i = 0; i < sheets.length; i++) {
var rules;
if (sheets[i].rules) {
rules = sheets[i].rules;
@@ -553,18 +414,18 @@ var getCssSelectors = function (filter) {
//try...catch because Firefox not able to enumerate document.styleSheets[].cssRules[] for cross-domain stylesheets.
try {
rules = sheets[i].cssRules;
} catch(e) {
} catch (e) {
console.warn("Can't read the css rules of: " + sheets[i].href, e);
continue;
}
}
if (rules) {
for(var r = 0; r < rules.length; r++) {
for (var r = 0; r < rules.length; r++) {
var selectorText = rules[r].selectorText;
if (selectorText) {
selectorText = selectorText.split(/\s*,\s*/);
var data = null;
for(var s = 0; s < selectorText.length; s++) {
for (var s = 0; s < selectorText.length; s++) {
var match = selectorText[s].match(filter);
if (match) {
var clean = match[1].slice(1).replace(/::?before$/, '');
@@ -596,28 +457,13 @@ var computeFonts = _.once(function () {
});
});
rte.Class.include({
init: function () {
this._super.apply(this, arguments);
computeFonts();
},
onEnableEditableArea: function ($editable) {
if ($editable.data('oe-type') === "monetary") {
$editable.attr('contenteditable', false);
$editable.find('.oe_currency_value').attr('contenteditable', true);
}
if ($editable.is('[data-oe-model]') && !$editable.is('[data-oe-model="ir.ui.view"]') && !$editable.is('[data-oe-type="html"]')) {
$editable.data('layoutInfo').popover().find('.btn-group:not(.note-history)').remove();
}
},
});
/**
* FontIconsDialog widget. Lets users change a font awesome, support all
* font awesome loaded in the css files.
*/
var fontIconsDialog = Widget.extend({
template: 'web_editor.dialog.font-icons',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : _.extend({}, Dialog.prototype.events, {
'click .font-icons-icon': function (e) {
e.preventDefault();
@@ -863,6 +709,7 @@ function createVideoNode(url, options) {
*/
var VideoDialog = Widget.extend({
template: 'web_editor.dialog.video',
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events : _.extend({}, Dialog.prototype.events, {
'click input#urlvideo ~ button': 'get_video',
'click input#embedvideo ~ button': 'get_embed_video',
@@ -936,7 +783,7 @@ var VideoDialog = Widget.extend({
if (this.media.dataset.src) {
try {
delete this.media.dataset.src;
} catch(e) {
} catch (e) {
this.media.dataset.src = undefined;
}
}
@@ -944,11 +791,164 @@ var VideoDialog = Widget.extend({
},
});
/**
* MediaDialog widget. Lets users change a media, including uploading a
* new image, font awsome or video and can change a media into an other
* media
*
* options: select_images: allow the selection of more of one image
*/
var MediaDialog = Dialog.extend({
template: 'web_editor.dialog.media',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
events : _.extend({}, Dialog.prototype.events, {
'input input#icon-search': 'search',
}),
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');
}
this.options = options || {};
this.old_media = media;
this.media = media;
this.isNewMedia = !media;
this.range = range.create();
},
start: function () {
var self = this;
this.$modal.addClass('note-image-dialog');
this.$modal.find('.modal-dialog').addClass('o_select_media_dialog');
this.only_images = this.options.only_images || this.options.select_images || (this.media && ($(this.media).parent().data("oe-field") === "image" || $(this.media).parent().data("oe-type") === "image"));
if (this.only_images) {
this.$('[href="#editor-media-document"], [href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
}
this.opened((function () {
if (this.media) {
if (this.media.nodeName === "IMG") {
this.$('[href="#editor-media-image"]').tab('show');
} else if ($(this.media).is('a.o_image')) {
this.$('[href="#editor-media-document"]').tab('show');
} else if (this.media.className.match(/(^|\s)media_iframe_video($|\s)/)) {
this.$('[href="#editor-media-video"]').tab('show');
} else if (this.media.parentNode.className.match(/(^|\s)media_iframe_video($|\s)/)) {
this.media = this.media.parentNode;
this.$('[href="#editor-media-video"]').tab('show');
} else if (this.media.className.match(/(^|\s)fa($|\s)/)) {
this.$('[href="#editor-media-icon"]').tab('show');
}
}
}).bind(this));
this.imageDialog = new ImageDialog(this, this.media, this.options);
this.imageDialog.appendTo(this.$("#editor-media-image"));
this.documentDialog = new ImageDialog(this, this.media, _.extend({'document': true}, this.options));
this.documentDialog.appendTo(this.$("#editor-media-document"));
if (!this.only_images) {
this.iconDialog = new fontIconsDialog(this, this.media, this.options);
this.iconDialog.appendTo(this.$("#editor-media-icon"));
this.videoDialog = new VideoDialog(this, this.media, this.options);
this.videoDialog.appendTo(this.$("#editor-media-video"));
}
this.active = this.imageDialog;
this.$('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
if ($(event.target).is('[href="#editor-media-image"]')) {
self.active = self.imageDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
} else if ($(event.target).is('[href="#editor-media-document"]')) {
self.active = self.documentDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
} else if ($(event.target).is('[href="#editor-media-icon"]')) {
self.active = self.iconDialog;
self.$('li.search, li.previous, li.next').removeClass("hidden");
self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
} else if ($(event.target).is('[href="#editor-media-video"]')) {
self.active = self.videoDialog;
self.$('.nav-tabs li.search').addClass("hidden");
}
});
return this._super.apply(this, arguments);
},
save: function () {
if (this.options.select_images) {
this.final_data = this.active.save();
this._super.apply(this, arguments);
return;
}
if (this.rte) {
this.range.select();
this.rte.historyRecordUndo(this.media);
}
var self = this;
if (self.media) {
this.media.innerHTML = "";
if (this.active !== this.imageDialog && this.active !== this.documentDialog) {
this.imageDialog.clear();
}
// if not mode only_images
if (this.iconDialog && this.active !== this.iconDialog) {
this.iconDialog.clear();
}
if (this.videoDialog && this.active !== this.videoDialog) {
this.videoDialog.clear();
}
} else {
this.media = document.createElement("img");
this.range.insertNode(this.media, true);
this.active.media = this.media;
}
this.active.save();
if (this.active.add_class) {
$(this.active.media).addClass(this.active.add_class);
}
var media = this.active.media;
this.final_data = [media, self.old_media];
$(document.body).trigger("media-saved", this.final_data);
$(self.old_media).trigger("save", this.final_data);
// Update editor bar after image edition (in case the image change to icon or other)
_.defer(function () {
if (!media.parentNode) return;
range.createFromNode(media).select();
click_event(media, "mousedown");
click_event(media, "mouseup");
});
this._super.apply(this, arguments);
},
searchTimer: null,
search: function () {
var self = this;
var needle = this.$("input#icon-search").val();
clearTimeout(this.searchTimer);
this.searchTimer = setTimeout(function () {
self.active.search(needle || "");
},250);
}
});
/**
* The Link Dialog allows to customize link content and style.
*/
var LinkDialog = Dialog.extend({
template: 'web_editor.dialog.link',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/web_editor/static/src/xml/editor.xml']
),
events: _.extend({}, Dialog.prototype.events, {
'change :input.url-source': 'changed',
'keyup :input.url': 'onkeyup',
@@ -1064,7 +1064,7 @@ var LinkDialog = Dialog.extend({
var label = this.$('#o_link_dialog_label_input').val() || val;
if (label && this.data.images) {
for(var i=0; i<this.data.images.length; i++) {
for (var i=0; i<this.data.images.length; i++) {
label = label.replace(/</, "&lt;").replace(/>/, "&gt;").replace(/\[IMG\]/, this.data.images[i].outerHTML);
}
}
@@ -1173,5 +1173,6 @@ return {
LinkDialog: LinkDialog,
getCssSelectors: getCssSelectors,
fontIcons: fontIcons,
computeFonts: computeFonts,
};
});
@@ -476,7 +476,6 @@ body .modal {
}
}
// FIXME bootstrap modal : 1050
.ui-autocomplete {
z-index: 1051;
z-index: @zindex-modal + 1;
}
@@ -166,7 +166,7 @@ html[lang] > body.editor_enable [data-oe-translation-state] {
}
}
&.disable {
&.o_disabled {
.oe_snippet_thumbnail {
background-color: rgba(255, 0, 0, 0.75);
}
@@ -302,6 +302,8 @@ html[lang] > body.editor_enable [data-oe-translation-state] {
@odoo-handle-edge-size: 1px;
@odoo-handle-border-width: 1px;
position: relative;
// SNIPPET MANIPULATORS
.oe_overlay {
.o-position-absolute();
+1 -1
View File
@@ -6,7 +6,7 @@
<!-- 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-top-edit">
<div id="web_editor-toolbars"/>
<form class="navbar-form text-muted">
<button type="button" class="btn btn-sm btn-default" data-action="cancel"><i class="fa fa-times"/> Discard</button>
@@ -1,18 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<!-- Snippet loader -->
<t t-name="web_editor.snippets_style">
<li class="navbar-form">
<div class="btn-group">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
Block style <span class="caret"></span>
</button>
<ul class="dropdown-menu" id="oe_snippets_style" role="menu"></ul>
</div>
</li>
</t>
<t t-name="web_editor.snippet_overlay">
<div class="oe_overlay">
<div class="oe_handles">
@@ -40,10 +27,6 @@
</div>
</t>
<t t-name="web_editor.snippet_manipulator">
<div class='oe_snippet_manipulator'></div>
</t>
<!-- options -->
<div t-name="web_editor.snippet.option.colorpicker" class="colorpicker">
<div class="note-palette-title">Background Color</div>
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<div t-name="web_editor.TranslatorInfoDialog">
<p>You are about to enter the translation mode.</p>
<p>Here are the visuals used to help you translate efficiently:</p>
<ul class="oe_translate_examples">
<li data-oe-translation-state="to_translate">Content to translate</li>
<li data-oe-translation-state="translated">Translated content</li>
</ul>
<p>
In this mode, you can only translate texts. To change the structure of the page, you must edit the master page.
Each modification on the master page is automatically applied to all translated versions.
</p>
</div>
</templates>
+9 -6
View File
@@ -61,9 +61,8 @@
<template id="assets_common" inherit_id="web.assets_common" name="Web Editor Common Assets (used in backend interface and website)">
<xpath expr="script[last()]" position="after">
<script type="text/javascript" src="/web_editor/static/lib/vkbeautify/vkbeautify.0.99.00.beta.js"></script>
<script type="text/javascript" src="/web_editor/static/src/js/widgets/ace.js" />
<script type="text/javascript" src="/web_editor/static/src/js/base.js" />
<script type="text/javascript" src="/web_editor/static/src/js/snippets.animation.js" />
<script type="text/javascript" src="/web_editor/static/src/js/base.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/widgets/ace.js"/>
</xpath>
</template>
@@ -81,6 +80,10 @@
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" type="text/less" href="/web_editor/static/src/less/web_editor.common.less"/> <!-- not in common because compiled differently with context -->
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/web_editor/static/src/js/content/body_manager.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/root_widget.js"/>
</xpath>
</template>
<template id="assets_editor" name="Web Editor Assets (used in private and public editor)">
@@ -93,12 +96,12 @@
<script type="text/javascript" src="/web_editor/static/src/js/editor/editor.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/rte.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/rte.summernote.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/widgets/widgets.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/snippets.editor.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/snippets.options.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/transcoder.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/translator.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/tours/rte.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/editor/transcoder.js"></script>
<script type="text/javascript" src="/web_editor/static/src/js/editor/translator.js"></script>
<script type="text/javascript" src="/web_editor/static/src/js/widgets/widgets.js"/>
</template>
<template id="webclient_bootstrap" inherit_id="web.webclient_bootstrap">
+3 -1
View File
@@ -12,7 +12,6 @@
<title>Odoo Editor layout</title>
<meta name="viewport" content="initial-scale=1"/>
<script type="text/javascript">
var snippets_url = '<t t-esc="snippets or ''"/>';
var callback = window.location.href.match(/callback=([^&amp;=]+)/)[1];
var odoo = {
@@ -21,6 +20,7 @@
is_frontend: true, // this is not really the frontend here but this key is used to detect if xml has to be manually loaded
},
_modules: ['web_editor'],
snippetsURL: '<t t-esc="snippets or ''"/>',
};
</script>
<t t-if="not dont_load_assets">
@@ -34,6 +34,8 @@
<t t-call-assets="web_editor.assets_editor" t-css="false"/>
<script type="text/javascript" src="/web/static/src/js/services/session.js"></script>
<script type="text/javascript" src="/web_editor/static/src/js/content/body_manager.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/root_widget.js"/>
<script type="text/javascript" src="/web_editor/static/src/js/iframe.js"></script>
<script t-if="enable_editor and inline_mode" type="text/javascript" src="/web_editor/static/src/js/inline.js"></script>
</t>
@@ -487,7 +487,7 @@ return core.Class.extend(mixins.EventDispatcherMixin, ServicesMixin, {
},
WEBSITE_NEW_PAGE: {
trigger: "#oe_main_menu_navbar a[data-action=new_page]",
trigger: "#new-content-menu > a",
auto: true,
position: "bottom",
},
+14 -15
View File
@@ -11,9 +11,9 @@ the drop zone.
Building Blocks
+++++++++++++++
Overwrite ``_get_snippet_url`` to set an other file to load the snippets (use by
Overwrite ``_getSnippetURL`` to set an other file to load the snippets (use by
website_mail for example)
Overwrite ``_add_check_selector`` to enable or disable other snippets. By default
Overwrite ``_computeSelectorFunctions`` to enable or disable other snippets. By default
the builder check if the node or his parent have the attribute data-oe-model
Trigger:
@@ -39,7 +39,7 @@ e.g.:
<span class="oe_snippet_thumbnail_title">...Block Name...</span>
</div>
<div class="oe_snippet_body">
<!--
<!--
The block with class 'oe_snippet_body' is inserted in the page.
This class is removed when the block is dropped.
The block can be made of any html tag and content. -->
@@ -69,26 +69,25 @@ Object:
html editor overlay who content resize bar, customize menu...
Methods:
- ``set_active``:
- ``_setActive``:
highlight the customize menu item when the user click on customize, and click on
an item.
- ``start``:
called when the editor is created on the DOM
- ``on_focus``:
- ``onFocus``:
called when the user click inside the block inserted in page and when the
user drop on block into the page
- ``on_blur``:
- ``onBlur``:
called when the user click outside the block inserted in page, if the block
is focused
- ``on_clone``:
- ``onClone``:
called when the snippet is duplicate
@variables: $clone is allready inserted is the page
- ``on_remove``:
- ``onRemove``:
called when the snippet is removed (dom is removing after this tigger)
- ``drop_and_build_snippet:
- ``onBuilt:
called just after that a thumbnail is drag and dropped into a drop zone.
The content is already inserted in the page.
- ``clean_for_save``:
- ``cleanForSave``:
is called just before to save the vue. Sometime it's important to remove or add
some datas (contentEditable, added classes to a running animation...)
@@ -103,8 +102,8 @@ By default to custom method are defined:
- ``check_class(type, className, $li)``:
li must have data-check_class="a_classname_for_test" to call this method. This method
toggle the className on $target
- ``select_class(type, className, $li)``:
This method remove all other select_class value (for this option) and add this current ClassName
- ``selectClass(type, className, $li)``:
This method remove all other selectClass value (for this option) and add this current ClassName
@@ -112,7 +111,7 @@ Snippet
+++++++
The ``snippets`` are the HTML code to defined the drop zone and the linked javascript object.
All HTML li tag defined inside the snippets HTML are insert into the customize menu. All
All HTML li tag defined inside the snippets HTML are insert into the customize menu. All
data attributes is optional:
- ``data-selector``:
@@ -151,4 +150,4 @@ The snippets are loaded in one time by js and the page stay editable.
More
++++
- Use the class ``o_not_editable`` to prevent the edition from an area.
- Use the class ``o_not_editable`` to prevent the edition from an area.
+6 -13
View File
@@ -1,5 +1,5 @@
odoo.define('website.backend', function (require) {
"use strict";
odoo.define('website.backend.button', function (require) {
'use strict';
var AbstractField = require('web.AbstractField');
var core = require('web.core');
@@ -33,19 +33,12 @@ var WidgetWebsiteButton = AbstractField.extend({
this._super.apply(this, arguments);
var $value = this.$('.o_value');
if (this.value === true) {
$value.html(_t('Published'))
.removeClass('text-danger')
.addClass('text-success');
} else {
$value.html(_t('Unpublished'))
.removeClass('text-success')
.addClass('text-danger');
}
var published = (this.value === true);
$value.html(published ? _t("Published") : _t("Unpublished"))
.toggleClass('text-danger', !published)
.toggleClass('text-success', published);
},
});
field_registry.add('website_button', WidgetWebsiteButton);
});
@@ -1,8 +1,6 @@
odoo.define('website.backendDashboard', function (require) {
"use strict";
odoo.define('website.backend.dashboard', function (require) {
'use strict';
var ajax = require('web.ajax');
var ControlPanelMixin = require('web.ControlPanelMixin');
var core = require('web.core');
var Dialog = require('web.Dialog');
@@ -17,7 +15,7 @@ var _t = core._t;
var QWeb = core.qweb;
var Dashboard = Widget.extend(ControlPanelMixin, {
template: "website.WebsiteDashboardMain",
template: 'website.WebsiteDashboardMain',
events: {
'click .js_link_analytics_settings': 'on_link_analytics_settings',
'click .o_dashboard_action': 'on_dashboard_action',
@@ -58,9 +56,12 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
fetch_data: function() {
var self = this;
return ajax.jsonRpc('/website/fetch_dashboard_data', 'call', {
'date_from': this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
'date_to': this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
return this._rpc({
route: '/website/fetch_dashboard_data',
params: {
date_from: this.date_from.year()+'-'+(this.date_from.month()+1)+'-'+this.date_from.date(),
date_to: this.date_to.year()+'-'+(this.date_to.month()+1)+'-'+this.date_to.date(),
},
}).done(function(result) {
self.data = result;
self.dashboards_data = result.dashboards;
@@ -235,11 +236,11 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
var $action = $(ev.currentTarget);
var additional_context = {};
if (this.date_range === 'week') {
additional_context = {'search_default_week': true}
additional_context = {search_default_week: true};
} else if (this.date_range === 'month') {
additional_context = {'search_default_month': true}
additional_context = {search_default_month: true};
} else if (this.date_range === 'year') {
additional_context = {'search_default_year': true}
additional_context = {search_default_year: true};
}
this.do_action($action.attr('name'), {
additional_context: additional_context,
@@ -597,7 +598,5 @@ var Dashboard = Widget.extend(ControlPanelMixin, {
core.action_registry.add('backend_dashboard', Dashboard);
return Dashboard;
});
@@ -0,0 +1,38 @@
odoo.define('website.content.compatibility', function (require) {
'use strict';
/**
* Tweaks the website rendering so that the old browsers correctly render the
* content too.
*/
require('web.dom_ready');
// Check the browser and its version and add the info as an attribute of the
// HTML element so that css selectors can match it
var browser = _.findKey($.browser, function (v) { return v === true; });
if ($.browser.mozilla && +$.browser.version.replace(/^([0-9]+\.[0-9]+).*/, '\$1') < 20) {
browser = 'msie';
}
browser += (',' + $.browser.version);
var mobileRegex = /android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i;
if (mobileRegex.test(window.navigator.userAgent.toLowerCase())) {
browser += ',mobile';
}
document.documentElement.setAttribute('data-browser', browser);
// Check if flex is supported and add the info as an attribute of the HTML
// element so that css selectors can match it (only if not supported)
var htmlStyle = document.documentElement.style;
var isFlexSupported = (('flexWrap' in htmlStyle)
|| ('WebkitFlexWrap' in htmlStyle)
|| ('msFlexWrap' in htmlStyle));
if (!isFlexSupported) {
document.documentElement.setAttribute('data-no-flex', '');
}
return {
browser: browser,
isFlexSupported: isFlexSupported,
};
});
@@ -0,0 +1,44 @@
odoo.define('website.content.lazy_template_call', function (require) {
'use strict';
var Widget = require('web.Widget');
var websiteRootData = require('website.WebsiteRoot');
var LazyTemplateRenderer = Widget.extend({
/**
* Lazy replaces the `[data-oe-call]` elements by their corresponding
* template content.
*
* @override
*/
start: function () {
var $oeCalls = this.$('[data-oe-call]');
var oeCalls = _.uniq($oeCalls.map(function () {
return $(this).data('oe-call');
}).get());
if (!oeCalls.length) {
return $.when(this._super.apply(this, arguments));
}
var def = this._rpc({
route: '/website/multi_render',
params: {
ids_or_xml_ids: oeCalls,
},
}).then(function (data) {
_.each(data, function (d, k) {
var $data = $(d).addClass('o_block_' + k);
$oeCalls.filter('[data-oe-call="' + k + '"]').each(function () {
$(this).replaceWith($data.clone());
});
});
});
return $.when(this._super.apply(this, arguments), def);
},
});
websiteRootData.websiteRootRegistry.add(LazyTemplateRenderer, '#wrapwrap');
return LazyTemplateRenderer;
});
@@ -1,87 +0,0 @@
odoo.define('website.share', function (require) {
"use strict";
var ajax = require('web.ajax');
var core = require('web.core');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var _t = core._t;
var qweb = core.qweb;
ajax.loadXML('/website/static/src/xml/website.share.xml', qweb);
var SocialShare = Widget.extend({
template: 'website.social_hover',
/**
:param element: element to bind popover
:param social_list: list of social media strings available from
['facebook','twitter', 'linkedin', 'google-plus']
:param hashtags: string of hashtags for twitter
*/
init: function (parent) {
this._super.apply(this, arguments);
this.element = parent;
if (parent.data('social')) {
this.social_list = (parent.data('social')).split();
} else {
this.social_list = ['facebook','twitter', 'linkedin', 'google-plus'];
}
this.hashtags = parent.data('hashtags') || '';
this.renderElement();
this.bind_events();
},
bind_events: function () {
$('.oe_social_facebook').click($.proxy(this.renderSocial, this, 'facebook'));
$('.oe_social_twitter').click($.proxy(this.renderSocial, this, 'twitter'));
$('.oe_social_linkedin').click($.proxy(this.renderSocial, this, 'linkedin'));
$('.oe_social_google-plus').click($.proxy(this.renderSocial, this, 'google-plus'));
},
renderElement: function () {
this.$el.append(
qweb.render('website.social_hover', {medias: this.social_list}));
//we need to re-render the element on each hover; popover has the nasty habit of not hiding but completely removing its
// code from the page
//so the binding is lost if we simply trigger on hover.
this.element.popover({
'content': this.$el.html(),
'placement': 'bottom',
'container': this.element,
'html': true,
'trigger': 'manual',
'animation': false,
}).popover("show").on("mouseleave", function () {
var self = this;
setTimeout(function () {
if (! $(".popover:hover").length) {
$(self).popover("destroy");
}
}, 200);
});
},
renderSocial: function (social) {
var url = document.URL.split(/[?#]/)[0]; // get current url without query string
var title = document.title.split(" | ")[0]; // get the page title without the company name
var hashtags = ' #'+ document.title.split(" | ")[1].replace(' ','') + ' ' + this.hashtags; // company name without spaces (for hashtag)
var social_network = {
'facebook':'https://www.facebook.com/sharer/sharer.php?u=' + encodeURIComponent(url),
'twitter': 'https://twitter.com/intent/tweet?original_referer=' + encodeURIComponent(url) + '&text=' + encodeURIComponent(title + hashtags + ' - ' + url),
'linkedin': 'https://www.linkedin.com/shareArticle?mini=true&url=' + encodeURIComponent(url) + '&title=' + encodeURIComponent(title),
'google-plus': 'https://plus.google.com/share?url=' + encodeURIComponent(url)
};
if (! _.contains(_.keys(social_network), social)) return;
var window_height = 500, window_width = 500;
window.open(social_network[social], '', 'menubar=no, toolbar=no, resizable=yes, scrollbar=yes, height=' + window_height + ',width=' + window_width);
},
});
// Initialize all social_share links when ready
base.ready().done(function () {
$('.oe_social_share').mouseenter(function () {
new SocialShare($(this));
});
});
return SocialShare;
});
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,232 @@
odoo.define('website.WebsiteRoot.instance', function (require) {
'use strict';
require('web.dom_ready');
var websiteRootData = require('website.WebsiteRoot');
var websiteRoot = new websiteRootData.WebsiteRoot(null);
return websiteRoot.attachTo(document.body).then(function () {
return websiteRoot;
});
});
//==============================================================================
odoo.define('website.WebsiteRoot', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var utils = require('web.utils');
var BodyManager = require('web_editor.BodyManager');
var weContext = require('web_editor.context');
var rootWidget = require('web_editor.root_widget');
var sAnimation = require('website.content.snippets.animation');
require("website.content.zoomodoo");
var _t = core._t;
var websiteRootRegistry = new rootWidget.RootWidgetRegistry();
// Load localizations outside the WebsiteRoot to not wait for DOM ready (but
// wait for them in WebsiteRoot)
var lang = utils.get_cookie('frontend_lang') || weContext.get().lang; // FIXME the cookie value should maybe be in the ctx?
var localeDef = ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
var WebsiteRoot = BodyManager.extend({
events: _.extend({}, BodyManager.prototype.events || {}, {
'click .js_change_lang': '_onLangChangeClick',
'click .js_publish_management .js_publish_btn': '_onPublishBtnClick',
'submit .js_website_submit_form': '_onWebsiteFormSubmit',
}),
custom_events: _.extend({}, BodyManager.prototype.custom_events || {}, {
animation_start_demand: '_onAnimationStartDemand',
}),
/**
* @override
*/
willStart: function () {
// TODO would be even greater to wait for localeDef only when necessary
return $.when(this._super.apply(this, arguments), localeDef);
},
/**
* @override
*/
start: function () {
var defs = [this._super.apply(this, arguments)];
// Animations
defs.push(this._startAnimations());
// Compatibility lang change ?
if (!this.$('.js_change_lang').length) {
var $links = this.$('ul.js_language_selector li a:not([data-oe-id])');
var m = $(_.min($links, function (l) {
return $(l).attr('href').length;
})).attr('href');
$links.each(function () {
var $link = $(this);
var t = $link.attr('href');
var l = (t === m) ? "default" : t.split('/')[1];
$link.data('lang', l).addClass('js_change_lang');
});
}
// Display image thumbnail
this.$(".o_image[data-mimetype^='image']").each(function () {
var $img = $(this);
if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
$img.css('background-image', "url('" + $img.data('src') + "')");
}
});
// Enable magnify on zommable img
this.$('.zoomable img[data-zoom]').zoomOdoo();
// Auto scroll
if (window.location.hash.indexOf("scrollTop=") > -1) {
this.el.scrollTop = +window.location.hash.match(/scrollTop=([0-9]+)/)[1];
}
// Fix for IE:
if ($.fn.placeholder) {
$('input, textarea').placeholder();
}
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* As the WebsiteRoot instance is designed to be unique, the associated
* registry has been instantiated outside of the class and is simply
* returned here.
*
* @override
*/
_getRegistry: function () {
return websiteRootRegistry;
},
/**
* Creates an Animation instance for each DOM element which matches the
* `selector` key of one of the registered animations
* (@see Animation.selector).
*
* @param {boolean} [editableMode=false] - true if the page is in edition mode
* @param {jQuery} [$initTarget]
* only initialize the animations whose `selector` matches the element
* @returns {Deferred}
*/
_startAnimations: function (editableMode, $initTarget) {
var self = this;
editableMode = editableMode || false;
var $wrapwrap = this.$('#wrapwrap');
var defs = _.map(sAnimation.registry, function (Animation) {
var selector = Animation.prototype.selector || '';
var $target = $initTarget ? $initTarget.filter(selector) : $wrapwrap.find(selector);
var defs = _.map($target, function (el) {
var $snippet = $(el);
var animation = $snippet.data('snippet-view');
if (animation) {
animation.destroy();
}
animation = new Animation(self, editableMode);
$snippet.data('snippet-view', animation);
return animation.attachTo($snippet);
});
return $.when.apply($, defs);
});
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the root is notified that the animations have to be
* (re)started.
*
* @private
* @param {OdooEvent} ev
*/
_onAnimationStartDemand: function (ev) {
this._startAnimations(ev.data.editableMode, ev.data.$target)
.done(ev.data.onSuccess)
.fail(ev.data.onFailure);
},
/**
* @todo review
* @private
*/
_onLangChangeClick: function (ev) {
ev.preventDefault();
var $target = $(ev.target);
// retrieve the hash before the redirect
var redirect = {
lang: $target.data('lang'),
url: encodeURIComponent($target.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),
hash: encodeURIComponent(window.location.hash)
};
window.location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect);
},
/**
* @todo review
* @private
*/
_onPublishBtnClick: function (ev) {
ev.preventDefault();
var self = this;
var $data = $(ev.currentTarget).parents(".js_publish_management:first");
this._rpc({
route: $data.data('controller') || '/website/publish',
params: {
id: +$data.data('id'),
object: $data.data('object'),
},
})
.done(function (result) {
$data.toggleClass("css_unpublished css_published");
$data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off');
})
.fail(function (err, data) {
return new Dialog(self, {
title: data.data ? data.data.arguments[0] : "",
$content: $('<div/>', {
html: (data.data ? data.data.arguments[1] : data.statusText)
+ '<br/>'
+ _.str.sprintf(
_t('It might be possible to edit the relevant items or fix the issue in <a href="%s">the classic Odoo interface</a>'),
'/web#return_label=Website&model=' + $data.data('object') + '&id=' + $data.data('id')
),
}),
}).open();
});
},
/**
* @todo review
* @private
*/
_onWebsiteFormSubmit: function (ev) {
var $buttons = $(ev.currentTarget).find('button[type="submit"], a.a-submit');
_.each($buttons, function (btn) {
var $btn = $(btn);
$btn.attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text());
$btn.button('loading');
});
},
});
return {
WebsiteRoot: WebsiteRoot,
websiteRootRegistry: websiteRootRegistry,
};
});
+245 -258
View File
@@ -1,292 +1,279 @@
(function ($) {
/**
This code has been more that widely inspired by easyZoom library.
odoo.define('website.content.zoomodoo', function (require) {
'use strict';
Copyright 2013 Matt Hinchliffe
/**
This code has been more that widely inspired by easyZoom library.
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
Copyright 2013 Matt Hinchliffe
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
**/
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
'use strict';
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
**/
var dw, dh, rw, rh, lx, ly;
var dw, dh, rw, rh, lx, ly;
var defaults = {
var defaults = {
// Attribute to retrieve the zoom image URL from.
linkTag: 'a',
linkAttribute: 'data-zoom-image',
// Attribute to retrieve the zoom image URL from.
linkTag: 'a',
linkAttribute: 'data-zoom-image',
// event to trigger zoom
event: 'click', //or mousenter
// event to trigger zoom
event: 'click', //or mousenter
// Prevent clicks on the zoom image link.
preventClicks: true,
// Prevent clicks on the zoom image link.
preventClicks: true,
// disable on mobile
disabledOnMobile: true,
// disable on mobile
disabledOnMobile: true,
// Callback function to execute before the flyout is displayed.
beforeShow: $.noop,
// Callback function to execute before the flyout is displayed.
beforeShow: $.noop,
// Callback function to execute before the flyout is removed.
beforeHide: $.noop,
// Callback function to execute before the flyout is removed.
beforeHide: $.noop,
// Callback function to execute when the flyout is displayed.
onShow: $.noop,
// Callback function to execute when the flyout is displayed.
onShow: $.noop,
// Callback function to execute when the flyout is removed.
onHide: $.noop,
// Callback function to execute when the flyout is removed.
onHide: $.noop,
// Callback function to execute when the cursor is moved while over the image.
onMove: $.noop
// Callback function to execute when the cursor is moved while over the image.
onMove: $.noop
};
};
/**
* ZoomOdoo
* @constructor
* @param {Object} target
* @param {Object} options (Optional)
*/
function ZoomOdoo(target, options) {
this.$target = $(target);
this.opts = $.extend({}, defaults, options, this.$target.data());
/**
* ZoomOdoo
* @constructor
* @param {Object} target
* @param {Object} options (Optional)
*/
function ZoomOdoo(target, options) {
this.$target = $(target);
this.opts = $.extend({}, defaults, options, this.$target.data());
if (this.isOpen === undefined) {
this._init();
}
if (this.isOpen === undefined) {
this._init();
}
}
/**
* Init
* @private
*/
ZoomOdoo.prototype._init = function() {
if (window.outerWidth > 467 || !this.opts.disabledOnMobile) {
this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target;
this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target;
this.$flyout = $('<div class="zoomodoo-flyout" />');
var $attach = this.$target;
if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) {
$attach = this.$target.parents(this.opts.attach);
}
$attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this));
$attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this));
this.$target.parent().on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this));
if (this.opts.preventClicks) {
this.$target.on('click.zoomodoo', function(e) { e.preventDefault(); });
}
}
};
/**
* Show
* @param {MouseEvent|TouchEvent} e
* @param {Boolean} testMouseOver (Optional)
*/
ZoomOdoo.prototype.show = function(e, testMouseOver) {
var w1, h1, w2, h2;
var self = this;
if (this.opts.beforeShow.call(this) === false) return;
if (!this.isReady) {
return this._loadImage(this.$link.attr(this.opts.linkAttribute), function() {
if (self.isMouseOver || !testMouseOver) {
self.show(e);
}
});
}
/**
* Init
* @private
*/
ZoomOdoo.prototype._init = function () {
if (window.outerWidth > 467 || !this.opts.disabledOnMobile) {
this.$link = this.$target.find(this.opts.linkTag).length && this.$target.find(this.opts.linkTag) || this.$target;
this.$image = this.$target.find('img').length && this.$target.find('img') || this.$target;
this.$flyout = $('<div class="zoomodoo-flyout" />');
var $attach = this.$target;
if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) {
$attach = this.$target.parents(this.opts.attach);
}
$attach.parent().append(this.$flyout);
$attach.parent().on('mousemove.zoomodoo touchmove.zoomodoo', $.proxy(this._onMove, this));
$attach.parent().on('mouseleave.zoomodoo touchend.zoomodoo', $.proxy(this._onLeave, this));
this.$target.parent().on(this.opts.event + '.zoomodoo touchstart.zoomodoo', $.proxy(this._onEnter, this));
w1 = this.$target.width();
h1 = this.$target.height();
w2 = this.$flyout.width();
h2 = this.$flyout.height();
dw = this.$zoom.width() - w2;
dh = this.$zoom.height() - h2;
// For the case where the zoom image is actually smaller than
// the flyout.
if (dw < 0) dw = 0;
if (dh < 0) dh = 0;
rw = dw / w1;
rh = dh / h1;
this.isOpen = true;
this.opts.onShow.call(this);
if (e) {
this._move(e);
if (this.opts.preventClicks) {
this.$target.on('click.zoomodoo', function (e) { e.preventDefault(); });
}
};
}
};
/**
* On enter
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onEnter = function(e) {
var touches = e.originalEvent.touches;
/**
* Show
* @param {MouseEvent|TouchEvent} e
* @param {Boolean} testMouseOver (Optional)
*/
ZoomOdoo.prototype.show = function (e, testMouseOver) {
var w1, h1, w2, h2;
var self = this;
this.isMouseOver = true;
if (!touches || touches.length === 1) {
e.preventDefault();
this.show(e, true);
}
};
if (this.opts.beforeShow.call(this) === false) return;
/**
* On move
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onMove = function(e) {
if (!this.isOpen) return;
e.preventDefault();
this._move(e);
};
/**
* On leave
* @private
*/
ZoomOdoo.prototype._onLeave = function() {
this.isMouseOver = false;
if (this.isOpen) {
this.hide();
}
};
/**
* On load
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onLoad = function(e) {
// IE may fire a load event even on error so test the image dimensions
if (!e.currentTarget.width) return;
this.isReady = true;
this.$flyout.html(this.$zoom);
if (e.data.call) {
e.data();
}
};
/**
* Load image
* @private
* @param {String} href
* @param {Function} callback
*/
ZoomOdoo.prototype._loadImage = function(href, callback) {
var zoom = new Image();
this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this));
zoom.style.position = 'absolute';
zoom.src = href;
};
/**
* Move
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._move = function(e) {
if (e.type.indexOf('touch') === 0) {
var touchlist = e.touches || e.originalEvent.touches;
lx = touchlist[0].pageX;
ly = touchlist[0].pageY;
} else {
lx = e.pageX || lx;
ly = e.pageY || ly;
}
var offset = this.$target.offset();
var pt = ly - offset.top;
var pl = lx - offset.left;
var xt = Math.ceil(pt * rh);
var xl = Math.ceil(pl * rw);
// Close if outside
if (xl < 0 || xt < 0 || xl > dw || xt > dh) {
this.hide();
} else {
var top = xt * -1;
var left = xl * -1;
this.$zoom.css({
top: top,
left: left
});
this.opts.onMove.call(this, top, left);
}
};
/**
* Hide
*/
ZoomOdoo.prototype.hide = function() {
if (!this.isOpen) return;
if (this.opts.beforeHide.call(this) === false) return;
this.$flyout.detach();
this.isOpen = false;
this.opts.onHide.call(this);
};
// jQuery plugin wrapper
$.fn.zoomOdoo = function(options) {
return this.each(function() {
var api = $.data(this, 'zoomOdoo');
if (!api) {
$.data(this, 'zoomOdoo', new ZoomOdoo(this, options));
} else if (api.isOpen === undefined) {
api._init();
if (!this.isReady) {
return this._loadImage(this.$link.attr(this.opts.linkAttribute), function () {
if (self.isMouseOver || !testMouseOver) {
self.show(e);
}
});
};
// AMD and CommonJS module compatibility
if (typeof define === 'function' && define.amd){
define(function() {
return ZoomOdoo;
});
} else if (typeof module !== 'undefined' && module.exports) {
module.exports = ZoomOdoo;
}
})(jQuery);
var $attach = this.$target;
if (this.opts.attach !== undefined && this.$target.parents(this.opts.attach).length) {
$attach = this.$target.parents(this.opts.attach);
}
$attach.parent().append(this.$flyout);
w1 = this.$target.width();
h1 = this.$target.height();
w2 = this.$flyout.width();
h2 = this.$flyout.height();
dw = this.$zoom.width() - w2;
dh = this.$zoom.height() - h2;
// For the case where the zoom image is actually smaller than
// the flyout.
if (dw < 0) dw = 0;
if (dh < 0) dh = 0;
rw = dw / w1;
rh = dh / h1;
this.isOpen = true;
this.opts.onShow.call(this);
if (e) {
this._move(e);
}
};
/**
* On enter
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onEnter = function (e) {
var touches = e.originalEvent.touches;
this.isMouseOver = true;
if (!touches || touches.length === 1) {
e.preventDefault();
this.show(e, true);
}
};
/**
* On move
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onMove = function (e) {
if (!this.isOpen) return;
e.preventDefault();
this._move(e);
};
/**
* On leave
* @private
*/
ZoomOdoo.prototype._onLeave = function () {
this.isMouseOver = false;
if (this.isOpen) {
this.hide();
}
};
/**
* On load
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._onLoad = function (e) {
// IE may fire a load event even on error so test the image dimensions
if (!e.currentTarget.width) return;
this.isReady = true;
this.$flyout.html(this.$zoom);
if (e.data.call) {
e.data();
}
};
/**
* Load image
* @private
* @param {String} href
* @param {Function} callback
*/
ZoomOdoo.prototype._loadImage = function (href, callback) {
var zoom = new Image();
this.$zoom = $(zoom).on('load', callback, $.proxy(this._onLoad, this));
zoom.style.position = 'absolute';
zoom.src = href;
};
/**
* Move
* @private
* @param {Event} e
*/
ZoomOdoo.prototype._move = function (e) {
if (e.type.indexOf('touch') === 0) {
var touchlist = e.touches || e.originalEvent.touches;
lx = touchlist[0].pageX;
ly = touchlist[0].pageY;
} else {
lx = e.pageX || lx;
ly = e.pageY || ly;
}
var offset = this.$target.offset();
var pt = ly - offset.top;
var pl = lx - offset.left;
var xt = Math.ceil(pt * rh);
var xl = Math.ceil(pl * rw);
// Close if outside
if (xl < 0 || xt < 0 || xl > dw || xt > dh) {
this.hide();
} else {
var top = xt * -1;
var left = xl * -1;
this.$zoom.css({
top: top,
left: left
});
this.opts.onMove.call(this, top, left);
}
};
/**
* Hide
*/
ZoomOdoo.prototype.hide = function () {
if (!this.isOpen) return;
if (this.opts.beforeHide.call(this) === false) return;
this.$flyout.detach();
this.isOpen = false;
this.opts.onHide.call(this);
};
// jQuery plugin wrapper
$.fn.zoomOdoo = function (options) {
return this.each(function () {
var api = $.data(this, 'zoomOdoo');
if (!api) {
$.data(this, 'zoomOdoo', new ZoomOdoo(this, options));
} else if (api.isOpen === undefined) {
api._init();
}
});
};
});
+14 -142
View File
@@ -1,151 +1,23 @@
odoo.define('website.editor', function (require) {
'use strict';
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 widget = require('web_editor.widget');
var website = require('website.website');
var qweb = core.qweb;
var _t = core._t;
ajax.loadXML('/website/static/src/xml/website.editor.xml', qweb);
/**
* Object who contains all method and bind for the top bar, the template is create server side.
*/
website.TopBar.include({
events: {
'click [data-action="edit"]': 'edit',
},
start: function () {
$("#wrapwrap").find("[data-oe-model] .oe_structure.oe_empty, [data-oe-model].oe_structure.oe_empty, [data-oe-type=html]:empty")
.filter(".oe_not_editable")
.filter(".oe_no_empty")
.addClass("oe_empty");
if (location.search.indexOf("enable_editor") >= 0 && $('html').attr('lang').match(/en[-_]US/)) {
this.$el.addClass('editing_mode');
this.delayed_hide();
}
return this._super.apply(this, arguments);
},
edit: function () {
this.$('button[data-action=edit]').prop('disabled', true);
this.$el.addClass('editing_mode');
editor.editor_bar = new editor.Class(this);
editor.editor_bar.prependTo(document.body);
$('.o_homepage_editor_welcome_message').remove();
this.delayed_hide();
},
delayed_hide: function () {
_.delay((function () {
this.do_hide();
}).bind(this), 800);
},
});
/* ----- Customize Template ---- */
website.TopBarCustomize = Widget.extend({
events: {
'click > ul a[data-view-id]': 'do_customize',
},
start: function () {
this.view_name = $(document.documentElement).data('view-xmlid');
if (!this.view_name) {
this.$el.hide();
}
if (this.$el.is(".open")) {
this.load_menu();
} else {
this.$el.one("mousedown", "> a.dropdown-toggle", this.load_menu.bind(this));
}
},
load_menu: function () {
var $menu = this.$el.children("ul");
ajax.jsonRpc('/website/get_switchable_related_views', 'call', {
key: this.view_name,
}).then(function (result) {
var current_group = "";
_.each(result, function (item) {
if (current_group !== item.inherit_id[1]) {
current_group = item.inherit_id[1];
$menu.append("<li class=\"dropdown-header\">" + current_group + "</li>");
}
var $li = $('<li/>', {role: 'presentation'})
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
$li.find('input').prop('checked', !!item.active);
$menu.append($li);
});
});
},
do_customize: function (e) {
e.preventDefault();
var view_id = $(e.currentTarget).data('view-id');
return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'ir.ui.view',
method: 'toggle',
args: [[parseInt(view_id, 10)]],
kwargs: {context: base.get_context()}
}).then(function () {
window.location.reload();
});
},
});
website.TopBar.include({
start: function () {
this.customize_menu = new website.TopBarCustomize();
var def = this.customize_menu.attachTo($('#customize-menu'));
return $.when(this._super(), def);
}
});
/* ----- EDITOR: LINK & MEDIA ---- */
var wUtils = require('website.utils');
widget.LinkDialog.include({
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Allows the URL input to propose existing website pages.
*
* @override
*/
bind_data: function () {
this.$( "#o_link_dialog_url_input" ).autocomplete({
source: function (request, response) {
return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'search_pages',
args: [null, request.term],
kwargs: {
limit: 15,
context: base.get_context(),
},
}).then(function (exists) {
var rs=_.map(exists, function (r) {
return r.loc;
});
response(rs);
});
}
});
return this._super();
}
wUtils.autocompleteWithPages(this, this.$('#o_link_dialog_url_input'));
return this._super.apply(this, arguments);
},
});
/**
* Display a welcome message on the homepage when it is empty and that the user is connected.
*/
base.ready().then(function () {
if (location.search.indexOf("enable_editor") < 0 && $(".editor_enable").length === 0) {
var $wrap = $("#wrapwrap.homepage #wrap");
if ($wrap.length && $wrap.html().trim() === "") {
var $welcome_message = $(qweb.render("website.homepage_editor_welcome_message"));
$welcome_message.css("min-height", $wrap.parent("main").height() - ($wrap.outerHeight(true) - $wrap.height()));
$wrap.empty().append($welcome_message);
}
}
});
});
@@ -0,0 +1,59 @@
odoo.define('website.rte.summernote', function (require) {
'use strict';
var core = require('web.core');
require('web_editor.rte.summernote');
var eventHandler = $.summernote.eventHandler;
var renderer = $.summernote.renderer;
var tplIconButton = renderer.getTemplate().iconButton;
var _t = core._t;
var fn_tplPopovers = renderer.tplPopovers;
renderer.tplPopovers = function (lang, options) {
var $popover = $(fn_tplPopovers.call(this, lang, options));
$popover.find('.note-image-popover .btn-group:has([data-value="img-thumbnail"])').append(
tplIconButton('fa fa-object-ungroup', {
title: _t('Transform the picture (click twice to reset transformation)'),
event: 'transform',
}));
return $popover;
};
$.summernote.pluginEvents.transform = function (event, editor, layoutInfo, sorted) {
var $selection = layoutInfo.handle().find('.note-control-selection');
var $image = $($selection.data('target'));
if ($image.data('transfo-destroy')) {
$image.removeData('transfo-destroy');
return;
}
$image.transfo();
var mouseup = function (event) {
$('.note-popover button[data-event="transform"]').toggleClass('active', $image.is('[style*="transform"]'));
};
$(document).on('mouseup', mouseup);
var mousedown = function (event) {
if (!$(event.target).closest('.transfo-container').length) {
$image.transfo('destroy');
$(document).off('mousedown', mousedown).off('mouseup', mouseup);
}
if ($(event.target).closest('.note-popover').length) {
$image.data('transfo-destroy', true).attr('style', ($image.attr('style') || '').replace(/[^;]*transform[\w:]*;?/g, ''));
}
$image.trigger('content_changed');
};
$(document).on('mousedown', mousedown);
};
var fn_boutton_update = eventHandler.modules.popover.button.update;
eventHandler.modules.popover.button.update = function ($container, oStyle) {
fn_boutton_update.call(this, $container, oStyle);
$container.find('button[data-event="transform"]')
.toggleClass('active', $(oStyle.image).is('[style*="transform"]'))
.toggleClass('hidden', !$(oStyle.image).is('img'));
};
});
File diff suppressed because it is too large Load Diff
+367 -249
View File
@@ -1,213 +1,142 @@
odoo.define('website.contentMenu', function (require) {
"use strict";
'use strict';
var core = require('web.core');
var ajax = require('web.ajax');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var editor = require('web_editor.editor');
var Dialog = require('web.Dialog');
var weContext = require('web_editor.context');
var widget = require('web_editor.widget');
var website = require('website.website');
var websiteNavbarData = require('website.navbar');
var _t = core._t;
var qweb = core.qweb;
ajax.loadXML('/website/static/src/xml/website.contentMenu.xml', qweb);
var MenuEntryDialog = widget.LinkDialog.extend({
xmlDependencies: widget.LinkDialog.prototype.xmlDependencies.concat(
['/website/static/src/xml/website.contentMenu.xml']
),
var TopBarContent = Widget.extend({
/**
* @constructor
* @override
*/
init: function (parent, options, editor, data) {
data.text = data.name || '';
data.isNewWindow = data.new_window;
this.data = data;
this.menu_link_options = options.menu_link_options;
return this._super.apply(this, arguments);
},
/**
* @override
*/
start: function () {
var self = this;
// Add page modal + menu content event
this.$el.add($('#o_website_add_page_modal')).on('click', 'a[data-action]', function (e) {
e.preventDefault();
self[$(this).data('action')]();
});
return this._super();
},
edit_menu: function (action_before_reload) {
var context = base.get_context();
var def = $.Deferred();
if ($("[data-content_menu_id]").length) {
var select = new SelectEditMenuDialog();
select.on('save', this, function (root) {
def.resolve(root);
this.$('.o_link_dialog_preview').remove();
this.$('.window-new, .link-style').closest('.form-group').remove();
this.$('label[for="o_link_dialog_label_input"]').text(_t("Menu Label"));
if (this.menu_link_options) { // add menu link option only when adding new menu
this.$('#o_link_dialog_label_input').closest('.form-group').after(qweb.render('website.contentMenu.dialog.edit.link_menu_options'));
this.$('input[name=link_menu_options]').on('change', function () {
self.$('#o_link_dialog_url_input').closest('.form-group').toggle();
});
select.open();
} else {
def.resolve(null);
}
return def.then(function (root_id) {
return ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website.menu',
method: 'get_tree',
args: [context.website_id, root_id],
kwargs: {
context: context
},
}).then(function (menu) {
var dialog = new EditMenuDialog(this, {}, menu).open();
dialog.on("save", null, function () {
$.when(action_before_reload && action_before_reload()).then(function () {
editor.reload();
});
});
return dialog;
});
});
this.$modal.find('.modal-lg').removeClass('modal-lg')
.find('.col-md-8').removeClass('col-md-8').addClass('col-xs-12');
return this._super.apply(this, arguments);
},
new_page: function () {
website.prompt({
id: "editor_new_page",
window_title: _t("New Page"),
input: _t("Page Title"),
init: function () {
var $group = this.$dialog.find("div.form-group");
$group.removeClass("mb0");
var $add = $('<div/>', {'class': 'form-group mb0'})
.append($('<span/>', {'class': 'col-sm-offset-3 col-sm-9 text-left'})
.append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add page in menu")})));
$add.find('input').prop('checked', true);
$group.after($add);
}
}).then(function (val, field, $dialog) {
if (val) {
var url = '/website/add/' + encodeURIComponent(val);
if ($dialog.find('input[type="checkbox"]').is(':checked')) url +="?add_menu=1";
document.location = url;
}
});
},
rename_page: function () {
var self = this;
var context = base.get_context();
self.mo_id = self.getMainObject().id;
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'page_search_dependencies',
args: [self.mo_id],
kwargs: {
context: context
},
}).then(function (deps) {
website.prompt({
id: "editor_rename_page",
window_title: _t("Rename This Page"),
dependencies: deps,
}, 'website.rename_page').then(function (val, field, $dialog) {
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'rename_page',
args: [
self.mo_id,
val,
],
kwargs: {
context: context
},
}).then(function (new_name) {
window.location = "/page/" + encodeURIComponent(new_name);
});
});
});
},
delete_page: function () {
var self = this;
var context = base.get_context();
self.mo_id = self.getMainObject().id;
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'page_search_dependencies',
args: [self.mo_id],
kwargs: {
context: context,
},
}).then(function (deps) {
website.prompt({
id: "editor_delete_page",
window_title: _t("Delete Page"),
dependencies: deps,
init: function () { $('.btn-continue').prop("disabled", true); },
}, 'website.delete_page').then(function (val, field, $dialog) {
if ($dialog.find('input[type="checkbox"]').is(':checked')) {
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website',
method: 'delete_page',
args: [self.mo_id],
kwargs: {
context: context
},
}).then(function () {
window.location = "/";
});
}
});
});
},
getMainObject: function () {
var repr = $('html').data('main-object');
var m = repr.match(/(.+)\((\d+),(.*)\)/);
if (!m) {
return null;
} else {
return {
model: m[1],
id: m[2]|0
};
/**
* @override
*/
save: function () {
var $e = this.$('#o_link_dialog_label_input');
if (!$e.val() || !$e[0].checkValidity()) {
$e.closest('.form-group').addClass('has-error');
$e.focus();
return;
}
}
});
website.TopBar.include({
start: function () {
this.content_menu = new TopBarContent();
var def = this.content_menu.attachTo($('.oe_content_menu'));
return $.when(this._super(), def);
}
if (this.$('input[name=link_menu_options]:checked').val() === 'new_page') {
window.location = '/website/add/' + encodeURIComponent($e.val()) + '?add_menu=1';
return;
}
return this._super.apply(this, arguments);
},
});
var SelectEditMenuDialog = widget.Dialog.extend({
template: 'website.contentMenu.dialog.select',
xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat(
['/website/static/src/xml/website.contentMenu.xml']
),
/**
* @constructor
* @override
*/
init: function (parent, options) {
var self = this;
self.roots = [{id: null, name: _t("Top Menu")}];
$("[data-content_menu_id]").each(function () {
self.roots.push({id: $(this).data("content_menu_id"), name: $(this).attr("name")});
$('[data-content_menu_id]').each(function () {
self.roots.push({id: $(this).data('content_menu_id'), name: $(this).attr('name')});
});
this._super(parent, _.extend({}, {
title: _t("Select a Menu"),
save_text: _t("Continue")
}, options || {}));
},
/**
* @override
*/
save: function () {
this.final_data = parseInt(this.$el.find("select").val() || null);
this.final_data = parseInt(this.$el.find('select').val() || null);
this._super.apply(this, arguments);
}
},
});
var EditMenuDialog = widget.Dialog.extend({
template: 'website.contentMenu.dialog.edit',
xmlDependencies: widget.Dialog.prototype.xmlDependencies.concat(
['/website/static/src/xml/website.contentMenu.xml']
),
events: _.extend({}, widget.Dialog.prototype.events, {
'click a.js_add_menu': 'add_menu',
'click button.js_edit_menu': 'edit_menu',
'click button.js_delete_menu': 'delete_menu',
'click a.js_add_menu': '_onAddMenuButtonClick',
'click button.js_delete_menu': '_onDeleteMenuButtonClick',
'click button.js_edit_menu': '_onEditMenuButtonClick',
}),
init: function (parent, options, menu) {
this.menu = menu;
this.root_menu_id = menu.id;
this.flat = this.flatenize(menu);
this.to_delete = [];
/**
* @constructor
* @override
*/
init: function (parent, options, rootID) {
this._super(parent, _.extend({}, {
title: _t("Edit Menu"),
size: 'medium',
}, options || {}));
this.rootID = rootID;
},
/**
* @override
*/
willStart: function () {
var defs = [this._super.apply(this, arguments)];
var self = this;
var context = weContext.get();
defs.push(this._rpc({
model: 'website.menu',
method: 'get_tree',
args: [context.website_id, this.rootID],
context: context,
}).then(function (menu) {
self.menu = menu;
self.root_menu_id = menu.id;
self.flat = self._flatenize(menu);
self.to_delete = [];
}));
return $.when.apply($, defs);
},
/**
* @override
*/
start: function () {
var r = this._super.apply(this, arguments);
this.$('.oe_menu_editor').nestedSortable({
@@ -225,16 +154,76 @@ var EditMenuDialog = widget.Dialog.extend({
});
return r;
},
flatenize: function (node, dict) {
dict = dict || {};
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
save: function () {
var _super = this._super.bind(this);
var self = this;
dict[node.id] = node;
node.children.forEach(function (child) {
self.flatenize(child, dict);
var new_menu = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0});
var levels = [];
var data = [];
var context = weContext.get();
// Resequence, re-tree and remove useless data
new_menu.forEach(function (menu) {
if (menu.id) {
levels[menu.depth] = (levels[menu.depth] || 0) + 1;
var mobj = self.flat[menu.id];
mobj.sequence = levels[menu.depth];
mobj.parent_id = (menu.parent_id|0) || menu.parent_id || self.root_menu_id;
delete(mobj.children);
data.push(mobj);
}
});
this._rpc({
model: 'website.menu',
method: 'save',
args: [[context.website_id], { data: data, to_delete: self.to_delete }],
context: context,
}).then(function () {
return _super();
});
return dict;
},
add_menu: function () {
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns a mapping id -> menu item containing all the menu items in the
* given menu hierarchy.
*
* @private
* @param {Object} node
* @param {Object} [_dict] internal use: the mapping being built
* @returns {Object}
*/
_flatenize: function (node, _dict) {
_dict = _dict || {};
var self = this;
_dict[node.id] = node;
node.children.forEach(function (child) {
self._flatenize(child, _dict);
});
return _dict;
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the "add menu" button is clicked -> Opens the appropriate
* dialog to edit this new menu.
*
* @private
*/
_onAddMenuButtonClick: function () {
var self = this;
var dialog = new MenuEntryDialog(this, {menu_link_options: true}, undefined, {});
dialog.on('save', this, function (link) {
@@ -253,7 +242,26 @@ var EditMenuDialog = widget.Dialog.extend({
});
dialog.open();
},
edit_menu: function (ev) {
/**
* Called when the "delete menu" button is clicked -> Deletes this menu.
*
* @private
*/
_onDeleteMenuButtonClick: function (ev) {
var $menu = $(ev.currentTarget).closest('[data-menu-id]');
var menuID = $menu.data('menu-id')|0;
if (menuID) {
this.to_delete.push(menuID);
}
$menu.remove();
},
/**
* Called when the "edit menu" button is clicked -> Opens the appropriate
* dialog to edit this menu.
*
* @private
*/
_onEditMenuButtonClick: function (ev) {
var self = this;
var menu_id = $(ev.currentTarget).closest('[data-menu-id]').data('menu-id');
var menu = self.flat[menu_id];
@@ -272,90 +280,200 @@ var EditMenuDialog = widget.Dialog.extend({
});
dialog.open();
} else {
alert("Could not find menu entry");
Dialog.alert(null, "Could not find menu entry");
}
},
delete_menu: function (ev) {
var $menu = $(ev.currentTarget).closest('[data-menu-id]');
var mid = $menu.data('menu-id')|0;
if (mid) {
this.to_delete.push(mid);
}
$menu.remove();
},
save: function () {
var _super = this._super.bind(this);
var self = this;
var new_menu = this.$('.oe_menu_editor').nestedSortable('toArray', {startDepthCount: 0});
var levels = [];
var data = [];
var context = base.get_context();
// Resequence, re-tree and remove useless data
new_menu.forEach(function (menu) {
if (menu.id) {
levels[menu.depth] = (levels[menu.depth] || 0) + 1;
var mobj = self.flat[menu.id];
mobj.sequence = levels[menu.depth];
mobj.parent_id = (menu.parent_id|0) || menu.parent_id || self.root_menu_id;
delete(mobj.children);
data.push(mobj);
}
});
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
model: 'website.menu',
method: 'save',
args: [[context.website_id], { data: data, to_delete: self.to_delete }],
kwargs: {
context: context
},
}).then(function () {
return _super();
});
},
});
var MenuEntryDialog = widget.LinkDialog.extend({
init: function (parent, options, editor, data) {
data.text = data.name || '';
data.isNewWindow = data.new_window;
this.data = data;
this.menu_link_options = options.menu_link_options;
return this._super.apply(this, arguments);
var ContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
xmlDependencies: ['/website/static/src/xml/website.xml'],
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
delete_page: '_deletePage',
edit_menu: '_editMenu',
rename_page: '_renamePage',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Returns information about the page main object.
*
* @private
* @returns {Object} model and id
*/
_getMainObject: function () {
var repr = $('html').data('main-object');
var m = repr.match(/(.+)\((\d+),(.*)\)/);
return {
model: m[1],
id: m[2] | 0,
};
},
start: function () {
/**
* Retrieves the page dependencies for the given object id.
*
* @private
* @param {integer} moID
* @param {Object} context
* @returns {Deferred<Array>}
*/
_getPageDependencies: function (moID, context) {
return this._rpc({
model: 'website',
method: 'page_search_dependencies',
args: [moID],
context: context,
});
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Searches for the page dependencies, inform the user about them then
* delete the page if the user agrees.
*
* @private
* @returns {Deferred}
*/
_deletePage: function () {
var self = this;
this.$(".o_link_dialog_preview").remove();
this.$(".window-new, .link-style").closest(".form-group").remove();
this.$("label[for='o_link_dialog_label_input']").text(_t("Menu Label"));
if (this.menu_link_options) { // add menu link option only when adding new menu
this.$('#o_link_dialog_label_input').closest('.form-group').after(qweb.render('website.contentMenu.dialog.edit.link_menu_options'));
this.$('input[name=link_menu_options]').on('change', function() {
self.$('#o_link_dialog_url_input').closest('.form-group').toggle();
var moID = self._getMainObject().id;
var context = weContext.get();
var def = $.Deferred();
// Search the page dependencies
this._getPageDependencies(moID, context)
.then(function (dependencies) {
// Inform the user about those dependencies and ask him confirmation
var confirmDef = $.Deferred();
Dialog.safeConfirm(self, "", {
title: _t("Delete Page"),
$content: $(qweb.render('website.delete_page', {dependencies: dependencies})),
confirm_callback: confirmDef.resolve.bind(confirmDef),
cancel_callback: def.resolve.bind(self),
});
}
this.$modal.find('.modal-lg').removeClass('modal-lg')
.find('.col-md-8').removeClass('col-md-8').addClass('col-xs-12');
return confirmDef;
}).then(function () {
// Delete the page if the user confirmed
return self._rpc({
model: 'website',
method: 'delete_page',
args: [moID],
context: context,
});
}).then(function () {
// Redirect to homepage as the page is now deleted
window.location.href = '/';
}, def.reject.bind(def));
return this._super.apply(this, arguments);
return def;
},
save: function () {
var $e = this.$('#o_link_dialog_label_input');
if (!$e.val() || !$e[0].checkValidity()) {
$e.closest('.form-group').addClass('has-error');
$e.focus();
return;
/**
* Asks the user which menu to edit if multiple menus exist on the page.
* Then opens the menu edition dialog.
* Then executes the given callback once the edition is saved, to finally
* reload the page.
*
* @private
* @param {function} [beforeReloadCallback]
* @returns {Deferred}
* Unresolved if the menu is edited and saved as the page will be
* reloaded.
* Resolved otherwise.
*/
_editMenu: function (beforeReloadCallback) {
var self = this;
var def = $.Deferred();
// If there is multiple menu on the page, ask the user which one he
// wants to edit
var selectDef = $.Deferred();
if ($('[data-content_menu_id]').length) {
var select = new SelectEditMenuDialog(this);
select.on('save', selectDef, selectDef.resolve);
select.on('cancel', def, def.resolve);
select.open();
} else {
selectDef.resolve(null);
}
if (this.$('input[name=link_menu_options]:checked').val() === 'new_page') {
window.location = '/website/add/' + encodeURIComponent($e.val()) + '?add_menu=1';
return;
}
return this._super.apply(this, arguments);
}
selectDef.then(function (rootID) {
// Open the dialog to show the menu structure and allow its edition
var editDef = $.Deferred();
var dialog = new EditMenuDialog(self, {}, rootID).open();
dialog.on('save', editDef, editDef.resolve);
dialog.on('cancel', def, def.resolve);
return editDef;
}).then(function () {
// Before reloading the page after menu modification, does the
// given action to do.
return beforeReloadCallback && beforeReloadCallback();
}).then(function () {
// Reload the page so that the menu modification are shown
window.location.reload(true);
});
return def;
},
/**
* Asks the user for the new name of the page, then reloads to the new page
* with the correct name if saved.
*
* @private
* @returns {Deferred}
* Unresolved if the page's name is changed and saved as the page
* will be reloaded.
* Resolved otherwise.
*/
_renamePage: function () {
var def = $.Deferred();
var self = this;
var moID = self._getMainObject().id;
var context = weContext.get();
// Search the page dependencies
this._getPageDependencies(moID, context).then(function (dependencies) {
// Ask the user for the new page name and inform him about the dependencies
var renameDef = $.Deferred();
var $content = $(qweb.render('website.rename_page', {dependencies: dependencies}));
Dialog.confirm(self, "", {
title: _t("Rename This Page"),
$content: $content,
confirm_callback: function () {
var $newNameInput = $content.find('input#new_name');
renameDef.resolve($newNameInput.val());
},
cancel_callback: def.resolve.bind(def),
});
return renameDef;
}).then(function (newName) {
// Rename the page with the user's choice
return self._rpc({
model: 'website',
method: 'rename_page',
args: [moID, newName],
context: context,
});
}).then(function (newTechnicalName) {
// Redirects to the new renamed page
window.location.href = ('/page/' + encodeURIComponent(newTechnicalName));
}, def.reject.bind(def));
return def;
},
});
websiteNavbarData.websiteNavbarRegistry.add(ContentMenu, '#content-menu');
return {
'TopBar': TopBarContent,
'EditMenuDialog': EditMenuDialog,
ContentMenu: ContentMenu,
EditMenuDialog: EditMenuDialog,
MenuEntryDialog: MenuEntryDialog,
SelectEditMenuDialog: SelectEditMenuDialog,
};
});
@@ -0,0 +1,181 @@
odoo.define('website.customizeMenu', function (require) {
'use strict';
var core = require('web.core');
var Widget = require('web.Widget');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');
var WebsiteAceEditor = require('website.ace');
var qweb = core.qweb;
var CustomizeMenu = Widget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
events: {
'click > ul a[data-view-id]': '_onCustomizeOptionClick',
},
/**
* @override
*/
start: function () {
var viewName = $(document.documentElement).data('view-xmlid');
if (!viewName) {
_.defer(this.destroy.bind(this));
}
if (this.$el.is('.open')) {
this._loadCustomizeOptions(viewName);
} else {
this.$el.one('mousedown', '> a.dropdown-toggle', this._loadCustomizeOptions.bind(this, viewName));
}
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* Enables/Disables a view customization whose id is given.
*
* @private
* @param {integer} viewID
* @returns {Deferred}
* Unresolved if the customization succeeded as the page will be
* reloaded.
* Rejected otherwise.
*/
_doCustomize: function (viewID) {
return this._rpc({
model: 'ir.ui.view',
method: 'toggle',
args: [[viewID]],
context: weContext.get(),
}).then(function () {
window.location.reload();
return $.Deferred();
});
},
/**
* Loads the information about the views which can be enabled/disabled on
* the current page and shows them as switchable elements in the menu.
*
* @private
* @param {string} viewName - xml id
* @return {Deferred}
*/
_loadCustomizeOptions: function (viewName) {
var $menu = this.$el.children('ul');
return this._rpc({
route: '/website/get_switchable_related_views',
params: {
key: viewName,
},
}).then(function (result) {
var currentGroup = '';
_.each(result, function (item) {
if (currentGroup !== item.inherit_id[1]) {
currentGroup = item.inherit_id[1];
$menu.append('<li class="dropdown-header">' + currentGroup + '</li>');
}
var $li = $('<li/>', {role: 'presentation'})
.append($('<a/>', {href: '#', 'data-view-id': item.id, role: 'menuitem'})
.append(qweb.render('web_editor.components.switch', {id: 'switch-' + item.id, label: item.name})));
$li.find('input').prop('checked', !!item.active);
$menu.append($li);
});
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when a view's related switchable element is clicked -> enable /
* disable the related view.
*
* @private
* @param {Event} ev
*/
_onCustomizeOptionClick: function (ev) {
ev.preventDefault();
var viewID = parseInt($(ev.currentTarget).data('view-id'), 10);
this._doCustomize(viewID);
},
});
var AceEditorMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
edit: '_enterEditMode',
ace: '_launchAce',
}),
/**
* Launches the ace editor automatically when the corresponding hash is in
* the page URL.
*
* @override
*/
start: function () {
var def;
if (window.location.hash.substr(0, WebsiteAceEditor.prototype.hash.length) === WebsiteAceEditor.prototype.hash) {
def = this._launchAce();
}
return $.when(this._super.apply(this, arguments), def);
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* When handling the "edit" website action, the ace editor has to be closed.
*
* @private
*/
_enterEditMode: function () {
if (this.globalEditor) {
this.globalEditor.do_hide();
}
},
/**
* Launches the ace editor to be able to edit the templates and less files
* which are used by the current page.
*
* @private
* @returns {Deferred}
*/
_launchAce: function () {
if (this.globalEditor) {
this.globalEditor.do_show();
return $.when();
} else {
var currentHash = window.location.hash;
var indexOfView = currentHash.indexOf("?res=");
var initialResID = undefined;
if (indexOfView >= 0) {
initialResID = currentHash.substr(indexOfView + ("?res=".length));
var parsedResID = parseInt(initialResID, 10);
if (parsedResID) {
initialResID = parsedResID;
}
}
this.globalEditor = new WebsiteAceEditor(this, $(document.documentElement).data('view-xmlid'), {
initialResID: initialResID,
defaultBundlesRestriction: [
"web.assets_frontend",
"website.assets_frontend",
],
});
return this.globalEditor.appendTo(document.body);
}
},
});
websiteNavbarData.websiteNavbarRegistry.add(CustomizeMenu, '#customize-menu');
websiteNavbarData.websiteNavbarRegistry.add(AceEditorMenu, '#html_editor');
return CustomizeMenu;
});
+102
View File
@@ -0,0 +1,102 @@
odoo.define('website.editMenu', function (require) {
'use strict';
var core = require('web.core');
var weContext = require('web_editor.context');
var editor = require('web_editor.editor');
var websiteNavbarData = require('website.navbar');
/**
* Adds the behavior when clicking on the 'edit' button (+ editor interaction)
*/
var EditPageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
xmlDependencies: ['/website/static/src/xml/website.editor.xml'],
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions, {
edit: '_startEditMode',
}),
custom_events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.custom_events || {}, {
snippet_dropped: '_onSnippetDropped',
}),
/**
* @constructor
*/
init: function () {
this._super.apply(this, arguments);
this._editorAutoStart = (weContext.getExtra().editable && window.location.search.indexOf('enable_editor') >= 0);
},
/**
* Auto-starts the editor if necessary or add the welcome message otherwise.
*
* @override
*/
start: function () {
var def = this._super.apply(this, arguments);
// If we auto start the editor, do not show a welcome message
if (this._editorAutoStart) {
return $.when(def, this._startEditMode());
}
// Check that the page is empty
var $wrap = $('#wrapwrap.homepage #wrap'); // TODO find this element another way
if (!$wrap.length || $wrap.html().trim() !== '') {
return def;
}
// If readonly empty page, show the welcome message
this.$welcomeMessage = $(core.qweb.render('website.homepage_editor_welcome_message'));
this.$welcomeMessage.css('min-height', $wrap.parent('main').height() - ($wrap.outerHeight(true) - $wrap.height()));
$wrap.empty().append(this.$welcomeMessage);
return def;
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Creates an editor instance and appends it to the DOM. Also remove the
* welcome message if necessary.
*
* @private
* @returns {Deferred}
*/
_startEditMode: function () {
var self = this;
return (new (editor.Class)(this)).prependTo(document.body).then(function () {
if (self.$welcomeMessage) {
self.$welcomeMessage.remove();
}
var def = $.Deferred();
self.trigger_up('animation_start_demand', {
editableMode: true,
onSuccess: def.resolve.bind(def),
onFailure: def.reject.bind(def),
});
return def;
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when a snippet is dropped in the page. Notifies the WebsiteRoot
* that is should start the animations for this snippet.
*
* @private
* @param {OdooEvent} ev
*/
_onSnippetDropped: function (ev) {
this.trigger_up('animation_start_demand', {
editableMode: true,
$target: ev.data.$target,
});
},
});
websiteNavbarData.websiteNavbarRegistry.add(EditPageMenu, '#edit-page-menu');
});
@@ -1,49 +1,65 @@
odoo.define('website.mobile', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
var websiteNavbarData = require('website.navbar');
var _t = core._t;
var MobilePreviewDialog = Dialog.extend({
template: 'website.mobile_preview',
init: function () {
this._super.apply(this, arguments);
this.mobile_src = $.param.querystring(window.location.href, 'mobilepreview');
},
/**
* Tweaks the modal so that it appears as a phone and modifies the iframe
* rendering to show more accurate mobile view.
*
* @override
*/
start: function () {
var self = this;
this.$modal.addClass('oe_mobile_preview');
this.$modal.on('click', '.modal-header', function () {
self.$el.toggleClass('o_invert_orientation');
});
this.$iframe = this.$('iframe');
this.$iframe = $('<iframe/>', {
id: 'mobile-viewport',
src: $.param.querystring(window.location.href, 'mobilepreview'),
});
this.$iframe.on('load', function (e) {
self.$iframe.contents().find('body').removeClass('o_connected_user');
self.$iframe.contents().find('#oe_main_menu_navbar, #o_website_add_page_modal').remove();
self.$iframe.contents().find('#oe_main_menu_navbar').remove();
});
this.$iframe.appendTo(this.$el);
return this._super.apply(this, arguments);
},
});
website.TopBar.include({
start: function () {
var self = this;
this.$el.on('click', 'a[data-action=show-mobile-preview]', function () {
new MobilePreviewDialog(self, {
title: _t('Mobile preview') + " <span class='fa fa-refresh'/>",
}).open();
});
return this._super();
var MobileMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
'show-mobile-preview': '_onMobilePreviewClick',
}),
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the mobile action is triggered -> instantiate the mobile
* preview dialog.
*
* @private
*/
_onMobilePreviewClick: function () {
new MobilePreviewDialog(this, {
title: _t('Mobile preview') + ' <span class="fa fa-refresh"/>',
}).open();
},
});
websiteNavbarData.websiteNavbarRegistry.add(MobileMenu, '#mobile-menu');
return {
MobileMenu: MobileMenu,
MobilePreviewDialog: MobilePreviewDialog,
};
});
+180
View File
@@ -0,0 +1,180 @@
odoo.define('website.navbar', function (require) {
'use strict';
var rootWidget = require('web_editor.root_widget');
var concurrency = require('web.concurrency');
var Widget = require('web.Widget');
var websiteRootData = require('website.WebsiteRoot');
var websiteNavbarRegistry = new rootWidget.RootWidgetRegistry();
var WebsiteNavbar = rootWidget.RootWidget.extend({
events: _.extend({}, rootWidget.RootWidget.prototype.events || {}, {
'click [data-action]': '_onActionMenuClick',
'mouseover > ul > li.dropdown:not(.open)': '_onMenuHovered',
'click .o_mobile_menu_toggle': '_onMobileMenuToggleClick',
}),
custom_events: _.extend({}, rootWidget.RootWidget.prototype.custom_events || {}, {
action_demand: '_onActionDemand',
edit_mode: '_onEditMode',
}),
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* As the WebsiteNavbar instance is designed to be unique, the associated
* registry has been instantiated outside of the class and is simply
* returned here.
*
* @override
*/
_getRegistry: function () {
return websiteNavbarRegistry;
},
/**
* Searches for the automatic widget {@see RootWidget} which can handle that
* action.
*
* @private
* @param {string} actionName
* @param {Array} params
* @returns {Deferred}
*/
_handleAction: function (actionName, params, _i) {
var defs = [];
_.each(this._widgets, function (w) {
if (!w.handleAction) {
return;
}
var def = w.handleAction(actionName, params);
if (def !== null) {
defs.push(def);
}
});
if (!defs.length) {
var self = this;
// Handle the case where all action-capable components are not
// instantiated yet (rare) -> retry some times to eventually abort
if (_i > 50) {
console.warn(_.str.sprintf("Action '%s' was not able to be handled.", actionName));
return $.Deferred().reject();
}
return concurrency.delay(100).then(function () {
return self._handleAction(actionName, params, (_i || 0) + 1);
});
}
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when an action menu is clicked -> searches for the automatic
* widget {@see RootWidget} which can handle that action.
*
* @private
* @param {Event} ev
*/
_onActionMenuClick: function (ev) {
var $button = $(ev.currentTarget);
$button.prop('disabled', true);
this._handleAction($button.data('action')).always(function () {
$button.prop('disabled', false);
});
},
/**
* Called when an action is asked to be executed from a child widget ->
* searches for the automatic widget {@see RootWidget} which can handle
* that action.
*/
_onActionDemand: function (ev) {
var def = this._handleAction(ev.data.actionName, ev.data.params);
if (ev.data.onSuccess) {
def.done(ev.data.onSuccess);
}
if (ev.data.onFailure) {
def.fail(ev.data.onFailure);
}
},
/**
* Called in response to edit mode activation -> hides the navbar.
*
* @private
*/
_onEditMode: function () {
var self = this;
this.$el.addClass('editing_mode');
_.delay(function () {
self.do_hide();
}, 800);
},
/**
* Called when a submenu is hovered -> automatically opens it if another
* menu was already opened.
*
* @private
* @param {Event} ev
*/
_onMenuHovered: function (ev) {
var $opened = this.$('> ul > li.dropdown.open');
if ($opened.length) {
$opened.removeClass('open');
$(ev.currentTarget).find('.dropdown-toggle').mousedown().focus().mouseup().click();
}
},
/**
* Called when the mobile menu toggle button is click -> modifies the DOM
* to open the mobile menu.
*
* @private
*/
_onMobileMenuToggleClick: function () {
this.$el.parent().toggleClass('o_mobile_menu_opened');
},
});
var WebsiteNavbarActionWidget = Widget.extend({
/**
* 'Action name' -> 'Handler name' object
*
* Any [data-action="x"] element inside the website navbar will
* automatically trigger an action "x". This action can then be handled by
* any `WebsiteNavbarActionWidget` instance if the action name "x" is
* registered in this `actions` object.
*/
actions: {},
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* Checks if the widget can execute an action whose name is given, with the
* given parameters. If it is the case, execute that action.
*
* @param {string} actionName
* @param {Array} params
* @returns {Deferred|null} action's deferred or null if no action was found
*/
handleAction: function (actionName, params) {
var action = this[this.actions[actionName]];
if (action) {
return $.when(action.apply(this, params || []));
}
return null;
},
});
websiteRootData.websiteRootRegistry.add(WebsiteNavbar, '#oe_main_menu_navbar');
return {
WebsiteNavbar: WebsiteNavbar,
websiteNavbarRegistry: websiteNavbarRegistry,
WebsiteNavbarActionWidget: WebsiteNavbarActionWidget,
};
});
@@ -0,0 +1,81 @@
odoo.define('website.newMenu', function (require) {
'use strict';
var core = require('web.core');
var websiteNavbarData = require('website.navbar');
var wUtils = require('website.utils');
var qweb = core.qweb;
var _t = core._t;
var NewContentMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
xmlDependencies: ['/web_editor/static/src/xml/editor.xml'],
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
new_page: '_createNewPage',
}),
events: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.events || {}, {
'click > a': '_onMenuToggleClick',
}),
/**
* @override
*/
start: function () {
this.$newContentMenuChoices = this.$('#o_new_content_menu_choices');
return this._super.apply(this, arguments);
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Asks the user information about a new page to create, then creates it and
* redirects the user to this new page.
*
* @private
*/
_createNewPage: function () {
wUtils.prompt({
id: 'editor_new_page',
window_title: _t("New Page"),
input: _t("Page Title"),
init: function () {
var $group = this.$dialog.find('div.form-group');
$group.removeClass('mb0');
var $add = $('<div/>', {'class': 'form-group mb0'})
.append($('<span/>', {'class': 'col-sm-offset-3 col-sm-9 text-left'})
.append(qweb.render('web_editor.components.switch', {id: 'switch_addTo_menu', label: _t("Add page in menu")})));
$add.find('input').prop('checked', true);
$group.after($add);
}
}).then(function (val, field, $dialog) {
if (val) {
var url = '/website/add/' + encodeURIComponent(val);
if ($dialog.find('input[type="checkbox"]').is(':checked')) url +='?add_menu=1';
document.location = url;
}
});
},
//--------------------------------------------------------------------------
// Handlers
//--------------------------------------------------------------------------
/**
* Called when the menu's toggle button is clicked -> Opens the menu.
*
* @private
* @param {Event} ev
*/
_onMenuToggleClick: function (ev) {
ev.preventDefault();
this.$newContentMenuChoices.toggleClass('o_hidden');
},
});
websiteNavbarData.websiteNavbarRegistry.add(NewContentMenu, '.o_new_content_menu');
return NewContentMenu;
});
+39 -28
View File
@@ -1,41 +1,52 @@
odoo.define('web.planner.website', function (require) {
odoo.define('website.planner', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var planner = require('web.planner.common');
var rpc = require('web.rpc');
var session = require('web.session');
var website = require('website.website');
var planner = require('web.planner.common');
var Widget = require('web.Widget');
var websiteNavbarData = require('website.navbar');
var qweb = core.qweb;
if (!session.is_system) {
return;
}
var WebsitePlannerLauncher = planner.PlannerLauncher.extend({
/**
* @override
*/
_fetch_planner_data: function () {
return rpc.query({
model: 'web.planner',
method: 'search_read',
args: [[['planner_application', '=', 'planner_website']]],
})
.then((function (planner) {
if (!planner.length) return;
var self = this;
return this._rpc({
model: 'web.planner',
method: 'search_read',
args: [[['planner_application', '=', 'planner_website']]],
}).then(function (planner) {
if (!planner.length) return;
planner[0].data = $.parseJSON(planner[0].data) || {};
this._setup_for_planner(planner[0]);
}).bind(this));
planner[0].data = $.parseJSON(planner[0].data) || {};
self._setup_for_planner(planner[0]);
});
},
});
if (session.is_system) {
website.TopBar.include({
start: function () {
var websitePlannerLauncher = new WebsitePlannerLauncher();
var def = ajax.loadXML('/web_planner/static/src/xml/web_planner.xml', qweb).then((function() {
return websitePlannerLauncher.prependTo(this.$(".o_menu_systray"));
}).bind(this));
return $.when(this._super.apply(this, arguments), def);
},
});
}
var PlannerMenu = Widget.extend({
xmlDependencies: ['/web_planner/static/src/xml/web_planner.xml'],
/**
* Instantiates the planner launcher and adds it to the DOM.
*
* @override
*/
start: function () {
var websitePlannerLauncher = new WebsitePlannerLauncher(this);
return $.when(
this._super.apply(this, arguments),
websitePlannerLauncher.prependTo(this.$el)
);
},
});
websiteNavbarData.websiteNavbarRegistry.add(PlannerMenu, '.o_menu_systray');
return WebsitePlannerLauncher;
});
+72 -84
View File
@@ -2,24 +2,19 @@ odoo.define('website.seo', function (require) {
'use strict';
var core = require('web.core');
var ajax = require('web.ajax');
var Class = require('web.Class');
var Dialog = require('web.Dialog');
var mixins = require('web.mixins');
var rpc = require("web.rpc");
var rpc = require('web.rpc');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');
var _t = core._t;
var qweb = core.qweb;
ajax.loadXML('/website/static/src/xml/website.seo.xml', qweb);
// This replaces \b, because accents(e.g. à, é) are not seen as word boundaries.
// Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b
var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)';
// This replaces \b, because accents(e.g. à, é) are not seen as word boundaries.
// Javascript \b is not unicode aware, and words beginning or ending by accents won't match \b
var WORD_SEPARATORS_REGEX = '([\\u2000-\\u206F\\u2E00-\\u2E7F\'!"#\\$%&\\(\\)\\*\\+,\\-\\.\\/:;<=>\\?¿¡@\\[\\]\\^_`\\{\\|\\}~\\s]+|^|$)';
function analyzeKeyword(htmlPage, keyword) {
return htmlPage.isInTitle(keyword) ? {
@@ -39,9 +34,11 @@ function analyzeKeyword(htmlPage, keyword) {
var Suggestion = Widget.extend({
template: 'website.seo_suggestion',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
events: {
'click .js_seo_suggestion': 'select',
},
init: function (parent, options) {
this.root = options.root;
this.keyword = options.keyword;
@@ -69,6 +66,8 @@ var Suggestion = Widget.extend({
var SuggestionList = Widget.extend({
template: 'website.seo_suggestion_list',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
init: function (parent, options) {
this.root = options.root;
this.language = options.language;
@@ -81,10 +80,13 @@ var SuggestionList = Widget.extend({
refresh: function () {
var self = this;
self.$el.append(_t("Loading..."));
var language = self.language || base.get_context().lang.toLowerCase();
ajax.jsonRpc('/website/seo_suggest', 'call', {
'keywords': self.root,
'lang': language,
var language = self.language || weContext.get().lang.toLowerCase();
this._rpc({
route: '/website/seo_suggest',
params: {
keywords: self.root,
lang: language,
},
}).then(function (keyword_list) {
self.addSuggestions(JSON.parse(keyword_list));
});
@@ -93,9 +95,9 @@ var SuggestionList = Widget.extend({
var self = this;
self.$el.empty();
// TODO Improve algorithm + Ajust based on custom user keywords
var regex = new RegExp(self.root, "gi");
var keywords = _.map(_.uniq(keywords), function (word) {
return word.replace(regex, "").trim();
var regex = new RegExp(self.root, 'gi');
keywords = _.map(_.uniq(keywords), function (word) {
return word.replace(regex, '').trim();
});
// TODO Order properly ?
_.each(keywords, function (keyword) {
@@ -117,10 +119,12 @@ var SuggestionList = Widget.extend({
var Keyword = Widget.extend({
template: 'website.seo_keyword',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
events: {
'click a[data-action=remove-keyword]': 'destroy',
},
maxWordsPerKeyword: 4, // TODO Check
init: function (parent, options) {
this.keyword = options.word;
this.language = options.language;
@@ -150,9 +154,9 @@ var Keyword = Widget.extend({
return this.analyze().description;
},
updateLabel: function () {
var cssClass = "oe_seo_keyword js_seo_keyword " + this.highlight();
this.$(".js_seo_keyword").attr('class', cssClass);
this.$(".js_seo_keyword").attr('title', this.tooltip());
var cssClass = 'oe_seo_keyword js_seo_keyword ' + this.highlight();
this.$('.js_seo_keyword').attr('class', cssClass);
this.$('.js_seo_keyword').attr('title', this.tooltip());
},
destroy: function () {
this.trigger('removed');
@@ -162,7 +166,9 @@ var Keyword = Widget.extend({
var KeywordList = Widget.extend({
template: 'website.seo_list',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
maxKeywords: 10,
init: function (parent, options) {
this.htmlPage = options.page;
this._super(parent);
@@ -192,7 +198,7 @@ var KeywordList = Widget.extend({
add: function (candidate, language) {
var self = this;
// TODO Refine
var word = candidate ? candidate.replace(/[,;.:<>]+/g, " ").replace(/ +/g, " ").trim().toLowerCase() : "";
var word = candidate ? candidate.replace(/[,;.:<>]+/g, ' ').replace(/ +/g, ' ').trim().toLowerCase() : '';
if (word && !self.isFull() && !self.exists(word)) {
var keyword = new Keyword(self, {
word: word,
@@ -216,44 +222,10 @@ var KeywordList = Widget.extend({
},
});
var Image = Widget.extend({
template: 'website.seo_image',
init: function (parent, options) {
this.src = options.src;
this.alt = options.alt;
this._super(parent);
},
});
var ImageList = Widget.extend({
init: function (parent, options) {
this.htmlPage = options.page;
this._super(parent);
},
start: function () {
var self = this;
this.htmlPage.images().each(function (index, image) {
new Image(self, image).appendTo(self.$el);
});
},
images: function () {
var result = [];
this.$('input').each(function () {
var $input = $(this);
result.push({
src: $input.attr('src'),
alt: $input.val(),
});
});
return result;
},
add: function (image) {
new Image(this, image).appendTo(this.$el);
},
});
var Preview = Widget.extend({
template: 'website.seo_preview',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
init: function (parent, options) {
this.title = options.title;
this.url = options.url;
@@ -288,12 +260,12 @@ var HtmlPage = Class.extend(mixins.PropertiesMixin, {
},
keywords: function () {
var $keywords = $('meta[name=keywords]');
var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(",");
var parsed = ($keywords.length > 0) && $keywords.attr('content') && $keywords.attr('content').split(',');
return (parsed && parsed[0]) ? parsed: [];
},
changeKeywords: function (keywords) {
// TODO create tag if missing
$('meta[name=keywords]').attr('content', keywords.join(","));
$('meta[name=keywords]').attr('content', keywords.join(','));
this.trigger('keywords-changed', keywords);
},
headers: function (tag) {
@@ -317,21 +289,23 @@ var HtmlPage = Class.extend(mixins.PropertiesMixin, {
return $('body').children().not('.js_seo_configuration').text();
},
isInBody: function (text) {
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.bodyText());
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.bodyText());
},
isInTitle: function (text) {
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.title());
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.title());
},
isInDescription: function (text) {
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, "gi").test(this.description());
return new RegExp(WORD_SEPARATORS_REGEX+text+WORD_SEPARATORS_REGEX, 'gi').test(this.description());
},
});
var Tip = Widget.extend({
template: 'website.seo_tip',
xmlDependencies: ['/website/static/src/xml/website.seo.xml'],
events: {
'closed.bs.alert': 'destroy',
},
init: function (parent, options) {
this.message = options.message;
// cf. http://getbootstrap.com/components/#alerts
@@ -341,8 +315,11 @@ var Tip = Widget.extend({
},
});
var Configurator = Dialog.extend({
var SeoConfigurator = Dialog.extend({
template: 'website.seo_configuration',
xmlDependencies: Dialog.prototype.xmlDependencies.concat(
['/website/static/src/xml/website.seo.xml']
),
events: {
'keyup input[name=seo_page_keywords]': 'confirmKeyword',
'blur input[name=seo_page_title]': 'titleChanged',
@@ -382,7 +359,7 @@ var Configurator = Dialog.extend({
this.keywordList = new KeywordList(self, { page: this.htmlPage });
this.keywordList.on('list-full', self, function () {
self.$('input[name=seo_page_keywords]').attr({
readonly: "readonly",
readonly: 'readonly',
placeholder: "Remove a keyword first"
});
self.$('button[data-action=add]').prop('disabled', true).addClass('disabled');
@@ -405,15 +382,15 @@ var Configurator = Dialog.extend({
},
getLanguages: function () {
var self = this;
ajax.jsonRpc('/web/dataset/call_kw', 'call', {
this._rpc({
model: 'website',
method: 'get_languages',
args: [[base.get_context().website_id]],
kwargs: {context: base.get_context()}
args: [[weContext.get().website_id]],
context: weContext.get(),
}).then( function (data) {
self.$('#language-box').html(core.qweb.render('Configurator.language_promote', {
'language': data,
'def_lang': base.get_context().lang
'def_lang': weContext.get().lang
}));
});
},
@@ -449,7 +426,7 @@ var Configurator = Dialog.extend({
}
},
confirmKeyword: function (e) {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
this.addKeyword();
}
},
@@ -459,7 +436,7 @@ var Configurator = Dialog.extend({
var keyword = _.isString(word) ? word : $input.val();
var language = $language.val().toLowerCase();
this.keywordList.add(keyword, language);
$input.val("").focus();
$input.val('').focus();
},
update: function () {
var self = this;
@@ -471,7 +448,7 @@ var Configurator = Dialog.extend({
data.website_meta_description = this.htmlPage.description();
}
if (this.canEditKeywords) {
data.website_meta_keywords = this.keywordList.keywords().join(", ");
data.website_meta_keywords = this.keywordList.keywords().join(', ');
}
this.saveMetaData(data).then(function () {
self.close();
@@ -500,7 +477,7 @@ var Configurator = Dialog.extend({
rpc.query({
model: obj.model,
method: 'read',
args: [[obj.id], fields, base.get_context()],
args: [[obj.id], fields, weContext.get()],
}).then(function (data) {
if (data.length) {
var meta = data[0];
@@ -523,7 +500,7 @@ var Configurator = Dialog.extend({
return rpc.query({
model: obj.model,
method: 'write',
args: [[obj.id], data, base.get_context()],
args: [[obj.id], data, weContext.get()],
});
}
},
@@ -565,18 +542,29 @@ var Configurator = Dialog.extend({
},
});
website.TopBar.include({
start: function () {
var self = this;
this.$el.on('click', 'a[data-action=promote-current-page]', function () {
new Configurator(self).open();
});
return this._super();
}
var SeoMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
'promote-current-page': '_promoteCurrentPage',
}),
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Opens the SEO configurator dialog.
*
* @private
*/
_promoteCurrentPage: function () {
new SeoConfigurator(this).open();
},
});
websiteNavbarData.websiteNavbarRegistry.add(SeoMenu, '#promote-menu');
return {
Configurator: Configurator,
SeoConfigurator: SeoConfigurator,
SeoMenu: SeoMenu,
};
});
+48 -75
View File
@@ -1,38 +1,45 @@
odoo.define('website.translator', function (require) {
odoo.define('website.translateMenu', function (require) {
'use strict';
var core = require('web.core');
var ajax = require('web.ajax');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var translate = require('web_editor.translate');
var website = require('website.website');
var local_storage = require('web.local_storage');
var utils = require('web.utils');
var weContext = require('web_editor.context');
var translate = require('web_editor.translate');
var websiteNavbarData = require('website.navbar');
var qweb = core.qweb;
if (!translate.translatable) {
var ctx = weContext.getExtra();
if (!ctx.translatable) {
return;
}
website.TopBar.include({
events: _.extend({}, website.TopBar.prototype.events, {
'click [data-action="edit_master"]': 'edit_master',
'click [data-action="translate"]': 'translate',
var TranslatePageMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
actions: _.extend({}, websiteNavbarData.WebsiteNavbar.prototype.actions || {}, {
edit_master: '_goToMasterPage',
translate: '_startTranslateMode',
}),
translate: function (ev) {
ev.preventDefault();
if (translate.edit_translations) {
translate.instance.edit();
} else {
location.search += '&edit_translations';
}
},
edit_master: function (ev) {
ev.preventDefault();
/**
* @override
*/
start: function () {
var defs = [this._super.apply(this, arguments)];
if (ctx.edit_translations) {
defs.push(this._startTranslateMode());
}
return $.when.apply($, defs);
},
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Redirects the user to the same page but in the original language and in
* edit mode.
*
* @private
* @returns {Deferred}
*/
_goToMasterPage: function () {
var lang = '/' + utils.get_cookie('frontend_lang');
var current = document.createElement('a');
@@ -47,58 +54,24 @@ website.TopBar.include({
link.search += (link.search ? '&' : '?') + 'r=' + encodeURIComponent(current.pathname + current.search + current.hash);
window.location = link.href;
return $.Deferred();
},
});
if (!translate.edit_translations) {
return;
}
ajax.loadXML('/website/static/src/xml/website.translator.xml', qweb);
var nodialog = 'website_translator_nodialog';
var Translate = translate.Class.include({
onTranslateReady: function () {
if(this.gengo_translate) {
this.translation_gengo_display();
/**
* Redirects the user to the same page in translation mode (or start the
* translator is translation mode is already enabled).
*
* @private
* @returns {Deferred}
*/
_startTranslateMode: function () {
if (!ctx.edit_translations) {
window.location.search += '&edit_translations';
return $.Deferred();
}
this._super();
},
edit: function () {
$("#oe_main_menu_navbar").hide();
if (!local_storage.getItem(nodialog)) {
var dialog = new TranslatorDialog();
dialog.appendTo($(document.body));
dialog.on('activate', this, function () {
if (dialog.$('input[name=do_not_show]').prop('checked')) {
local_storage.removeItem(nodialog);
} else {
local_storage.setItem(nodialog, true);
}
dialog.$el.modal('hide');
});
}
return this._super();
},
cancel: function () {
$("#oe_main_menu_navbar").show();
return this._super();
}
});
var TranslatorDialog = Widget.extend({
events: _.extend({}, website.TopBar.prototype.events, {
'hidden.bs.modal': 'destroy',
'click button[data-action=activate]': function (ev) {
this.trigger('activate');
},
}),
template: 'website.TranslatorDialog',
start: function () {
this.$el.modal();
var translator = new (translate.Class)(this, $('#wrapwrap'));
return translator.prependTo(document.body);
},
});
websiteNavbarData.websiteNavbarRegistry.add(TranslatePageMenu, '.o_menu_systray');
});
+71 -70
View File
@@ -1,81 +1,82 @@
odoo.define("website.tour.banner", function (require) {
"use strict";
"use strict";
var core = require("web.core");
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var core = require("web.core");
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var _t = core._t;
var _t = core._t;
tour.register("banner", {
url: "/",
wait_for: base.ready(),
}, [{
trigger: "a[data-action=edit]",
content: _t("<b>Click Edit</b> to start designing your homepage."),
extra_trigger: ".homepage",
position: "bottom",
}, {
trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail",
content: _t("Drag the <i>Cover</i> block and drop it in your page."),
position: "bottom",
run: "drag_and_drop",
}, {
trigger: "#wrapwrap .s_text_block_image_fw h2",
content: _t("<b>Click on a text</b> to start editing it. <i>It's that easy to edit your content!</i>"),
position: "left",
width: 150,
run: "text",
}, {
trigger: ".oe_overlay_options .oe_options",
extra_trigger: "#wrapwrap .s_text_block_image_fw h2:not(:containsExact(\"Headline\"))",
content: _t("Customize any block through this menu. Try to change the background of the banner."),
position: "bottom",
}, {
trigger: "#snippet_structure .oe_snippet:eq(3) .oe_snippet_thumbnail",
content: _t("Drag another block in your page, below the cover."),
position: "bottom",
run: "drag_and_drop",
}, {
trigger: "button[data-action=save]",
extra_trigger: ".oe_overlay_options .oe_options",
content: _t("Publish your page by clicking on the <b>Save</b> button."),
position: "bottom",
}, {
trigger: "a[data-action=show-mobile-preview]",
content: _t("Good Job! You created your first page. Let's check how this page looks like on <b>mobile devices</b>."),
position: "bottom",
}, {
trigger: ".modal-dialog:has(#mobile-viewport) button[data-dismiss=modal]",
content: _t("After having checked how it looks on mobile, <b>close the preview</b>."),
position: "right",
}, {
trigger: "#oe_main_menu_navbar a[data-action=new_page]",
content: _t("<p><b>That's it.</b> Your homepage is live.</p><p>Continue adding more pages to your site or edit this page to make it even more awesome.</p>"),
position: "bottom",
}]);
tour.register("banner", {
url: "/",
wait_for: base.ready(),
}, [{
trigger: "a[data-action=edit]",
content: _t("<b>Click Edit</b> to start designing your homepage."),
extra_trigger: ".homepage",
position: "bottom",
}, {
trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail",
content: _t("Drag the <i>Cover</i> block and drop it in your page."),
position: "bottom",
run: "drag_and_drop",
}, {
trigger: "#wrapwrap .s_text_block_image_fw h2",
content: _t("<b>Click on a text</b> to start editing it. <i>It's that easy to edit your content!</i>"),
position: "left",
width: 150,
run: "text",
}, {
trigger: ".oe_overlay_options .oe_options",
extra_trigger: "#wrapwrap .s_text_block_image_fw h2:not(:containsExact(\"Headline\"))",
content: _t("Customize any block through this menu. Try to change the background of the banner."),
position: "bottom",
}, {
trigger: "#snippet_structure .oe_snippet:eq(3) .oe_snippet_thumbnail",
content: _t("Drag another block in your page, below the cover."),
position: "bottom",
run: "drag_and_drop",
}, {
trigger: "button[data-action=save]",
extra_trigger: ".oe_overlay_options .oe_options",
content: _t("Publish your page by clicking on the <b>Save</b> button."),
position: "bottom",
}, {
trigger: "a[data-action=show-mobile-preview]",
content: _t("Good Job! You created your first page. Let's check how this page looks like on <b>mobile devices</b>."),
position: "bottom",
}, {
trigger: ".modal-dialog:has(#mobile-viewport) button[data-dismiss=modal]",
content: _t("After having checked how it looks on mobile, <b>close the preview</b>."),
position: "right",
}, {
trigger: "#new-content-menu > a",
content: _t("<p><b>That's it.</b> Your homepage is live.</p><p>Continue adding more pages to your site or edit this page to make it even more awesome.</p>"),
position: "bottom",
}]);
});
//==============================================================================
odoo.define("website.tour.contact", function (require) {
"use strict";
"use strict";
var core = require("web.core");
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var _t = core._t;
var core = require("web.core");
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var _t = core._t;
tour.register("contact", {
url: "/page/contactus",
wait_for: base.ready(),
}, [{
trigger: "li#customize-menu",
content: _t("<b>Install a contact form</b> to improve this page."),
extra_trigger: "#o_contact_mail",
position: "bottom",
}, {
trigger: "li#install_apps",
content: _t("<b>Install new apps</b> to get more features. Let's install the <i>'Contact form'</i> app."),
position: "bottom",
}]);
tour.register("contact", {
url: "/page/contactus",
wait_for: base.ready(),
}, [{
trigger: "li#customize-menu",
content: _t("<b>Install a contact form</b> to improve this page."),
extra_trigger: "#o_contact_mail",
position: "bottom",
}, {
trigger: "li#install_apps",
content: _t("<b>Install new apps</b> to get more features. Let's install the <i>'Contact form'</i> app."),
position: "bottom",
}]);
});
+146 -151
View File
@@ -1,159 +1,154 @@
odoo.define('website.tour.rte', function (require) {
'use strict';
var tour = require("web_tour.tour");
var base = require("web_editor.base");
var tour = require('web_tour.tour');
var base = require('web_editor.base');
tour.register('rte_translator', {
test: true,
wait_for: base.ready()
},
[
{
content: "click on Add a language",
trigger: '.js_language_selector a:has(i.fa)',
}, {
content: "select french",
trigger: 'select[name="lang"]',
run: 'text "fr_BE"',
}, {
content: "load french",
trigger: '.modal-footer button:first',
extra_trigger: '.modal select[name="lang"]:propValueContains(fr_BE)',
}, {
content: "go to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
extra_trigger: 'html[lang*="fr"]',
timeout: 60000,
}, {
content: "Open new page menu",
trigger: '#oe_main_menu_navbar a[data-action=new_page]',
extra_trigger: 'a[data-action="edit"]',
}, {
content: "click on new page",
trigger: 'a[data-action="new_page"]',
}, {
content: "insert page name",
trigger: '#editor_new_page input[type="text"]',
run: 'text rte_translator',
}, {
content: "create page",
trigger: 'button.btn-continue',
extra_trigger: 'input[type="text"]:propValue(rte_translator)',
}, {
content: "drop a snippet",
trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail",
run: 'drag_and_drop',
}, {
content: "change content",
trigger: '.oe_overlay_options .oe_options:visible',
run: function () {
$("#wrap p:first").replaceWith('<p>Write one or <font style="background-color: yellow;">two paragraphs <b>describing</b></font> your product or\
<font style="color: rgb(255, 0, 0);">services</font>. To be successful your content needs to be\
useful to your <a href="/999">readers</a>.</p> <input placeholder="test translate placeholder"/>\
<p>&lt;b&gt;&lt;/b&gt; is an HTML&nbsp;tag &amp; is empty</p>');
$("#wrap img").attr("title", "test translate image title");
}
}, {
content: "save",
trigger: 'button[data-action=save]',
extra_trigger: '#wrap p:first b',
}, {
content: "click on french version",
trigger: '.js_language_selector a[data-lang="fr_BE"]',
extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
}, {
content: "translate",
trigger: 'html:not(:has(#wrap p span)) .o_menu_systray a[data-action="translate"]',
}, {
content: "close modal",
trigger: 'button[data-action="activate"]',
}, {
content: "check if translation is activate",
trigger: '[data-oe-translation-id]',
}, {
content: "translate text",
trigger: '#wrap p font:first',
run: function (action_helper) {
action_helper.text('translated french text');
this.$anchor.trigger('keydown');
},
}, {
content: "translate text with special char",
trigger: '#wrap input + p span:first',
run: function (action_helper) {
action_helper.click();
this.$anchor.prepend('&lt;{translated}&gt;').trigger('keydown');
},
}, {
content: "click on input",
trigger: 'input:first',
extra_trigger: '#wrap .o_dirty font:first:contains(translated french text)',
run: 'click',
}, {
content: "translate placeholder",
trigger: 'input:first',
run: 'text test french placeholder',
}, {
content: "close modal",
trigger: 'button.o_save_button',
extra_trigger: '.modal input:propValue(test french placeholder)',
}, {
content: "save translation",
trigger: 'button[data-action=save]',
}, {
content: "check: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
run: function () {}, // it's a check
}, {
content: "check: content with special char is translated",
trigger: "#wrap input + p:contains(<{translated}><b></b> is an HTML\xa0tag & )",
run: function () {}, // it's a check
}, {
content: "check: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
run: function () {}, // it's a check
}, {
content: "return to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))',
}, {
content: "edit english version",
trigger: 'a[data-action=edit]',
extra_trigger: 'body:not(:has(#wrap p font:first:containsExact(paragraphs <b>describing</b>)))',
}, {
content: "select text",
extra_trigger: 'button[data-action=save]',
trigger: '#wrap p',
run: function (action_helper) {
action_helper.click();
var el = this.$anchor[0];
var evt = document.createEvent("MouseEvents");
$.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select();
evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
},
}, {
content: "underline",
trigger: '.note-air-popover button[data-event="underline"]',
}, {
content: "save new change",
trigger: 'button[data-action=save]',
extra_trigger: '#wrap p u',
}, {
content: "return in french",
trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]',
extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
}, {
content: "check bis: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
extra_trigger: 'body:not(:has(button[data-action=save]))',
}, {
content: "check bis: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
},
]
);
wait_for: base.ready(),
}, [{
content: "click on Add a language",
trigger: '.js_language_selector a:has(i.fa)',
}, {
content: "select french",
trigger: 'select[name="lang"]',
run: 'text "fr_BE"',
}, {
content: "load french",
trigger: '.modal-footer button:first',
extra_trigger: '.modal select[name="lang"]:propValueContains(fr_BE)',
}, {
content: "go to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
extra_trigger: 'html[lang*="fr"]',
timeout: 60000,
}, {
content: "Open new page menu",
trigger: '#new-content-menu > a',
extra_trigger: 'a[data-action="edit"]',
}, {
content: "click on new page",
trigger: 'a[data-action="new_page"]',
}, {
content: "insert page name",
trigger: '#editor_new_page input[type="text"]',
run: 'text rte_translator',
}, {
content: "create page",
trigger: 'button.btn-continue',
extra_trigger: 'input[type="text"]:propValue(rte_translator)',
}, {
content: "drop a snippet",
trigger: "#snippet_structure .oe_snippet:eq(1) .oe_snippet_thumbnail",
run: 'drag_and_drop',
}, {
content: "change content",
trigger: '.oe_overlay_options .oe_options:visible',
run: function () {
$("#wrap p:first").replaceWith('<p>Write one or <font style="background-color: yellow;">two paragraphs <b>describing</b></font> your product or\
<font style="color: rgb(255, 0, 0);">services</font>. To be successful your content needs to be\
useful to your <a href="/999">readers</a>.</p> <input placeholder="test translate placeholder"/>\
<p>&lt;b&gt;&lt;/b&gt; is an HTML&nbsp;tag &amp; is empty</p>');
$("#wrap img").attr("title", "test translate image title");
}
}, {
content: "save",
trigger: 'button[data-action=save]',
extra_trigger: '#wrap p:first b',
}, {
content: "click on french version",
trigger: '.js_language_selector a[data-lang="fr_BE"]',
extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
}, {
content: "translate",
trigger: 'html:not(:has(#wrap p span)) .o_menu_systray a[data-action="translate"]',
}, {
content: "close modal",
trigger: '.modal-footer .btn-primary',
}, {
content: "check if translation is activate",
trigger: '[data-oe-translation-id]',
}, {
content: "translate text",
trigger: '#wrap p font:first',
run: function (action_helper) {
action_helper.text('translated french text');
this.$anchor.trigger('keydown');
},
}, {
content: "translate text with special char",
trigger: '#wrap input + p span:first',
run: function (action_helper) {
action_helper.click();
this.$anchor.prepend('&lt;{translated}&gt;').trigger('keydown');
},
}, {
content: "click on input",
trigger: 'input:first',
extra_trigger: '#wrap .o_dirty font:first:contains(translated french text)',
run: 'click',
}, {
content: "translate placeholder",
trigger: 'input:first',
run: 'text test french placeholder',
}, {
content: "close modal",
trigger: 'button.o_save_button',
extra_trigger: '.modal input:propValue(test french placeholder)',
}, {
content: "save translation",
trigger: 'button[data-action=save]',
}, {
content: "check: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
run: function () {}, // it's a check
}, {
content: "check: content with special char is translated",
trigger: "#wrap input + p:contains(<{translated}><b></b> is an HTML\xa0tag & )",
run: function () {}, // it's a check
}, {
content: "check: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
run: function () {}, // it's a check
}, {
content: "return to english version",
trigger: '.js_language_selector a[data-lang="en_US"]',
extra_trigger: 'html[lang*="fr"]:not(:has(#wrap p span))',
}, {
content: "edit english version",
trigger: 'a[data-action=edit]',
extra_trigger: 'body:not(:has(#wrap p font:first:containsExact(paragraphs <b>describing</b>)))',
}, {
content: "select text",
extra_trigger: 'button[data-action=save]',
trigger: '#wrap p',
run: function (action_helper) {
action_helper.click();
var el = this.$anchor[0];
var evt = document.createEvent("MouseEvents");
$.summernote.core.range.create(el.childNodes[2], 6, el.childNodes[2], 13).select();
evt.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, el);
el.dispatchEvent(evt);
},
}, {
content: "underline",
trigger: '.note-air-popover button[data-event="underline"]',
}, {
content: "save new change",
trigger: 'button[data-action=save]',
extra_trigger: '#wrap p u',
}, {
content: "return in french",
trigger: 'html[lang="en-US"] .js_language_selector a[data-lang="fr_BE"]',
extra_trigger: 'html[lang*="en"]:not(:has(button[data-action=save]))',
}, {
content: "check bis: content is translated",
trigger: '#wrap p font:first:contains(translated french text)',
extra_trigger: 'body:not(:has(button[data-action=save]))',
}, {
content: "check bis: placeholder translation",
trigger: 'input[placeholder="test french placeholder"]',
}]);
});
+138
View File
@@ -0,0 +1,138 @@
odoo.define('website.utils', function (require) {
'use strict';
var core = require('web.core');
var weContext = require('web_editor.context');
var qweb = core.qweb;
/**
* Allows the given input to propose existing website pages.
*
* @param {ServicesMixin|Widget} self - an element capable to trigger an RPC
* @param {jQuery} $input
*/
function autocompleteWithPages(self, $input) {
$input.autocomplete({
source: function (request, response) {
return self._rpc({
model: 'website',
method: 'search_pages',
args: [null, request.term],
kwargs: {
limit: 15,
context: weContext.get(),
},
}).then(function (exists) {
var rs = _.map(exists, function (r) {
return r.loc;
});
response(rs);
});
},
});
}
/**
* @deprecated
* @todo create Dialog.prompt instead of this
*/
function prompt(options, _qweb) {
/**
* A bootstrapped version of prompt() albeit asynchronous
* This was built to quickly prompt the user with a single field.
* For anything more complex, please use editor.Dialog class
*
* Usage Ex:
*
* website.prompt("What... is your quest ?").then(function (answer) {
* arthur.reply(answer || "To seek the Holy Grail.");
* });
*
* website.prompt({
* select: "Please choose your destiny",
* init: function () {
* return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ];
* }
* }).then(function (answer) {
* mame_station.loadCharacter(answer);
* });
*
* @param {Object|String} options A set of options used to configure the prompt or the text field name if string
* @param {String} [options.window_title=''] title of the prompt modal
* @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title
* @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title
* @param {String} [options.select] tell the modal to use a select box, the given value will be the field title
* @param {Object} [options.default=''] default value of the field
* @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a deferred]
*/
if (typeof options === 'string') {
options = {
text: options
};
}
if (_.isUndefined(_qweb)) {
_qweb = 'website.prompt';
}
options = _.extend({
window_title: '',
field_name: '',
'default': '', // dict notation for IE<9
init: function () {},
}, options || {});
var type = _.intersection(Object.keys(options), ['input', 'textarea', 'select']);
type = type.length ? type[0] : 'input';
options.field_type = type;
options.field_name = options.field_name || options[type];
var def = $.Deferred();
var dialog = $(qweb.render(_qweb, options)).appendTo('body');
options.$dialog = dialog;
var field = dialog.find(options.field_type).first();
field.val(options['default']); // dict notation for IE<9
field.fillWith = function (data) {
if (field.is('select')) {
var select = field[0];
data.forEach(function (item) {
select.options[select.options.length] = new window.Option(item[1], item[0]);
});
} else {
field.val(data);
}
};
var init = options.init(field, dialog);
$.when(init).then(function (fill) {
if (fill) {
field.fillWith(fill);
}
dialog.modal('show');
field.focus();
dialog.on('click', '.btn-primary', function () {
var backdrop = $('.modal-backdrop');
def.resolve(field.val(), field, dialog);
dialog.modal('hide').remove();
backdrop.remove();
});
});
dialog.on('hidden.bs.modal', function () {
var backdrop = $('.modal-backdrop');
def.reject();
dialog.remove();
backdrop.remove();
});
if (field.is('input[type="text"], select')) {
field.keypress(function (e) {
if (e.which === 13) {
e.preventDefault();
dialog.find('.btn-primary').trigger('click');
}
});
}
return def;
}
return {
autocompleteWithPages: autocompleteWithPages,
prompt: prompt,
};
});
+9 -288
View File
@@ -1,292 +1,13 @@
odoo.define('website.website', function (require) {
"use strict";
'use strict';
var ajax = require('web.ajax');
var Dialog = require("web.Dialog");
var core = require('web.core');
var Widget = require('web.Widget');
var session = require('web.session');
var base = require('web_editor.base');
var utils = require('web.utils');
var weContext = require('web_editor.context');
var qweb = core.qweb;
var _t = core._t;
/* --- Set the browser into the dom for css selectors --- */
var browser;
if ($.browser.webkit) browser = "webkit";
else if ($.browser.safari) browser = "safari";
else if ($.browser.opera) browser = "opera";
else if ($.browser.msie || ($.browser.mozilla && +$.browser.version.replace(/^([0-9]+\.[0-9]+).*/, '\$1') < 20)) browser = "msie";
else if ($.browser.mozilla) browser = "mozilla";
browser += ","+$.browser.version;
if (/android|webos|iphone|ipad|ipod|blackberry|iemobile|opera mini/i.test(navigator.userAgent.toLowerCase())) browser += ",mobile";
document.documentElement.setAttribute('data-browser', browser);
/* --- Add no-flex attr into the dom if flex is not supported for css selectors --- */
var htmlStyle = document.documentElement.style;
var isFlexSupported = (('flexWrap' in htmlStyle) || ('WebkitFlexWrap' in htmlStyle) || ('msFlexWrap' in htmlStyle));
if (!isFlexSupported) {
document.documentElement.setAttribute('data-no-flex', '');
}
/* ----------------------------------------------------
Helpers
---------------------------------------------------- */
var get_context = base.get_context;
base.get_context = base.get_context = function (dict) {
var html = document.documentElement;
return _.extend({
'website_id': html.getAttribute('data-website-id')|0
}, get_context(dict), dict);
};
/* ----------------------------------------------------
Widgets
---------------------------------------------------- */
var prompt = function (options, _qweb) {
/**
* A bootstrapped version of prompt() albeit asynchronous
* This was built to quickly prompt the user with a single field.
* For anything more complex, please use editor.Dialog class
*
* Usage Ex:
*
* website.prompt("What... is your quest ?").then(function (answer) {
* arthur.reply(answer || "To seek the Holy Grail.");
* });
*
* website.prompt({
* select: "Please choose your destiny",
* init: function () {
* return [ [0, "Sub-Zero"], [1, "Robo-Ky"] ];
* }
* }).then(function (answer) {
* mame_station.loadCharacter(answer);
* });
*
* @param {Object|String} options A set of options used to configure the prompt or the text field name if string
* @param {String} [options.window_title=''] title of the prompt modal
* @param {String} [options.input] tell the modal to use an input text field, the given value will be the field title
* @param {String} [options.textarea] tell the modal to use a textarea field, the given value will be the field title
* @param {String} [options.select] tell the modal to use a select box, the given value will be the field title
* @param {Object} [options.default=''] default value of the field
* @param {Function} [options.init] optional function that takes the `field` (enhanced with a fillWith() method) and the `dialog` as parameters [can return a deferred]
*/
if (typeof options === 'string') {
options = {
text: options
};
}
if (_.isUndefined(_qweb)) {
_qweb = 'website.prompt';
}
options = _.extend({
window_title: '',
field_name: '',
'default': '', // dict notation for IE<9
init: function () {},
}, options || {});
var type = _.intersection(Object.keys(options), ['input', 'textarea', 'select']);
type = type.length ? type[0] : 'input';
options.field_type = type;
options.field_name = options.field_name || options[type];
var def = $.Deferred();
var dialog = $(qweb.render(_qweb, options)).appendTo("body");
options.$dialog = dialog;
var field = dialog.find(options.field_type).first();
field.val(options['default']); // dict notation for IE<9
field.fillWith = function (data) {
if (field.is('select')) {
var select = field[0];
data.forEach(function (item) {
select.options[select.options.length] = new Option(item[1], item[0]);
});
} else {
field.val(data);
}
};
var init = options.init(field, dialog);
$.when(init).then(function (fill) {
if (fill) {
field.fillWith(fill);
}
dialog.modal('show');
field.focus();
dialog.on('click', '.btn-primary', function () {
var backdrop = $('.modal-backdrop');
def.resolve(field.val(), field, dialog);
dialog.modal('hide').remove();
backdrop.remove();
});
});
dialog.on('hidden.bs.modal', function () {
var backdrop = $('.modal-backdrop');
def.reject();
dialog.remove();
backdrop.remove();
});
if (field.is('input[type="text"], select')) {
field.keypress(function (e) {
if (e.which == 13) {
e.preventDefault();
dialog.find('.btn-primary').trigger('click');
}
});
}
return def;
};
var error = function (title, message, url) {
return new Dialog(null, {
title: title || "",
$content: $(qweb.render('website.error_dialog', {
message: message || "",
backend_url: url,
})),
}).open();
};
function _add_input(form, name, value) {
var param = document.createElement('input');
param.setAttribute('type', 'hidden');
param.setAttribute('name', name);
param.setAttribute('value', value);
form.appendChild(param);
}
var form = function (url, method, params) {
var form = document.createElement('form');
form.setAttribute('action', url);
form.setAttribute('method', method);
if (core.csrf_token) {
_add_input(form, 'csrf_token', core.csrf_token);
}
_.each(params, function (v, k) {
_add_input(form, k, v);
});
document.body.appendChild(form);
form.submit();
};
ajax.loadXML('/website/static/src/xml/website.xml', qweb);
base.ready().then(function () {
data.topBar = new TopBar();
return data.topBar.attachTo($("#oe_main_menu_navbar"));
});
/* ----- PUBLISHING STUFF ---- */
$(document).on('click', '.js_publish_management .js_publish_btn', function (e) {
e.preventDefault();
var $data = $(this).parents(".js_publish_management:first");
ajax.jsonRpc($data.data('controller') || '/website/publish', 'call', {'id': +$data.data('id'), 'object': $data.data('object')})
.then(function (result) {
$data.toggleClass("css_unpublished css_published");
$data.parents("[data-publish]").attr("data-publish", +result ? 'on' : 'off');
}).fail(function (err, data) {
error(data.data ? data.data.arguments[0] : "", data.data ? data.data.arguments[1] : data.statusText, '/web#return_label=Website&model='+$data.data('object')+'&id='+$data.data('id'));
});
});
if (!$('.js_change_lang').length) {
// in case template is not up to date...
var links = $('ul.js_language_selector li a:not([data-oe-id])');
var m = $(_.min(links, function (l) { return $(l).attr('href').length; })).attr('href');
links.each(function () {
var t = $(this).attr('href');
var l = (t === m) ? "default" : t.split('/')[1];
$(this).data('lang', l).addClass('js_change_lang');
});
}
$(document).on('click', '.js_change_lang', function (e) {
e.preventDefault();
var self = $(this);
// retrieve the hash before the redirect
var redirect = {
lang: self.data('lang'),
url: encodeURIComponent(self.attr('href').replace(/[&?]edit_translations[^&?]+/, '')),
hash: encodeURIComponent(location.hash)
};
location.href = _.str.sprintf("/website/lang/%(lang)s?r=%(url)s%(hash)s", redirect);
});
/* ----- KANBAN WEBSITE ---- */
$('.js_kanban').each(function () {
init_kanban(this);
});
$('.js_website_submit_form').on('submit', function () {
var $buttons = $(this).find('button[type="submit"], a.a-submit');
_.each($buttons, function (btn) {
$(btn).attr('data-loading-text', '<i class="fa fa-spinner fa-spin"></i> ' + $(btn).text()).button('loading');
});
});
_.defer(function () {
if (window.location.hash.indexOf("scrollTop=") > -1) {
window.document.body.scrollTop = +location.hash.match(/scrollTop=([0-9]+)/)[1];
}
});
// display image thumbnail
$(".o_image[data-mimetype^='image']").each(function () {
var $img = $(this);
if (/gif|jpe|jpg|png/.test($img.data('mimetype')) && $img.data('src')) {
$img.css('background-image', "url('" + $img.data('src') + "')");
}
});
/* Load localizations */
var lang = utils.get_cookie('frontend_lang') || $('html').attr('lang') || 'en_US';
ajax.loadJS('/web/webclient/locale/' + lang.replace('-', '_'));
/**
* Object who contains all method and bind for the top bar, the template is create server side.
*/
var TopBar = Widget.extend({
start: function () {
var $collapse = this.$('#oe_applications ul.dropdown-menu').clone()
.attr("id", "oe_applications_collapse")
.attr("class", "nav navbar-nav navbar-left navbar-collapse collapse");
this.$('#oe_applications').before($collapse);
$collapse.wrap('<div class="visible-xs"/>');
this.$('[data-target="#oe_applications"]').attr("data-target", "#oe_applications_collapse");
var self = this;
this.$el.on('mouseover', '> ul > li.dropdown:not(.open)', function (e) {
var $opened = self.$('> ul > li.dropdown.open');
if($opened.length) {
$opened.removeClass('open');
$(e.currentTarget).find('.dropdown-toggle').mousedown().focus().mouseup().click();
}
});
this.$el.on('click', '.o_mobile_menu_toggle', function (ev) {
self.$el.parent().toggleClass('o_mobile_menu_opened');
});
return this._super.apply(this, arguments);
}
});
// enable magnify on zommable img
$('.zoomable img[data-zoom]').zoomOdoo();
var data = {
prompt: prompt,
error: error,
form: form,
TopBar: TopBar,
ready: function () {
console.warn("website.ready is deprecated: Please use require('web_editor.base').ready()");
return base.ready();
},
};
return data;
var weGetContext = weContext.get;
weContext.get = function (context) {
var html = document.documentElement;
return _.extend({
website_id: html.getAttribute('data-website-id') | 0,
}, weGetContext(context), context);
};
});
@@ -1,377 +0,0 @@
odoo.define('website.snippets.editor.gallery', function (require) {
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var base = require('web_editor.base');
var widget = require('web_editor.widget');
var animation = require('web_editor.snippets.animation');
var options = require('web_editor.snippets.options');
var snippet_editor = require('web_editor.snippet.editor');
var _t = core._t;
var qweb = core.qweb;
/*--------------------------------------------------------------------------
Template files to load
--------------------------------------------------------------------------*/
ajax.loadXML('/website/static/src/xml/website.gallery.xml', qweb);
/*--------------------------------------------------------------------------
Gallery Snippet
This is the snippet responsible for configuring the image galleries.
Look at /website/views/snippets.xml for the available options
------------------------------------------------------------------------*/
options.registry.gallery = options.Class.extend({
start : function () {
this._super();
this.bind_change();
var index = Math.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; }));
this.$target.find("img:not([data-index])").each(function () {
index++;
$(this).attr('data-index', index).data('index', index);
});
this.$target.attr("contentEditable", false);
this._temp_mode = this.$el.find("data-mode").data("mode");
this._temp_col = this.$el.find("data-columns").data("columns");
var self = this;
this.$target.on('click', '.o_add_images', function (e) {
e.stopImmediatePropagation();
self.images_add('click');
});
},
drop_and_build_snippet: function () {
var uuid = new Date().getTime();
this.$target.find('.carousel').attr('id', 'slideshow_' + uuid);
this.$target.find('[data-target]').attr('data-target', '#slideshow_' + uuid);
},
styling : function (type, value) {
var classes = this.$el.find('li[data-styling]').map(function () {
return $(this).data('styling');
}).get().join(' ');
this.$target.find("img").removeClass(classes).addClass(value);
},
interval : function (type, value) {
this.$target.find('.carousel:first').attr("data-interval", value);
},
reapply : function () {
var self = this,
modes = [ 'o_nomode', 'o_grid', 'o_masonry', 'o_slideshow' ],
classes = this.$target.attr("class").split(/\s+/);
this.cancel_masonry();
modes.forEach(function (mode) {
if (classes.indexOf(mode) != -1) {
self.mode("reapply", mode.slice(2, Infinity));
return;
}
});
this.$target.attr("contentEditable", false);
},
bind_change: function () {
var self = this;
return this.$target.find("img").off('save').on('save', function (event, img) {
var $parent = $(img).parent();
$parent.addClass("saved_active");
var index = self.$target.find(".item.active").index();
$parent.removeClass("saved_active");
self.$target.find(".carousel:first li[data-target]:eq("+index+")").css("background-image", "url("+$(img).attr("src")+")");
});
},
get_imgs: function () {
var imgs = this.$target.find("img").addClass("img img-responsive mb8 mt8").detach().get();
imgs.sort(function (a,b) { return $(a).data('index')-$(b).data('index'); });
return imgs;
},
mode: function (type, value, $li) {
if (type !== "reapply" && type !== "click" && this._temp_mode === value) {
return;
}
this._temp_mode = value;
this.cancel_masonry();
if (!value) value = 'nomode';
this[value](type);
this.$target.removeClass('o_nomode o_masonry o_grid o_slideshow').addClass("o_"+value);
this.bind_change();
},
replace: function ($content) {
var $container = this.$target.find(".container:first");
$container.empty().append($content);
return $container;
},
nomode : function (type) {
if (type !== "reapply" && !this.$target.attr('class').match(/o_grid|o_masonry|o_slideshow/)) return;
var self = this,
$row = $('<div class="row"></div>'),
$imgs = $(this.get_imgs());
this.replace($row);
$imgs.each(function () {
var $wrap = $(this).wrap('<div>').parent();
var img = this;
if (img.width >= img.height * 2) {
$wrap.addClass("col-md-6");
} else if (img.width > 600) {
$wrap.addClass("col-md-6");
} else {
$wrap.addClass("col-md-3");
}
$row.append($wrap);
});
this.$target.css("height", "");
},
cancel_masonry: function () {
clearTimeout(this.timer);
$(this.masonry_imgs).appendTo(this.$target);
this.masonry_imgs = [];
},
masonry : function (type) {
var self = this,
imgs = this.get_imgs(),
columns = this.get_columns(),
colClass = undefined,
$cols = [];
var $row = $("<div class='row'/>");
this.replace($row);
// if no columns let's default to 3, here we must update the DOM accordingly :'(
if (columns === 0) {
columns = 3;
this.$target.attr("data-columns", columns);
}
colClass = "col-md-"+(12/columns);
// create columns
for (var c = 0; c < columns; c++) {
var $col = $('<div class="col o_snippet_not_selectable"></div>').addClass(colClass);
$row.append($col);
$cols.push($col.get()[0]);
}
imgs.reverse();
$cols = $($cols);
function add() {
self.lowest($cols).append(imgs.pop());
if (imgs.length) self.timer = setTimeout(add, 0);
}
this.masonry_imgs = imgs;
if (imgs.length) add();
this.$target.css("height", "");
},
grid : function (type) {
if (type !== "reapply" && this.$target.hasClass('o_grid')) return;
var self = this,
$imgs = $(this.get_imgs()),
$col, $img,
$row = $('<div class="row"></div>'),
columns = this.get_columns() || 3,
colClass = "col-md-"+(12/columns),
$container = this.replace($row);
$imgs.each(function (index) { // 0 based index
$img = $(this);
$col = $img.wrap('<div>').parent();
self.img_responsive($img);
$col.addClass(colClass);
$col.appendTo($row);
if ( (index+1) % columns === 0) {
$row = $('<div class="row"></div>');
$row.appendTo($container);
}
});
this.$target.css("height", "");
},
slideshow :function (type) {
var imgStyle = this.$el.find('li.active[data-styling]').data('styling') || '';
if (type !== "reapply" && this.$target.hasClass('o_slideshow')) return;
var self = this,
$imgs = $(this.get_imgs()),
urls = $imgs.map(function () { return $(this).attr("src"); } ).get();
var params = {
srcs : urls,
index: 0,
title: "",
interval : this.$target.data("interval") || false,
id: "slideshow_" + new Date().getTime(),
userStyle: imgStyle,
},
$slideshow = $(qweb.render('website.gallery.slideshow', params));
this.replace($slideshow);
this.$target.find(".item img").each(function (index) {
$(this).attr({'data-index': index, 'contenteditable': true});
});
this.$target.css("height", Math.round(window.innerHeight*0.7));
// apply layout animation
this.$target.off('slide.bs.carousel').off('slid.bs.carousel');
this.$target.find('li.fa').off('click');
if (this.$target.data("snippet-view", view)) {
var view = new animation.registry.gallery_slider(this.$target, true);
this.$target.data("snippet-view", view);
} else {
this.$target.data("snippet-view").start(true);
}
},
columns : function (type, value) {
this.$target.attr("data-columns", value);
if (this._temp_col !== value) {
this._temp_col = value;
this.reapply();
}
},
images_add : function (type) {
if(type !== "click") return;
var self = this;
var $container = this.$target.find(".container:first");
var editor = new widget.MediaDialog(null, {select_images: true}, this.$target.closest('.o_editable'), null).open();
var index = Math.max(0, _.max(_.map(this.$target.find("img").get(), function (img) { return img.dataset.index | 0; })) + 1);
editor.on('save', this, function (attachments) {
for (var i = 0 ; i < attachments.length; i++) {
$('<img class="img img-responsive mb8 mt8"/>')
.attr("src", attachments[i].src)
.attr('data-index', index+i)
.data('index', index+i)
.appendTo($container);
}
self.reapply(); // refresh the $target
setTimeout(function () {
self.buildingBlock.make_active(self.$target);
},0);
});
},
images_rm : function (type) {
if(type !== "click") return;
var $addImg = $('<div>', {
class: 'alert alert-info css_editable_mode_display text-center',
});
var $text = $('<span>', {
class: 'o_add_images',
style: 'cursor: pointer;',
text: _t(" Add Images"),
});
var $icon = $('<i>', {
class: ' fa fa-plus-circle',
});
this.replace($addImg.append($icon).append($text));
},
sizing : function () { // done via css, keep it to avoid undefined error
},
/*
* helpers
*/
img_responsive : function (img) {
img.addClass("img img-responsive");
return img;
},
lowest : function ($cols) {
var height = 0, min = -1, col=0, lowest = undefined;
$cols.each(function () {
var $col = $(this);
height = $col.height();
if (min === -1 || height < min) {
min = height;
lowest = $col;
}
});
return lowest;
},
get_columns : function () {
return parseInt(this.$target.attr("data-columns") || 3);
},
clean_for_save: function () {
var self = this;
if (this.$target.hasClass("slideshow")) {
this.$target.removeAttr("style");
}
},
set_active: function () {
this._super();
var classes = _.uniq((this.$target.attr("class").replace(/(^|\s)o_/g, ' ') || '').split(/\s+/));
var $li = this.$el.find('[data-mode]')
.removeClass("active")
.filter('[data-mode="' + classes.join('"], [data-mode="') + '"]').addClass("active");
var mode = this.$el.find('[data-mode].active').data('mode');
var classes = _.uniq((this.$target.find("img:first").attr("class") || '').split(/\s+/));
var $li = this.$el.find('[data-styling]')
.removeClass("active")
.filter('[data-styling="' + classes.join('"], [data-styling="') + '"]').addClass("active");
this.$el.find('li[data-interval]').removeClass("active")
.filter('li[data-interval='+this.$target.find(".carousel:first").attr("data-interval")+']')
.addClass("active");
var interval = this.$target.find('.carousel:first').attr("data-interval");
var $li = this.$el.find('[data-interval]')
.removeClass("active")
.filter('[data-interval=' + interval + ']').addClass("active");
var columns = this.get_columns();
var $li = this.$el.find('[data-columns]')
.removeClass("active")
.filter('[data-columns=' + columns + ']').addClass("active");
this.$el.find('[data-columns]:first, [data-select_class="spc-none"]')
.parent().parent().toggle(["grid", "masonry"].indexOf(mode) !== -1);
this.$el.find('[data-interval]:first').parent().parent().toggle(mode === "slideshow");
},
}); // options.Class.extend
options.registry.gallery_img = options.Class.extend({
position: function (type, value) {
if (type !== "click") return;
var $parent = this.$target.closest("section");
var editor = $parent.data('snippet-editor').styles.gallery;
var imgs = $parent.find('img').get();
imgs.sort(function (a,b) { return $(a).data('index')-$(b).data('index'); });
var index = imgs.indexOf(this.$target[0]);
switch (value) {
case 'first': index = $(imgs.shift()).data('index')-1; break;
case 'prev': index = index <= 1 ? $(imgs.shift()).data('index')-1 : ($(imgs[index-2]).data('index') + $(imgs[index-1]).data('index'))/2; break;
case 'next': index = index >= imgs.length-2 ? $(imgs.pop()).data('index')+1 : ($(imgs[index+2]).data('index') + $(imgs[index+1]).data('index'))/2; break;
case 'last': index = $(imgs.pop()).data('index')+1; break;
}
this.$target.data('index',index);
this.buildingBlock.make_active(false);
setTimeout(function () {
editor.reapply();
},0);
},
on_remove: function () {
var $parent = snippet_editor.globalSelector.closest(this.$target.parent());
_.defer((function () {
this.buildingBlock.make_active($parent);
$parent.data('snippet-editor').styles.gallery.reapply();
}).bind(this));
},
on_focus: function () {
this._super.apply(this, arguments);
if (this._current_src && this._current_src !== this.$target.attr("src")) {
_.defer((function () {
snippet_editor.globalSelector.closest(this.$target.parent()).data('snippet-editor').styles.gallery.reapply();
}).bind(this));
}
this._current_src = this.$target.attr("src");
},
});
});
+52 -83
View File
@@ -1,96 +1,65 @@
odoo.define('website.ace', function (require) {
'use strict';
odoo.define("website.ace", function (require) {
"use strict";
var Widget = require('web.Widget');
var website = require('website.website');
var mixins = require('web.mixins');
var ViewEditor = require('web_editor.ace');
var hash = "#advanced-view-editor";
var Ace = Widget.extend(mixins.ServiceProvider, {
events: {
"click a[data-action=ace]": function (e) {
e.preventDefault();
this.launchAce();
},
},
init: function () {
mixins.ServiceProvider.init.apply(this, arguments);
this._super.apply(this, arguments);
},
start: function () {
if (window.location.hash.substr(0, hash.length) === hash) {
this.launchAce();
}
return this._super.apply(this, arguments);
},
launchAce: function () {
if (this.globalEditor) {
this.globalEditor.do_show();
} else {
var currentHash = window.location.hash;
var indexOfView = currentHash.indexOf("?res=");
var initialResID = undefined;
if (indexOfView >= 0) {
initialResID = currentHash.substr(indexOfView + ("?res=".length));
var parsedResID = parseInt(initialResID, 10);
if (parsedResID) {
initialResID = parsedResID;
}
}
this.globalEditor = new ViewEditor(this, $(document.documentElement).data('view-xmlid'), {
initialResID: initialResID,
defaultBundlesRestriction: [
"web.assets_frontend",
"website.assets_frontend",
],
});
this.globalEditor.appendTo(document.body);
$("a[data-action=edit]").on("click", this.globalEditor.do_hide.bind(this.globalEditor));
}
},
});
var AceEditor = require('web_editor.ace');
/**
* Extend the default view editor so that the URL hash is updated with view id.
* Extends the default view editor so that the URL hash is updated with view ID
*/
ViewEditor = ViewEditor.extend({
displayResource: function () {
this._super.apply(this, arguments);
this._updateHash();
},
saveResources: function () {
return this._super.apply(this, arguments).then((function () {
this._updateHash();
window.location.reload();
}).bind(this));
},
resetResource: function () {
return this._super.apply(this, arguments).then((function () {
window.location.reload();
}).bind(this));
},
var WebsiteAceEditor = AceEditor.extend({
hash: '#advanced-view-editor',
//--------------------------------------------------------------------------
// Public
//--------------------------------------------------------------------------
/**
* @override
*/
do_hide: function () {
this._super.apply(this, arguments);
window.location.hash = "";
},
//--------------------------------------------------------------------------
// Private
//--------------------------------------------------------------------------
/**
* @override
*/
_displayResource: function () {
this._super.apply(this, arguments);
this._updateHash();
},
/**
* @override
*/
_saveResources: function () {
return this._super.apply(this, arguments).then((function () {
this._updateHash();
window.location.reload();
return $.Deferred();
}).bind(this));
},
/**
* @override
*/
_resetResource: function () {
return this._super.apply(this, arguments).then((function () {
window.location.reload();
return $.Deferred();
}).bind(this));
},
/**
* Adds the current resource ID in the URL.
*
* @private
*/
_updateHash: function () {
window.location.hash = hash + "?res=" + this.selectedResource();
},
});
website.TopBar.include({
start: function () {
this.ace = new Ace();
return $.when(
this._super.apply(this, arguments),
this.ace.attachTo(this.$('#html_editor'))
);
window.location.hash = this.hash + "?res=" + this._getSelectedResource();
},
});
return WebsiteAceEditor;
});
+104 -99
View File
@@ -5,28 +5,14 @@ var ajax = require('web.ajax');
var core = require('web.core');
var session = require('web.session');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
var weContext = require('web_editor.context');
var websiteNavbarData = require('website.navbar');
var QWeb = core.qweb;
ajax.jsonRpc('/web/dataset/call', 'call', {
'model': 'ir.ui.view',
'method': 'read_template',
'args': ['website.theme_customize', base.get_context()]
}).done(function (data) {
QWeb.add_template(data);
});
var templateDef = null;
ajax.jsonRpc('/web/dataset/call', 'call', {
'model': 'ir.ui.view',
'method': 'read_template',
'args': ['web_editor.colorpicker', base.get_context()]
}).done(function (data) {
QWeb.add_template(data);
});
var Theme = Widget.extend({
var ThemeCustomizeDialog = Widget.extend({
template: 'website.theme_customize',
events: {
'change input[data-xmlid],input[data-enable],input[data-disable]': 'change_selection',
@@ -40,13 +26,25 @@ var Theme = Widget.extend({
'click .close': 'close',
'click': 'click',
},
willStart: function () {
if (templateDef === null) {
templateDef = this._rpc({
model: 'ir.ui.view',
method: 'read_template',
args: ['website.theme_customize', weContext.get()],
}).then(function (data) {
return QWeb.add_template(data);
});
}
return $.when(this._super.apply(this, arguments), templateDef);
},
start: function () {
var self = this;
this.timer = null;
this.reload = false;
this.flag = false;
this.active_select_tags();
this.$inputs = this.$("input[data-xmlid],input[data-enable],input[data-disable]");
this.$inputs = this.$('input[data-xmlid],input[data-enable],input[data-disable]');
setTimeout(function () {self.$el.addClass('in');}, 0);
this.keydown_escape = function (event) {
if (event.keyCode === 27) {
@@ -76,33 +74,36 @@ var Theme = Widget.extend({
$option.removeAttr('id');
$option.data('input', $input);
$input.on('update', function () {
$option.attr('selected', $(this).prop("checked"));
$option.attr('selected', $(this).prop('checked'));
});
self.$el.append($input);
});
$select.data("value", $options.first());
$options.first().attr("selected", true);
$select.data('value', $options.first());
$options.first().attr('selected', true);
});
$selects.change(function () {
var $option = $(this).find('option:selected');
$(this).data("value").data("input").prop("checked", true).change();
$(this).data("value", $option);
$option.data("input").change();
$(this).data('value').data('input').prop('checked', true).change();
$(this).data('value', $option);
$option.data('input').change();
});
},
load_xml_data: function () {
var self = this;
$('#theme_error').remove();
return ajax.jsonRpc('/website/theme_customize_get', 'call', {
'xml_ids': this.get_xml_ids(this.$inputs)
return this._rpc({
route: '/website/theme_customize_get',
params: {
xml_ids: this.get_xml_ids(this.$inputs),
},
}).done(function (data) {
self.$inputs.filter('[data-xmlid=""]').prop("checked", true).change();
self.$inputs.filter('[data-xmlid=""]').prop('checked', true).change();
self.$inputs.filter('[data-xmlid]:not([data-xmlid=""])').each(function () {
if (!_.difference(self.get_xml_ids($(this)), data[1]).length) {
$(this).prop("checked", false).trigger("change", true);
$(this).prop('checked', false).trigger('change', true);
}
if (!_.difference(self.get_xml_ids($(this)), data[0]).length) {
$(this).prop("checked", true).trigger("change", true);
$(this).prop('checked', true).trigger('change', true);
}
});
}).fail(function (d, error) {
@@ -110,7 +111,7 @@ var Theme = Widget.extend({
});
},
get_inputs: function (string) {
return this.$inputs.filter('#'+string.split(/\s*,\s*/).join(", #"));
return this.$inputs.filter('#'+string.split(/\s*,\s*/).join(', #'));
},
get_xml_ids: function ($inputs) {
var xml_ids = [];
@@ -122,17 +123,20 @@ var Theme = Widget.extend({
return xml_ids;
},
update_style: function (enable, disable, reload) {
if (this.$el.hasClass("loading")) {
if (this.$el.hasClass('loading')) {
return;
}
this.$el.addClass('loading');
if (!reload && session.debug !== "assets") {
if (!reload && session.debug !== 'assets') {
var self = this;
return ajax.jsonRpc('/website/theme_customize', 'call', {
enable: enable,
disable: disable,
get_bundle: true,
return this._rpc({
route: '/website/theme_customize',
params: {
enable: enable,
disable: disable,
get_bundle: true,
},
}).then(function (bundleHTML) {
var $links = $('link[href*=".assets_frontend"]');
var $newLinks = $(bundleHTML).filter('link');
@@ -146,7 +150,7 @@ var Theme = Widget.extend({
});
$newLinks.on('error', function (e) {
linksLoaded.reject();
window.location.hash = "theme=true";
window.location.hash = 'theme=true';
window.location.reload();
});
@@ -159,20 +163,20 @@ var Theme = Widget.extend({
} else {
var href = '/website/theme_customize_reload'+
'?href='+encodeURIComponent(window.location.href)+
'&enable='+encodeURIComponent(enable.join(","))+
'&disable='+encodeURIComponent(disable.join(","));
'&enable='+encodeURIComponent(enable.join(','))+
'&disable='+encodeURIComponent(disable.join(','));
window.location.href = href;
return $.Deferred();
}
},
enable_disable: function ($inputs, enable) {
$inputs.each(function () {
var check = $(this).prop("checked");
var $label = $(this).closest("label");
$(this).prop("checked", enable);
if (enable) $label.addClass("checked");
else $label.removeClass("checked");
if (check != enable) {
var check = $(this).prop('checked');
var $label = $(this).closest('label');
$(this).prop('checked', enable);
if (enable) $label.addClass('checked');
else $label.removeClass('checked');
if (check !== enable) {
$(this).change();
}
});
@@ -181,51 +185,52 @@ var Theme = Widget.extend({
var self = this;
clearTimeout(this.time_select);
if (this.$el.hasClass("loading")) return; // prevent to change selection when css is loading
var $option = $(event.target).is('input') ? $(event.target) : $("input", event.target),
if (this.$el.hasClass('loading')) return; // prevent to change selection when css is loading
var $option = $(event.target).is('input') ? $(event.target) : $('input', event.target),
$options = $option,
checked = $option.prop("checked");
checked = $option.prop('checked');
if (checked) {
if($option.data('enable')) {
var $inputs = this.get_inputs($option.data('enable'));
var $inputs;
if ($option.data('enable')) {
$inputs = this.get_inputs($option.data('enable'));
$options = $options.add($inputs.filter(':not(:checked)'));
this.enable_disable($inputs, true);
}
if($option.data('disable')) {
var $inputs = this.get_inputs($option.data('disable'));
if ($option.data('disable')) {
$inputs = this.get_inputs($option.data('disable'));
$options = $options.add($inputs.filter(':checked'));
this.enable_disable($inputs, false);
}
$option.closest("label").addClass("checked");
$option.closest('label').addClass('checked');
} else {
$option.closest("label").removeClass("checked");
$option.closest('label').removeClass('checked');
}
var $enable = this.$inputs.filter('[data-xmlid]:checked');
$enable.closest("label").addClass("checked");
$enable.closest('label').addClass('checked');
var $disable = this.$inputs.filter('[data-xmlid]:not(:checked)');
$disable.closest("label").removeClass("checked");
$disable.closest('label').removeClass('checked');
var $sets = this.$inputs.filter('input[data-enable]:not([data-xmlid]), input[data-disable]:not([data-xmlid])');
$sets.each(function () {
var $set = $(this);
var checked = true;
if ($set.data("enable")) {
self.get_inputs($(this).data("enable")).each(function () {
if (!$(this).prop("checked")) checked = false;
if ($set.data('enable')) {
self.get_inputs($(this).data('enable')).each(function () {
if (!$(this).prop('checked')) checked = false;
});
}
if ($set.data("disable")) {
self.get_inputs($(this).data("disable")).each(function () {
if ($(this).prop("checked")) checked = false;
if ($set.data('disable')) {
self.get_inputs($(this).data('disable')).each(function () {
if ($(this).prop('checked')) checked = false;
});
}
if (checked) {
$set.prop("checked", true).closest("label").addClass("checked");
$set.prop('checked', true).closest('label').addClass('checked');
} else {
$set.prop("checked", false).closest("label").removeClass("checked");
$set.prop('checked', false).closest('label').removeClass('checked');
}
$set.trigger('update');
});
@@ -255,7 +260,7 @@ var Theme = Widget.extend({
clearTimeout(this.time_select);
},
click: function (event) {
if (!$(event.target).closest("#theme_customize_modal > *").length) {
if (!$(event.target).closest('#theme_customize_modal > *').length) {
this.close();
}
},
@@ -270,42 +275,42 @@ var Theme = Widget.extend({
}
});
function themeError(message) {
var _t = core._t;
var ThemeCustomizeMenu = websiteNavbarData.WebsiteNavbarActionWidget.extend({
actions: _.extend({}, websiteNavbarData.WebsiteNavbarActionWidget.prototype.actions || {}, {
customize_theme: '_openThemeCustomizeDialog',
}),
if (message.indexOf('lessc')) {
message = '<span class="text-muted">' + message + "</span><br/><br/>" + _t("Please install or update node-less");
}
website.error(_t("Theme Error"), message);
}
function theme_customize() {
if (Theme.open && !Theme.open.isDestroyed()) return;
Theme.open = new Theme();
Theme.open.appendTo("body");
var error = window.getComputedStyle(document.body, ':before').getPropertyValue('content');
if (error && error !== 'none') {
themeError(eval(error));
}
}
website.TopBar.include({
/**
* Automatically opens the theme customization dialog if the corresponding
* hash is in the page URL.
*
* @override
*/
start: function () {
var self = this;
base.ready().then(function () {
self.$el.on('click', "#theme_customize a", theme_customize);
if ((window.location.hash || "").indexOf("theme=true") !== -1) {
theme_customize();
window.location.hash = "";
}
});
var def;
if ((window.location.hash || '').indexOf('theme=true') > 0) {
def = this._openThemeCustomizeDialog();
window.location.hash = '';
}
return $.when(this._super.apply(this, arguments), def);
},
return this._super();
}
//--------------------------------------------------------------------------
// Actions
//--------------------------------------------------------------------------
/**
* Instantiates and opens the theme customization dialog.
*
* @private
* @returns {Deferred}
*/
_openThemeCustomizeDialog: function () {
return new ThemeCustomizeDialog(this).appendTo(document.body);
},
});
return Theme;
websiteNavbarData.websiteNavbarRegistry.add(ThemeCustomizeMenu, '#theme_customize');
return ThemeCustomizeDialog;
});
@@ -32,7 +32,6 @@
}
}
// MODALS
body .modal {
&.o_technical_modal {
@@ -158,75 +157,6 @@ body .modal {
}
}
// ADD NEW PAGE MODAL
&#o_website_add_page_modal {
font-family: @o-we-font-family;
&.in {
.o-flex-display()!important;
.o-justify-content(center);
.o-align-items(center);
}
.modal-dialog {
width: auto;
max-width: 60%;
max-height: 100%;
ul {
.o-flex-display();
.o-flex-flow(row, wrap);
.o-justify-content(space-around);
margin: 0;
padding: 0;
list-style: none;
li {
display: block;
opacity: 0;
.animation(fadeInDownSmall 1s forwards);
margin: 2em 32px;
padding: 0 15px;
a {
display: block;
font-size: 34px;
margin: auto;
text-align: center;
i {
width: 110px;
height: 110px;
border: 3px solid lighten(#2C2C36, 10%);
border-radius: 100%;
line-height: 104px;
background-color: #2C2C36;
color: white;
.o-transition(all, 0.5s, cubic-bezier(0.19, 1, 0.22, 1));
}
p {
color: white;
margin-top: 0.7em;
font-size: 0.5em;
}
&:hover, &:focus {
text-decoration: none;
i {
border-color: #1cc1a9;
box-shadow: 0 0 10px rgba(28, 193, 169, 0.46);
}
}
}
}
}
}
~ .modal-backdrop {
opacity: 0.8;
}
}
// TOP MENU EDITOR
.oe_menu_editor {
ul {
@@ -246,7 +176,7 @@ body .modal {
&.oe_seo_configuration {
#language-box {
padding-right: 25px;
background-color:white;
background-color: white;
margin-left: -1px;
}
@@ -395,6 +325,77 @@ body .modal {
}
}
// ADD NEW PAGE MODAL
#o_new_content_menu_choices {
.o-position-absolute(@odoo-navbar-height, 0, 0, 0);
position: fixed;
.o-flex-display();
.o-justify-content(center);
.o-align-items(center);
font-family: @o-we-font-family;
&::before {
content: " ";
.o-position-absolute(0, 0, 0, 0);
z-index: -1;
pointer-events: none;
background-color: black;
opacity: 0.8;
}
> ul {
.o-flex-display();
.o-flex-flow(row, wrap);
.o-justify-content(space-around);
width: auto;
max-width: 60%;
max-height: 100%;
margin: 0;
padding: 0;
list-style: none;
li {
display: block;
opacity: 0;
.animation(fadeInDownSmall 1s forwards);
margin: 2em 32px;
padding: 0 15px;
a {
display: block;
font-size: 34px;
margin: auto;
text-align: center;
i {
width: 110px;
height: 110px;
border: 3px solid lighten(#2C2C36, 10%);
border-radius: 100%;
line-height: 104px;
background-color: #2C2C36;
color: white;
.o-transition(all, 0.5s, cubic-bezier(0.19, 1, 0.22, 1));
}
p {
color: white;
margin-top: 0.7em;
font-size: 0.5em;
}
&:hover, &:focus {
text-decoration: none;
i {
border-color: #1cc1a9;
box-shadow: 0 0 10px rgba(28, 193, 169, 0.46);
}
}
}
}
}
}
// LOGIN FORM
.oe_login_form, .oe_signup_form, .oe_reset_password_form {
max-width: 300px;
@@ -1,3 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="website.contentMenu.dialog.submenu">
<li t-att-data-menu-id="submenu.id">
@@ -1,58 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<!--
=======================================================================
Gallery Dialog
This template is shown when creating a gallery, allowing users to upload
images.
=======================================================================
-->
<t t-name="website.gallery.dialog.upload">
<div class="modal o_technical_modal fade" >
<div class="modal-dialog" style="max-width: 50%;">
<div class="modal-content">
<div class="modal-header">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true"><![CDATA[&times;]]></button>
<h4 class="modal-title">Add images</h4>
</div>
<div class="modal-body">
<form action="/web_editor/attachment/add" method="post" enctype="multipart/form-data" >
<input type="hidden" name="csrf_token" t-att-value="request.csrf_token()"/>
<div class="form-group">
<input name="upload" type="file" value="Choose images" multiple="multiple" accept="image/*"/>
</div>
<div class="form-group hidden">
<input type="submit" value="Upload images" class="btn btn-default" />
</div>
</form>
<div class="text-center"><span class="fa fa-refresh fa-5x fa-spin hidden"></span></div>
<div class="alert alert-success" role="alert">Upload successful.</div>
<div class="alert alert-danger" role="alert">Upload failed, some images might not have been uploaded. Check your network connectivity.</div>
</div>
</div>
</div>
</div>
</t>
<!--
<!--
========================================================================
Gallery Slideshow
This template is used to display a slideshow of images inside a
This template is used to display a slideshow of images inside a
bootstrap carousel.
========================================================================
-->
<t t-name="website.gallery.slideshow">
<div t-attf-id="#{id}" class="carousel slide" data-ride="carousel" t-attf-data-interval="#{interval}" style="margin: 0 12px;">
<div class="carousel-inner container" style="padding: 0;">
<t t-foreach="srcs" t-as="src">
<div t-attf-class="item #{src_index == index and 'active' or ''}">
<img t-attf-class="img #{userStyle}" t-att-src="src"/>
<img t-attf-class="img img-responsive #{userStyle}" t-att-src="src"/>
</div>
</t>
</div>
@@ -64,32 +26,24 @@
</t>
<li class="fa fa-chevron-right o_indicators_right" style="overflow: hidden; padding: 12px; border: 0;"></li>
</ul>
<a class="carousel-control left" t-attf-href="##{id}" data-slide="prev"><span class="fa fa-chevron-left" /></a>
<a class="carousel-control left" t-attf-href="##{id}" data-slide="prev"><span class="fa fa-chevron-left" /></a>
<a class="carousel-control right" t-attf-href="##{id}" data-slide="next"><span class="fa fa-chevron-right" /></a>
</div> <!-- carousel-id -->
<script type="text/javascript">
(function () {
$(".slideshow").css("height", Math.round(window.innerHeight*0.7));
$(window).on("resize", function () { $(".slideshow").css("height", Math.round(window.innerHeight*0.7)); });
})();
</script>
</div>
</t>
<!--
========================================================================
Gallery Slideshow LightBox
This template is used to display a lightbox with a slideshow.
This template wraps website.gallery.slideshow in a bootstrap modal
dialog.
========================================================================
-->
<t t-name="website.gallery.slideshow.lightbox">
<div class="modal o_technical_modal fade" aria-labbelledby="Image Gallery Dialog" aria-hidden="true">
<div class="modal-dialog modal-lg" role="Picture Gallery"
t-attf-style="min-width: #{dim.min_width}px ; min-height: #{dim.min_height}px ; max-width: #{dim.max_width}px ; max-height: #{dim.max_height}px ; height: #{dim.height}px ;">
<div class="modal-content">
@@ -98,7 +52,7 @@
<t t-call="website.gallery.slideshow"></t>
</div>
</div>
</div>
</div>
</div>
</t>
@@ -1,10 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="website.TopBar.menu.promote">
<li><a data-action="promote-current-page" href="#"><span title="Promote page on the web">Promote</span></a></li>
</t>
<t t-name="Configurator.language_promote">
<t t-foreach="language" t-as="lang">
<option t-att-value="lang[0]" t-att-selected="lang[0] == def_lang ? 'selected' : null"><t t-esc="lang[1]" /></option>
@@ -118,13 +113,6 @@
</li>
</t>
<t t-name="website.seo_image">
<div class="col-sm-6 col-md-3 oe_seo_image_preview">
<img t-att-src="widget.src" class="img-rounded thumbnail"/>
<input type="text" class="form-control" t-att-src="widget.src" t-att-value="widget.alt" placeholder="Description..."/>
</div>
</t>
<t t-name="website.seo_preview">
<li class="oe_seo_preview_g">
<div class="rc">
@@ -136,5 +124,4 @@
</div>
</li>
</t>
</templates>
@@ -1,52 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<templates id="template" xml:space="preserve">
<t t-name="website.TranslatorAdditionalButtons">
<li><a class="btn btn-link" data-action="edit_master" href="#"> or Edit Master</a></li>
</t>
<t t-name="website.TranslatorDialog">
<div class="modal o_technical_modal fade oe_website_translator" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button title="Close" type="button" class="close" data-dismiss="modal">×</button>
<h2 class="modal-title">Translate this page</h2>
</div>
<div class="modal-body">
<section>
<p>
You are about to enter the translation mode.
</p>
<p>
Here are the visuals used to help you translate efficiently:
<ul class="oe_translate_examples">
<li data-oe-translation-state="to_translate">
Content to translate
</li>
<li data-oe-translation-state="translated">
Translated content
</li>
</ul>
</p>
<p>
In this mode, you can only translate texts. To
change the structure of the page, you must edit the
master page. Each modification on the master page
is automatically applied to all translated
versions.
</p>
</section>
</div>
<div class="modal-footer">
<div class="pull-left">
<input type="checkbox" name="do_not_show"/>
Do not show this dialog later.
</div>
<button type="button" data-action="activate" class="btn btn-primary">Ok</button>
</div>
</div>
</div>
</div>
</t>
</templates>
+59 -99
View File
@@ -1,117 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="website.mobile_preview">
<iframe id="mobile-viewport" t-att-src="widget.mobile_src"/>
</div>
<div t-name="website.error_dialog">
<section t-if="message">
<t t-raw="message"/>
</section>
<section class="mt32" t-if="backend_url">
<p>The web site has encountered an error.</p>
<p>It might be possible to edit the relevant items or fix the issue in <a t-attf-href="#{backend_url}"> the classic Odoo interface</a>.</p>
</section>
</div>
<t t-name="website.prompt">
<t t-call="website.modal_base">
<form class="form-horizontal" role="form" t-att-id="id">
<div class="form-group mb0">
<label for="page-name" class="col-sm-3 control-label">
<t t-esc="field_name"/>:
</label>
<div class="col-sm-8">
<input t-if="field_type == 'input'" type="text" class="form-control" required="required"/>
<textarea t-if="field_type == 'textarea'" class="form-control" required="required" rows="5"></textarea>
<select t-if="field_type == 'select'" class="form-control"></select>
</div>
</div>
</form>
</t>
</t>
<t t-name="website.modal_base">
<div class="modal o_technical_modal " tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal o_technical_modal" tabindex="-1" role="dialog" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<t t-call="website.modal_header" />
<div class="modal-body">
<t t-raw="0"/>
<div class="modal-header" t-if="window_title">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 class="modal-title"><t t-esc="window_title"/></h3>
</div>
<div class="modal-body">
<form class="form-horizontal" role="form" t-att-id="id">
<div class="form-group mb0">
<label for="page-name" class="col-sm-3 control-label">
<t t-esc="field_name"/>:
</label>
<div class="col-sm-8">
<input t-if="field_type == 'input'" type="text" class="form-control" required="required"/>
<textarea t-if="field_type == 'textarea'" class="form-control" required="required" rows="5"></textarea>
<select t-if="field_type == 'select'" class="form-control"></select>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary btn-continue">Continue</button>
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button>
</div>
<t t-call="website.modal_footer" />
</div>
</div>
</div>
</t>
<t t-name="website.modal_header">
<div class="modal-header" t-if="window_title">
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
<h3 class="modal-title"><t t-esc="window_title"/></h3>
</div>
</t>
<t t-name="website.modal_footer">
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-primary btn-continue">Continue</button>
<button type="button" class="btn btn-sm btn-default" data-dismiss="modal" aria-hidden="true">Cancel</button>
</div>
</t>
<t t-name="website.dependencies">
<div t-if="_.keys(dependencies).length > 0" class="panel panel-warning">
<div class="panel-heading"><strong>Warning!</strong> Don't forget to update all links referring to this page:</div>
<t t-set='tid' t-value='0'/>
<div class="panel-body">
<div t-foreach="_.keys(dependencies)" t-as="type">
<a class="collapsed" data-toggle="collapse" t-att-href="'#collapseDependencies'+tid" aria-expanded="false" t-att-aria-controls="'collapseDependencies'+tid">
<i class="fa fa-caret-right"/>
<b><t t-esc="type"/></b>:&amp;nbsp;
<span class="text-muted"><t t-esc="dependencies[type].length"/> found<span t-if="dependencies[type].length > 1">s</span></span>
</a>
<div t-att-id="'collapseDependencies'+tid" class="collapse" aria-expanded="false">
<ul>
<li t-foreach="dependencies[type]" t-as="error">
<a t-if="!_.contains(['', '#', false,], error.link)" t-att-href="error.link">
<t t-raw="error.text"/>
</a>
<t t-if="_.contains(['', '#', false,], error.link)" t-raw="error.text"/>
</li>
</ul>
</div>
<t t-set='tid' t-value='tid+1'/>
<p class="text-warning">Don't forget to update all links referring to this page.</p>
<t t-if="dependencies and _.keys(dependencies).length">
<p class="text-warning">We found these ones:</p>
<div t-foreach="dependencies" t-as="type" class="mb16">
<a class="collapsed fa fa-caret-right" data-toggle="collapse" t-attf-href="#collapseDependencies#{type_index}" aria-expanded="false" t-attf-aria-controls="collapseDependencies#{type_index}">
<t t-esc="type"/>&amp;nbsp;
<span class="text-muted"><t t-esc="type_value.length"/> found(s)</span>
</a>
<div t-attf-id="collapseDependencies#{type_index}" class="collapse" aria-expanded="false">
<ul>
<li t-foreach="type_value" t-as="error">
<a t-if="!_.contains(['', '#', false], error.link)" t-att-href="error.link">
<t t-raw="error.text"/>
</a>
<t t-else="">
<t t-raw="error.text"/>
</t>
</li>
</ul>
</div>
</div>
</div>
</t>
</t>
<t t-name="website.delete_page">
<t t-call="website.modal_base">
<t t-call="website.dependencies" />
<form class="form-horizontal" role="form" t-att-id="id">
<div class="form-group">
<label class="col-xs-offset-1 col-xs-10 col-sm-offset-2 col-sm-8 mt16 text-center alert alert-danger">
<input style="margin-right:10px" type="checkbox" onchange="$('.btn-continue').prop('disabled', !this.checked)"/>
<span style="font-weight:normal"><br class="visible-xs"/><b>I'm sure</b>, I want to delete this page <b> <ins>definitively</ins></b></span>
</label>
<div t-name="website.delete_page">
<p>Are you sure you want to delete this page ?</p>
<t t-call="website.dependencies"/>
</div>
<div t-name="website.rename_page">
<div class="well">
<form class="form-horizontal">
<div class="form-group mb0">
<label for="new_name" class="control-label col-sm-4">Rename Page To:</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="new_name" placeholder="e.g. About Us"/>
</div>
</div>
</form>
</t>
</t>
<t t-name="website.rename_page">
<t t-call="website.modal_base">
<t t-call="website.dependencies" />
<div class="well">
<form class="form-horizontal" role="form" t-att-id="id">
<div class="form-group mb0" >
<label for="new_name" class="control-label col-sm-4">Rename Page To:</label>
<div class="col-sm-8">
<input type="text" class="form-control" id="new_name" placeholder="e.g. About Us" />
</div>
</div>
</form>
</div>
</t>
</t>
</div>
<t t-call="website.dependencies"/>
</div>
</templates>
+2 -8
View File
@@ -4,22 +4,16 @@
import odoo
import odoo.tests
class TestUiTranslate(odoo.tests.HttpCase):
def test_admin_tour_rte_translator(self):
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('rte_translator')", "odoo.__DEBUG__.services['web_tour.tour'].tours.rte_translator.ready", login='admin', timeout=120)
class TestUi(odoo.tests.HttpCase):
post_install = True
at_install = False
def test_01_public_homepage(self):
self.phantom_js("/", "console.log('ok')", "'website.snippets.animation' in odoo.__DEBUG__.services")
self.phantom_js("/", "console.log('ok')", "'website.content.snippets.animation' in odoo.__DEBUG__.services")
def test_02_admin_homepage(self):
self.phantom_js("/", "console.log('ok')", "'website.snippets.editor' in odoo.__DEBUG__.services", login='admin')
def test_03_admin_tour_banner(self):
def test_02_admin_tour_banner(self):
self.phantom_js("/", "odoo.__DEBUG__.services['web_tour.tour'].run('banner')", "odoo.__DEBUG__.services['web_tour.tour'].tours.banner.ready", login='admin')
+24 -22
View File
@@ -747,8 +747,8 @@
<t t-call="web_editor.snippet_options"/>
<div data-js='gallery' data-selector=".o_gallery">
<li data-images_add="true"><a href="#"><i class="fa fa-plus-circle"/>Add images</a></li>
<li data-images_rm="true"><a href="#"><i class="fa fa-trash"/>Remove all images</a></li>
<li data-add-images="true" data-no-preview="true"><a href="#"><i class="fa fa-plus-circle"/>Add images</a></li>
<li data-remove-all-images="true" data-no-preview="true"><a href="#"><i class="fa fa-trash"/>Remove all images</a></li>
<li class="separator"/>
<li class="dropdown-submenu">
<a tabindex="-2" href="#"><i class="fa fa-magic"/>Mode</a>
@@ -785,10 +785,10 @@
<a tabindex="-2" href="#"><i class="fa fa-arrows-h"/>Images spacing</a>
<ul class="dropdown-menu">
<li data-select_class="o_spc-none"><a href="#">None</a></li>
<li data-select_class="o_spc-small"><a href="#">Small</a></li>
<li data-select_class="o_spc-medium"><a href="#">Medium</a></li>
<li data-select_class="o_spc-big"><a href="#">Big</a></li>
<li data-select-class="o_spc-none"><a href="#">None</a></li>
<li data-select-class="o_spc-small"><a href="#">Small</a></li>
<li data-select-class="o_spc-medium"><a href="#">Medium</a></li>
<li data-select-class="o_spc-big"><a href="#">Big</a></li>
</ul>
</li>
<li class="dropdown-submenu">
@@ -809,7 +809,7 @@
<li class="dropdown-submenu">
<a tabindex="-1" href="#"><i class="fa fa-picture-o"/>Background Image</a>
<ul class="dropdown-menu">
<li data-choose_image="choose_image"><a href="#"><b>Set Custom Image...</b></a></li>
<li data-choose-image="true" data-no-preview="true"><a href="#"><b>Set Custom Image...</b></a></li>
<li class="divider"></li>
<li data-background="" class="none"><a href="#">None</a></li>
<li class="divider"></li>
@@ -828,13 +828,14 @@
<div data-js='background_position'
data-selector="section, :not(.o_gallery > .container) > .carousel, .s_banner .carousel-content">
<li class="background_position_li" data-background_position="background_position">
<li class="background_position_li" data-background-position="true" data-no-preview="true">
<a tabindex="-1" href="#"><i class="fa fa-arrows"/>Background Image Sizing</a>
</li>
</div>
<div data-js="colorpicker"
data-selector="section:not(.parallax), :not(.o_gallery > .container) > .carousel">
data-selector="section, :not(.o_gallery > .container) > .carousel"
data-exclude=".parallax">
<li class="dropdown-submenu">
<a tabindex="-1" href="#"><i class="fa fa-eyedropper"/>Background Color</a>
<ul class="dropdown-menu">
@@ -858,7 +859,7 @@
<div data-js='gallery_img' data-selector=".o_gallery img">
<li class="dropdown-submenu">
<a tabindex="-2" href="#"><i class="fa fa-refresh"/>Re-order</a>
<ul class="dropdown-menu">
<ul class="dropdown-menu" data-no-preview="true">
<li data-position="first"><a href="#">Move to first</a></li>
<li data-position="prev"><a href="#">Move to previous</a></li>
<li data-position="next"><a href="#">Move to next</a></li>
@@ -881,8 +882,8 @@
<li data-interval="0"><a href="#">Disable autoplay</a></li>
</ul>
</li>
<li data-add_slide="true"><a href="#"><i class="fa fa-plus-circle"/>Add Slide</a></li>
<li data-remove_slide="true"><a href="#" ><i class="fa fa-trash-o"/>Remove Slide</a></li>
<li data-add-slide="true" data-no-preview="true"><a href="#"><i class="fa fa-plus-circle"/>Add Slide</a></li>
<li data-remove-slide="true" data-no-preview="true"><a href="#" ><i class="fa fa-trash-o"/>Remove Slide</a></li>
</div>
<div data-js='margin-y'
@@ -914,11 +915,11 @@
<li class="dropdown-submenu">
<a tabindex="-2" href="#"><i class="fa fa-expand"/>Margin</a>
<ul class="dropdown-menu">
<li data-select_class="o_margin_xl"><a href="#">Extra-Large</a></li>
<li data-select_class="o_margin_l"><a href="#">Large</a></li>
<li data-select_class="o_margin_m"><a href="#">Medium</a></li>
<li data-select_class="o_margin_s"><a href="#">Small</a></li>
<li data-select_class=""><a href="#">None</a></li>
<li data-select-class="o_margin_xl"><a href="#">Extra-Large</a></li>
<li data-select-class="o_margin_l"><a href="#">Large</a></li>
<li data-select-class="o_margin_m"><a href="#">Medium</a></li>
<li data-select-class="o_margin_s"><a href="#">Small</a></li>
<li data-select-class=""><a href="#">None</a></li>
</ul>
</li>
</div>
@@ -926,9 +927,9 @@
<div data-selector=".o_image_floating">
<li class="dropdown-submenu">
<a tabindex="-2" href="#"><i class="fa fa-magnet"/>Float</a>
<ul class="dropdown-menu">
<li data-select_class="pull-left" data-only="click"><a href="#">Left</a></li>
<li data-select_class="pull-right" data-only="click"><a href="#">Right</a></li>
<ul class="dropdown-menu" data-no-preview="true">
<li data-select-class="pull-left"><a href="#">Left</a></li>
<li data-select-class="pull-right"><a href="#">Right</a></li>
</ul>
</li>
</div>
@@ -951,7 +952,7 @@
<div data-js='ul'
data-selector=":not(li) > ul:has(ul,ol), :not(li) > ol:has(ul,ol)">
<li data-toggle_class="o_ul_folded"><a href="#">Folded list</a></li>
<li data-toggle-class="o_ul_folded"><a href="#">Folded list</a></li>
</div>
<div data-js='collapse'
@@ -960,7 +961,8 @@
</div>
<div data-js="menu_data"
data-selector="#top_menu li > a:not(.dropdown-toggle)"
data-selector="#top_menu li > a"
data-exclude=".dropdown-toggle"
data-no-check="true"/>
<div data-js="company_data"
@@ -30,9 +30,9 @@
<ul class="o_menu_sections" groups="website.group_website_designer">
<!-- Content -->
<li class="dropdown content" t-if="editable">
<li t-if="editable" class="dropdown" id="content-menu">
<a id="content-menu-button" class="dropdown-toggle waves" data-toggle="dropdown" href="#">Content</a>
<ul class="dropdown-menu oe_content_menu" role="menu">
<ul class="dropdown-menu" role="menu">
<li>
<a data-action="edit_menu" href="#" title="Edit Top Menu">Edit Menu</a>
</li>
@@ -43,17 +43,17 @@
</li>
<!-- Customize -->
<li class="dropdown" id="customize-menu">
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#" id="customize-menu-button">Customize</a>
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#">Customize</a>
<ul class="dropdown-menu" role="menu">
<li id="theme_customize"><a href="#">Customize Theme</a></li>
<li id="html_editor"><a data-action="ace" href="#advanced-view-editor">HTML/CSS Editor</a></li>
<li id="theme_customize"><a href="#" data-action="customize_theme">Customize Theme</a></li>
<li id="html_editor"><a href="#" data-action="ace">HTML/CSS Editor</a></li>
<li id="install_apps"><a href="/web#action=website.action_website_configuration">Add Features</a></li>
<li class="divider"/>
</ul>
</li>
<!-- Promote -->
<li class="dropdown">
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#" id="promote-menu-button">Promote</a>
<li class="dropdown" id="promote-menu">
<a class="dropdown-toggle waves" data-toggle="dropdown" href="#">Promote</a>
<ul class="dropdown-menu oe_promote_menu" role="menu">
<li><a data-action="promote-current-page" href="#" title="Promote page on the web">Optimize SEO</a></li>
</ul>
@@ -62,14 +62,19 @@
<ul class="o_menu_systray hidden-xs" groups="website.group_website_publisher">
<!-- Mobile preview -->
<li class="o_mobile_preview">
<li class="o_mobile_preview" id="mobile-menu">
<a data-action="show-mobile-preview" href="#"><span title="Mobile preview" class="fa fa-mobile"/></a>
</li>
<!-- Page Edition -->
<li>
<a data-action="new_page" href="#" data-toggle="modal" data-target="#o_website_add_page_modal"><span class="fa fa-plus"/>New</a>
<li class="o_new_content_menu" id="new-content-menu">
<a href="#"><span class="fa fa-plus"/>New</a>
<div id="o_new_content_menu_choices" class="o_hidden">
<ul>
<li groups="website.group_website_designer"><a href="#" data-action="new_page"><i class="fa fa-file-o"/><p>New Page</p></a></li>
</ul>
</div>
</li>
<li t-if="not translatable">
<li t-if="not translatable" id="edit-page-menu">
<a data-action="edit" href="#"><span class="fa fa-pencil"/>Edit</a>
</li>
<li t-if="translatable">
@@ -81,16 +86,5 @@
</ul>
</nav>
</xpath>
<!-- Add new page/content modal -->
<xpath expr="//div[@id='wrapwrap']" position="after">
<div groups="website.group_website_publisher" class="modal" id="o_website_add_page_modal">
<div class="modal-dialog modal-lg">
<ul>
<li groups="website.group_website_designer"><a href="#" data-action="new_page"><i class="fa fa-file-o"/><p>New Page</p></a></li>
</ul>
</div>
</div>
</xpath>
</template>
</odoo>
+38 -21
View File
@@ -2,12 +2,14 @@
<odoo>
<!-- Assets -->
<template id="assets_backend" inherit_id="web.assets_backend" name="Website Backend Assets (used in backend interface)">
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website/static/src/js/backend/button.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.tour.rte.js"></script>
<script type="text/javascript" src="/website/static/src/js/backend/dashboard.js"></script>
<xpath expr="//link[last()]" position="after">
<link rel="stylesheet" href="/website/static/src/less/website.backendDashboard.less"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website/static/src/js/backend/button.js"/>
<script type="text/javascript" src="/website/static/src/js/backend/dashboard.js"/>
<script type="text/javascript" src="/website/static/src/js/tours/rte.js"/>
</xpath>
</template>
<template id="qunit_suite" inherit_id="web.qunit_suite">
@@ -23,10 +25,16 @@
<link rel="stylesheet" type="text/less" href="/website/static/src/less/website.zoomodoo.less"/>
</xpath>
<xpath expr="//script[last()]" position="after">
<script type="text/javascript" src="/website/static/src/js/website.js"></script>
<script type="text/javascript" src="/website/static/src/js/content/share.js"></script>
<script type="text/javascript" src="/website/static/src/js/content/snippets.animation.js"></script>
<script type="text/javascript" src="/website/static/src/js/content/zoomodoo.js"></script>
<script type="text/javascript" src="/website/static/src/js/utils.js"/>
<script type="text/javascript" src="/website/static/src/js/website.js"/>
<script type="text/javascript" src="/website/static/src/js/content/compatibility.js"/>
<script type="text/javascript" src="/website/static/src/js/content/lazy_template_call.js"/>
<script type="text/javascript" src="/website/static/src/js/content/snippets.animation.js"/>
<script type="text/javascript" src="/website/static/src/js/content/website_root.js"/>
<script type="text/javascript" src="/website/static/src/js/content/zoomodoo.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/navbar.js"/>
</xpath>
</template>
@@ -35,21 +43,26 @@
<link rel="stylesheet" type="text/less" href="/website/static/src/less/website.edit_mode.less"/>
<script type="text/javascript" src="/website/static/src/js/tours/rte.js"></script>
<script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/editor.js"></script>
<script type="text/javascript" src="/website/static/lib/jQuery.transfo.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/mobile_view.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/seo.js"></script>
<script type="text/javascript" src="/website/static/src/js/tours/banner.js"></script>
<script type="text/javascript" src="/website/static/src/js/widgets/ace.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/translate.js"></script>
<script type="text/javascript" src="/website/static/src/js/widgets/theme.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/planner.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/content.js"></script>
<script type="text/javascript" src="/website/static/src/js/editor/editor.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/rte.summernote.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/snippets.options.js"/>
<script type="text/javascript" src="/website/static/src/js/editor/snippets.options.js"></script>
<script type="text/javascript" src="/website/static/src/js/website.snippets.gallery.js"></script>
<script type="text/javascript" src="/website/static/src/js/menu/content.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/customize.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/edit.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/mobile_view.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/new_content.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/planner.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/seo.js"/>
<script type="text/javascript" src="/website/static/src/js/menu/translate.js"/>
<script type="text/javascript" src="/website/static/src/js/tours/banner.js"/>
<script type="text/javascript" src="/website/static/src/js/tours/rte.js"/>
<script type="text/javascript" src="/website/static/src/js/widgets/ace.js"/>
<script type="text/javascript" src="/website/static/src/js/widgets/theme.js"/>
</template>
<!-- Layout -->
@@ -127,6 +140,10 @@
</t>
<script type="text/javascript">
<t groups="website.group_website_publisher">
odoo.snippetsURL = '/website/snippets';
</t>
odoo.session_info = {
is_superuser: <t t-esc="json.dumps(request.env.user._is_superuser())"/>,
is_system: <t t-esc="json.dumps(request.env.user._is_system())"/>,
@@ -68,7 +68,7 @@ odoo.define("website_blog.tour", function (require) {
position: "bottom",
content: _t("<b>Publish your blog post</b> to make it visible to your visitors."),
}, {
trigger: "#customize-menu-button",
trigger: "#customize-menu > a",
extra_trigger: "body:not(.editor_enable) .js_publish_management button.js_publish_btn.btn-success:visible",
content: _t("<b>That's it, your blog post is published!</b> Discover more features through the <i>Customize</i> menu."),
position: "bottom",
@@ -1,169 +1,189 @@
odoo.define('website_blog.new_blog_post', function (require) {
"use strict";
'use strict';
var core = require('web.core');
var rpc = require('web.rpc');
var base = require('web_editor.base');
var website = require('website.website');
var contentMenu = require('website.contentMenu');
var weContext = require('web_editor.context');
var wUtils = require('website.utils');
var WebsiteNewMenu = require('website.newMenu');
var _t = core._t;
contentMenu.TopBar.include({
new_blog_post: function () {
rpc.query({
model: 'blog.blog',
method: 'name_search',
context: base.get_context(),
})
.then(function (blog_ids) {
if (blog_ids.length === 1) {
document.location = '/blog/' + blog_ids[0][0] + '/post/new';
} else if (blog_ids.length > 1) {
website.prompt({
id: "editor_new_blog",
window_title: _t("New Blog Post"),
select: "Select Blog",
init: function (field) {
return blog_ids;
},
}).then(function (blog_id) {
document.location = '/blog/' + blog_id + '/post/new';
});
}
});
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_blog_post: '_createNewBlogPost',
}),
//----------------------------------------------------------------------
// Actions
//----------------------------------------------------------------------
/**
* Asks the user information about a new blog post to create, then creates
* it and redirects the user to this new page.
*
* @private
*/
_createNewBlogPost: function () {
return this._rpc({
model: 'blog.blog',
method: 'name_search',
context: weContext.get(),
}).then(function (blog_ids) {
if (blog_ids.length === 1) {
document.location = '/blog/' + blog_ids[0][0] + '/post/new';
} else if (blog_ids.length > 1) {
wUtils.prompt({
id: 'editor_new_blog',
window_title: _t("New Blog Post"),
select: "Select Blog",
init: function (field) {
return blog_ids;
},
}).then(function (blog_id) {
document.location = '/blog/' + blog_id + '/post/new';
});
}
});
},
});
});
//==============================================================================
odoo.define('website_blog.editor', function (require) {
"use strict";
'use strict';
var ajax = require('web.ajax');
var widget = require('web_editor.widget');
var options = require('web_editor.snippets.options');
var rte = require('web_editor.rte');
require('web.dom_ready');
var widget = require('web_editor.widget');
var options = require('web_editor.snippets.options');
var rte = require('web_editor.rte');
if(!$('.website_blog').length) {
return $.Deferred().reject("DOM doesn't contain '.website_blog'");
}
if (!$('.website_blog').length) {
return $.Deferred().reject("DOM doesn't contain '.website_blog'");
}
rte.Class.include({
// Destroy popOver and stop listening mouseup event on edit mode
start: function () {
$(".js_tweet, .js_comment").off('mouseup').trigger('mousedown');
return this._super.apply(this, arguments);
},
saveElement: function ($el, context) {
if ($el.is('.o_blog_cover_container')) {
return ajax.jsonRpc("/blog/post_change_background", 'call', {
'post_id' : parseInt($el.closest("[name=\"blog_post\"], .website_blog").find("[data-oe-model=\"blog.post\"]").first().data("oe-id"), 10),
'cover_properties' : {
"background-image": $el.children(".o_blog_cover_image").css("background-image").replace(/"/g, '').replace(location.protocol + "//" + location.host, ''),
rte.Class.include({
// Destroy popOver and stop listening mouseup event on edit mode
start: function () {
$(".js_tweet, .js_comment").off('mouseup').trigger('mousedown');
return this._super.apply(this, arguments);
},
_saveElement: function ($el, context) {
// TODO the o_dirty class is not put on the right element for blog cover
// edition. For some strange reason, it was forcly put (even if not
// dirty) in <= saas-16 but this is not the case anymore.
if (this.__blogCoverSaved) {
return $.when();
}
var $blogContainer = $el.closest('.o_blog_cover_container');
if ($blogContainer.length) {
$el = $blogContainer;
this.__blogCoverSaved = true;
return this._rpc({
route: '/blog/post_change_background',
params: {
post_id: parseInt($el.closest("[name=\"blog_post\"], .website_blog").find("[data-oe-model=\"blog.post\"]").first().data("oe-id"), 10),
cover_properties: {
"background-image": $el.children(".o_blog_cover_image").css("background-image").replace(/"/g, '').replace(window.location.protocol + "//" + window.location.host, ''),
"background-color": $el.data("filter_color"),
"opacity": $el.data("filter_value"),
"resize_class": $el.data("cover_class"),
}
});
}
return this._super.apply(this, arguments);
},
});
options.registry.many2one.include({
select_record: function (li) {
var self = this;
this._super(li);
if (this.$target.data('oe-field') === "author_id") {
var $nodes = $('[data-oe-model="blog.post"][data-oe-id="'+this.$target.data('oe-id')+'"][data-oe-field="author_avatar"]');
$nodes.each(function () {
var $img = $(this).find("img");
var css = window.getComputedStyle($img[0]);
$img.css({ width: css.width, height: css.height });
$img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
});
setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
}
}
});
options.registry.blog_cover = options.Class.extend({
init: function () {
this._super.apply(this, arguments);
this.$image = this.$target.children(".o_blog_cover_image");
this.$filter = this.$target.children(".o_blog_cover_filter");
this.$filter_value_options = this.$el.find('li[data-filter_value]');
this.$filter_color_options = this.$el.find('li[data-filter_color]');
this.filter_color_classes = this.$filter_color_options.map(function () {
return $(this).data("filter_color");
}).get().join(" ");
},
clear: function (type, value, $li) {
if (type !== 'click') return;
this.select_class(type, "", $());
this.$image.css("background-image", "");
this.$target.addClass("o_dirty");
},
change: function (type, value, $li) {
if (type !== 'click') return;
var $image = $("<img/>", {src: this.$image.css("background-image")});
var editor = new widget.MediaDialog(null, {only_images: true}, $image, $image[0]).open();
editor.on("save", this, function (event, img) {
var src = $image.attr("src");
this.$image.css("background-image", src ? ("url(" + src + ")") : "");
if (!this.$target.hasClass("cover")) {
var $li = this.$el.find("[data-select_class]").first();
this.select_class(type, $li.data("select_class"), $li);
}
this.set_active();
this.$target.addClass("o_dirty");
},
},
});
},
filter_value: function (type, value, $li) {
this.$filter.css("opacity", value);
this.$target.addClass('o_dirty');
},
filter_color: function (type, value, $li) {
this.$filter.removeClass(this.filter_color_classes);
if (value) {
this.$filter.addClass(value);
}
this.$target.addClass("o_dirty");
var $first_visible_filter_option = this.$filter_value_options.eq(1);
if (parseFloat(this.$filter.css('opacity')) < parseFloat($first_visible_filter_option.data("filter_value"))) {
this.filter_value(type, $first_visible_filter_option.data("filter_value"), $first_visible_filter_option);
}
},
set_active: function () {
this._super.apply(this, arguments);
var self = this;
this.$el.filter(":not([data-change])").toggleClass("hidden", !this.$target.hasClass("cover"));
this.$el.filter("li:has(li[data-select_class])").toggleClass("hidden", this.$target.hasClass("o_list_cover"));
this.$filter_value_options.removeClass("active");
this.$filter_color_options.removeClass("active");
var active_filter_value = this.$filter_value_options
.filter(function () {
return (parseFloat($(this).data('filter_value')).toFixed(1) === parseFloat(self.$filter.css('opacity')).toFixed(1));
}).addClass("active").data("filter_value");
var active_filter_color = this.$filter_color_options
.filter(function () {
return self.$filter.hasClass($(this).data("filter_color"));
}).addClass("active").data("filter_color");
this.$target.data("cover_class", this.$el.find(".active[data-select_class]").data("select_class") || "");
this.$target.data("filter_value", active_filter_value || 0.0);
this.$target.data("filter_color", active_filter_color || "");
},
});
}
return this._super.apply(this, arguments);
},
});
options.registry.many2one.include({
_selectRecord: function ($li) {
var self = this;
this._super.apply(this, arguments);
if (this.$target.data('oe-field') === "author_id") {
var $nodes = $('[data-oe-model="blog.post"][data-oe-id="'+this.$target.data('oe-id')+'"][data-oe-field="author_avatar"]');
$nodes.each(function () {
var $img = $(this).find("img");
var css = window.getComputedStyle($img[0]);
$img.css({ width: css.width, height: css.height });
$img.attr("src", "/web/image/res.partner/"+self.ID+"/image");
});
setTimeout(function () { $nodes.removeClass('o_dirty'); },0);
}
}
});
options.registry.blog_cover = options.Class.extend({
init: function () {
this._super.apply(this, arguments);
this.$image = this.$target.children(".o_blog_cover_image");
this.$filter = this.$target.children(".o_blog_cover_filter");
},
start: function () {
this.$filter_value_options = this.$el.find('li[data-filter_value]');
this.$filter_color_options = this.$el.find('li[data-filter_color]');
this.filter_color_classes = this.$filter_color_options.map(function () {
return $(this).data("filter_color");
}).get().join(" ");
return this._super.apply(this, arguments);
},
clear: function (previewMode, value, $li) {
this.selectClass(previewMode, "", $());
this.$image.css("background-image", "");
},
change: function (previewMode, value, $li) {
var $image = $("<img/>", {src: this.$image.css("background-image")});
var editor = new widget.MediaDialog(this, {only_images: true}, $image, $image[0]).open();
editor.on("save", this, function (event, img) {
var src = $image.attr("src");
this.$image.css("background-image", src ? ("url(" + src + ")") : "");
if (!this.$target.hasClass("cover")) {
var $li = this.$el.find("[data-select-class]").first();
this.selectClass(previewMode, $li.data("selectClass"), $li);
}
this._setActive();
});
},
filter_value: function (previewMode, value, $li) {
this.$filter.css("opacity", value);
},
filter_color: function (previewMode, value, $li) {
this.$filter.removeClass(this.filter_color_classes);
if (value) {
this.$filter.addClass(value);
}
var $first_visible_filter_option = this.$filter_value_options.eq(1);
if (parseFloat(this.$filter.css('opacity')) < parseFloat($first_visible_filter_option.data("filter_value"))) {
this.filter_value(previewMode, $first_visible_filter_option.data("filter_value"), $first_visible_filter_option);
}
},
_setActive: function () {
this._super.apply(this, arguments);
var self = this;
this.$el.filter(":not([data-change])").toggleClass("hidden", !this.$target.hasClass("cover"));
this.$el.filter("li:has(li[data-select-class])").toggleClass("hidden", this.$target.hasClass("o_list_cover"));
this.$filter_value_options.removeClass("active");
this.$filter_color_options.removeClass("active");
var active_filter_value = this.$filter_value_options
.filter(function () {
return (parseFloat($(this).data('filter_value')).toFixed(1) === parseFloat(self.$filter.css('opacity')).toFixed(1));
}).addClass("active").data("filter_value");
var active_filter_color = this.$filter_color_options
.filter(function () {
return self.$filter.hasClass($(this).data("filter_color"));
}).addClass("active").data("filter_color");
this.$target.data("cover_class", this.$el.find(".active[data-select-class]").data("selectClass") || "");
this.$target.data("filter_value", active_filter_value || 0.0);
this.$target.data("filter_color", active_filter_color || "");
},
});
});
+5 -5
View File
@@ -3,14 +3,14 @@
<template id="website_blog" inherit_id="website.snippets" name="Snippet Blog">
<xpath expr="//div[@id='snippet_options']" position="inside">
<div data-js="blog_cover" data-selector=".website_blog .o_blog_cover_container" data-no-check="true">
<li data-change="true"><a href="#">Change Cover</a></li>
<li data-clear="true"><a href="#" >No Cover</a></li>
<li data-change="true" data-no-preview="true"><a href="#">Change Cover</a></li>
<li data-clear="true" data-no-preview="true"><a href="#" >No Cover</a></li>
<li class="dropdown-submenu">
<a href="#">Size</a>
<ul class="dropdown-menu">
<li data-select_class="cover container-fluid cover_full"><a href="#">Full Screen</a></li>
<li data-select_class="cover container-fluid cover_narrow"><a href="#">Mid screen</a></li>
<li data-select_class="cover container cover_narrow"><a href="#">Narrow</a></li>
<li data-select-class="cover container-fluid cover_full"><a href="#">Full Screen</a></li>
<li data-select-class="cover container-fluid cover_narrow"><a href="#">Mid screen</a></li>
<li data-select-class="cover container cover_narrow"><a href="#">Narrow</a></li>
</ul>
</li>
<li class="dropdown-submenu">
@@ -538,7 +538,7 @@
<!-- User Navbar -->
<template id="content_new_blogpost" inherit_id="website.user_navbar">
<xpath expr="//div[@id='o_website_add_page_modal']//ul" position="inside">
<xpath expr="//div[@id='o_new_content_menu_choices']//ul" position="inside">
<li groups="website.group_website_designer"><a href="#" data-action="new_blog_post"><i class="fa fa-rss"/><p>New Blog Post</p></a></li>
</xpath>
</template>
@@ -2,7 +2,7 @@ odoo.define('crm.partner_assign', function (require) {
'use strict';
var rpc = require('web.rpc');
var website = require('website.website');
require('web.dom_ready');
/*
* This file is intended to add interactivity to survey forms rendered by
* the website engine.
@@ -193,4 +193,4 @@ $("div.input-group span.fa-calendar").on('click', function(e) {
},
});
});
});
});
+4 -4
View File
@@ -174,10 +174,10 @@ class WebsiteEventController(http.Controller):
}
return request.render("website_event.event_description_full", values)
@http.route('/event/add_event', type='http', auth="user", methods=['POST'], website=True)
@http.route('/event/add_event', type='json', auth="user", methods=['POST'], website=True)
def add_event(self, event_name="New Event", **kwargs):
event = self._add_event(event_name, request.context)
return request.redirect("/event/%s/register?enable_editor=1" % slug(event))
return "/event/%s/register?enable_editor=1" % slug(event)
def _add_event(self, event_name=None, context=None, **kwargs):
if not event_name:
@@ -197,7 +197,7 @@ class WebsiteEventController(http.Controller):
month = babel.dates.get_month_names('abbreviated', locale=event.env.context.get('lang', 'en_US'))[start_date.month]
return ('%s %s%s') % (month, start_date.strftime("%e"), (end_date != start_date and ("-" + end_date.strftime("%e")) or ""))
@http.route('/event/get_country_event_list', type='http', auth='public', website=True)
@http.route('/event/get_country_event_list', type='json', auth='public', website=True)
def get_country_events(self, **post):
Event = request.env['event.event']
country_code = request.session['geoip'].get('country_code')
@@ -215,7 +215,7 @@ class WebsiteEventController(http.Controller):
"date": self.get_formated_date(event),
"event": event,
"url": event.website_url})
return request.render("website_event.country_events_list", result)
return request.env['ir.ui.view'].render_template("website_event.country_events_list", result)
def _process_tickets_details(self, data):
nb_register = int(data.get('nb_register-0', 0))
@@ -2,23 +2,42 @@ odoo.define('website_event.editor', function (require) {
"use strict";
var core = require('web.core');
var contentMenu = require('website.contentMenu');
var website = require('website.website');
var wUtils = require('website.utils');
var WebsiteNewMenu = require("website.newMenu");
var _t = core._t;
contentMenu.TopBar.include({
new_event: function() {
website.prompt({
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_event: '_createNewEvent',
}),
//----------------------------------------------------------------------
// Actions
//----------------------------------------------------------------------
/**
* Asks the user information about a new event to create, then creates it
* and redirects the user to this new event.
*
* @private
*/
_createNewEvent: function () {
var self = this;
wUtils.prompt({
id: "editor_new_event",
window_title: _t("New Event"),
input: "Event Name",
}).then(function (event_name) {
website.form('/event/add_event', 'POST', {
event_name: event_name
}).then(function (eventName) {
self._rpc({
route: '/event/add_event',
params: {
event_name: eventName,
},
}).then(function (url) {
window.location.href = url;
});
});
},
});
});
@@ -1,14 +1,32 @@
odoo.define('website_event.registration_form.instance', function (require) {
'use strict';
require('web_editor.ready');
var EventRegistrationForm = require('website_event.website_event');
var $form = $('#registration_form');
if (!$form.length) {
return null;
}
var instance = new EventRegistrationForm();
return instance.appendTo($form).then(function () {
return instance;
});
});
//==============================================================================
odoo.define('website_event.website_event', function (require) {
var ajax = require('web.ajax');
var Widget = require('web.Widget');
var web_editor_base = require('web_editor.base')
// Catch registration form event, because of JS for attendee details
var EventRegistrationForm = Widget.extend({
start: function() {
start: function () {
var self = this;
var res = this._super.apply(this.arguments).then(function() {
var res = this._super.apply(this.arguments).then(function () {
$('#registration_form .a-submit')
.off('click')
.removeClass('a-submit')
@@ -16,23 +34,22 @@ var EventRegistrationForm = Widget.extend({
self.on_click(ev);
});
});
return res
return res;
},
on_click: function(ev) {
on_click: function (ev) {
ev.preventDefault();
ev.stopPropagation();
var $form = $(ev.currentTarget).closest('form');
var post = {};
$("#registration_form select").each(function() {
$('#registration_form select').each(function () {
post[$(this).attr('name')] = $(this).val();
});
var tickets_ordered = _.some(_.map(post, function(value, key) { return parseInt(value) }));
if (! tickets_ordered) {
var tickets_ordered = _.some(_.map(post, function (value, key) { return parseInt(value); }));
if (!tickets_ordered) {
return $('#registration_form table').after(
'<div class="alert alert-info">Please select at least one ticket.</div>'
);
}
else {
} else {
return ajax.jsonRpc($form.attr('action'), 'call', post).then(function (modal) {
var $modal = $(modal);
$modal.appendTo($form).modal();
@@ -44,10 +61,5 @@ var EventRegistrationForm = Widget.extend({
},
});
web_editor_base.ready().then(function(){
var event_registration_form = new EventRegistrationForm().appendTo($('#registration_form'));
});
return { EventRegistrationForm: EventRegistrationForm };
return EventRegistrationForm;
});
@@ -1,17 +1,23 @@
odoo.define('website_event.geolocation', function (require) {
"use strict";
'use strict';
var animation = require('web_editor.snippets.animation');
var sAnimation = require('website.content.snippets.animation');
animation.registry.visitor = animation.Class.extend({
selector: ".oe_country_events",
sAnimation.registry.visitor = sAnimation.Class.extend({
selector: '.oe_country_events',
/**
* @override
*/
start: function () {
$.get("/event/get_country_event_list").then(function( data ) {
if(data){
$( ".country_events_list" ).replaceWith( data );
var defs = [this._super.apply(this, arguments)];
var self = this;
defs.push(this._rpc({route: '/event/get_country_event_list'}).then(function (data) {
if (data) {
self.$('.country_events_list').replaceWith(data);
}
});
}
}));
return $.when.apply($, defs);
},
});
});
@@ -722,8 +722,8 @@
<!-- User Navbar -->
<template id="content_new_event" inherit_id="website.user_navbar">
<xpath expr="//div[@id='o_website_add_page_modal']//ul" position="inside">
<li groups="event.group_event_manager"><a href="#" data-action="new_event"><i class="fa fa-glass"></i><p>New Event</p></a></li>
<xpath expr="//div[@id='o_new_content_menu_choices']//ul" position="inside">
<li groups="event.group_event_manager"><a href="#" data-action="new_event"><i class="fa fa-glass"/><p>New Event</p></a></li>
</xpath>
</template>
</odoo>
@@ -1,15 +1,16 @@
odoo.define('website_event_questions.website_event_questions', function (require) {
var event_registration_form = require('website_event.website_event');
'use strict';
event_registration_form.EventRegistrationForm.include({
on_click: function(ev) {
var EventRegistrationForm = require('website_event.website_event');
EventRegistrationForm.include({
on_click: function (ev) {
var def = this._super.apply(this, arguments);
def.then(function() {
$("select.o_specific_answer,select.o_general_answer").each(function(index, elem) {
def.then(function () {
$("select.o_specific_answer,select.o_general_answer").each(function (index, elem) {
$(elem).select2({ minimumResultsForSearch: -1, width: '100%' });
});
});
}
},
});
});
});
@@ -4,27 +4,26 @@ odoo.define('website_form.animation', function (require) {
var core = require('web.core');
var time = require('web.time');
var ajax = require('web.ajax');
var snippet_animation = require('web_editor.snippets.animation');
var sAnimation = require('website.content.snippets.animation');
var _t = core._t;
var qweb = core.qweb;
snippet_animation.registry.form_builder_send = snippet_animation.Class.extend({
sAnimation.registry.form_builder_send = sAnimation.Class.extend({
selector: '.s_website_form',
willStart: function(){
willStart: function () {
var def;
if(!$.fn.datetimepicker){
if (!$.fn.datetimepicker) {
def = ajax.loadJS("/web/static/lib/bootstrap-datetimepicker/src/js/bootstrap-datetimepicker.js");
}
return $.when(this._super.apply(this, arguments), def);
},
start: function() {
start: function () {
var self = this;
qweb.add_template('/website_form/static/src/xml/website_form.xml');
this.$target.find('.o_website_form_send').on('click',function(e) {self.send(e);});
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
// Initialize datetimepickers
var l10n = _t.database.parameters;
@@ -48,13 +47,16 @@ odoo.define('website_form.animation', function (require) {
// Adapt options to date-only pickers
datepickers_options.format = time.strftime_to_moment_format(l10n.date_format);
this.$target.find('.o_website_form_date').datetimepicker(datepickers_options);
return this._super.apply(this, arguments);
},
stop: function() {
destroy: function () {
this._super.apply(this, arguments);
this.$target.find('button').off('click');
},
send: function(e) {
send: function (e) {
e.preventDefault(); // Prevent the default submit behavior
this.$target.find('.o_website_form_send').off(); // Prevent users from crazy clicking
@@ -68,8 +70,8 @@ odoo.define('website_form.animation', function (require) {
// Prepare form inputs
this.form_fields = this.$target.serializeArray();
_.each(this.$target.find('input[type=file]'), function(input) {
$.each($(input).prop('files'), function(index, file) {
_.each(this.$target.find('input[type=file]'), function (input) {
$.each($(input).prop('files'), function (index, file) {
// Index field name as ajax won't accept arrays of files
// when aggregating multiple files into a single field value
self.form_fields.push({
@@ -82,7 +84,7 @@ odoo.define('website_form.animation', function (require) {
// Serialize form inputs into a single object
// Aggregate multiple values into arrays
var form_values = {};
_.each(this.form_fields, function(input) {
_.each(this.form_fields, function (input) {
if (input.name in form_values) {
// If a value already exists for this field,
// we are facing a x2many field, so we store
@@ -93,7 +95,7 @@ odoo.define('website_form.animation', function (require) {
form_values[input.name] = [form_values[input.name], input.value];
}
} else {
if (input.value != '') {
if (input.value !== '') {
form_values[input.name] = input.value;
}
}
@@ -110,9 +112,9 @@ odoo.define('website_form.animation', function (require) {
// Post form and handle result
ajax.post(this.$target.attr('action') + (this.$target.data('force_action')||this.$target.data('model_name')), form_values)
.then(function(result_data) {
.then(function (result_data) {
result_data = $.parseJSON(result_data);
if(!result_data.id) {
if (!result_data.id) {
// Failure, the server didn't return the created record ID
self.update_status('error');
if (result_data.error_fields) {
@@ -122,8 +124,8 @@ odoo.define('website_form.animation', function (require) {
} else {
// Success, redirect or update status
var success_page = self.$target.attr('data-success_page');
if(success_page) {
$(location).attr('href', success_page);
if (success_page) {
$(window.location).attr('href', success_page);
}
else {
self.update_status('success');
@@ -133,37 +135,35 @@ odoo.define('website_form.animation', function (require) {
self.$target[0].reset();
}
})
.fail(function(result_data){
.fail(function (result_data){
self.update_status('error');
});
},
check_error_fields: function(error_fields) {
check_error_fields: function (error_fields) {
var self = this;
var form_valid = true;
// Loop on all fields
this.$target.find('.form-field').each(function(k, field){
this.$target.find('.form-field').each(function (k, field){
var $field = $(field);
var $fields = self.$fields;
var field_name = $field.find('.control-label').attr('for')
var field_name = $field.find('.control-label').attr('for');
// Validate inputs for this field
var field_valid = true;
var inputs = $field.find('.o_website_form_input:not(#editable_select)');
var invalid_inputs = inputs.toArray().filter(function(input, k, inputs) {
var invalid_inputs = inputs.toArray().filter(function (input, k, inputs) {
// Special check for multiple required checkbox for same
// field as it seems checkValidity forces every required
// checkbox to be checked, instead of looking at other
// checkboxes with the same name and only requiring one
// of them to be checked.
if (input.required && input.type == 'checkbox') {
if (input.required && input.type === 'checkbox') {
// Considering we are currently processing a single
// field, we can assume that all checkboxes in the
// inputs variable have the same name
var checkboxes = _.filter(inputs, function(input){
return input.required && input.type == 'checkbox'
})
return !_.any(checkboxes, function(checkbox){return checkbox.checked})
var checkboxes = _.filter(inputs, function (input){
return input.required && input.type === 'checkbox';
});
return !_.any(checkboxes, function (checkbox) { return checkbox.checked; });
// Special cases for dates and datetimes
} else if ($(input).hasClass('o_website_form_date')) {
@@ -176,11 +176,11 @@ odoo.define('website_form.animation', function (require) {
}
}
return !input.checkValidity();
})
});
// Update field color if invalid or erroneous
$field.removeClass('has-error');
if(invalid_inputs.length || error_fields[field_name]){
if (invalid_inputs.length || error_fields[field_name]){
$field.addClass('has-error');
if (_.isString(error_fields[field_name])){
$field.popover({content: error_fields[field_name], trigger: 'hover', container: 'body', placement: 'top'});
@@ -194,14 +194,14 @@ odoo.define('website_form.animation', function (require) {
return form_valid;
},
is_datetime_valid: function(value, type_of_date) {
is_datetime_valid: function (value, type_of_date) {
if (value === "") {
return true;
} else {
try {
this.parse_date(value, type_of_date);
return true;
} catch(e) {
} catch (e) {
return false;
}
}
@@ -228,12 +228,12 @@ odoo.define('website_form.animation', function (require) {
return value;
},
update_status: function(status) {
update_status: function (status) {
var self = this;
if (status != 'success') { // Restore send button behavior if result is an error
this.$target.find('.o_website_form_send').on('click',function(e) {self.send(e);});
if (status !== 'success') { // Restore send button behavior if result is an error
this.$target.find('.o_website_form_send').on('click',function (e) {self.send(e);});
}
this.$target.find('#o_website_form_result').replaceWith(qweb.render("website_form.status_" + status))
this.$target.find('#o_website_form_result').replaceWith(qweb.render("website_form.status_" + status));
},
});
});
+2 -2
View File
@@ -83,7 +83,7 @@ class WebsiteForum(http.Controller):
forums = request.env['forum.forum'].search([])
return request.render("website_forum.forum_all", {'forums': forums})
@http.route('/forum/new', type='http', auth="user", methods=['POST'], website=True)
@http.route('/forum/new', type='json', auth="user", methods=['POST'], website=True)
def forum_create(self, forum_name="New Forum", add_menu=False):
forum_id = request.env['forum.forum'].create({'name': forum_name})
if add_menu:
@@ -93,7 +93,7 @@ class WebsiteForum(http.Controller):
'parent_id': request.website.menu_id.id,
'website_id': request.website.id,
})
return request.redirect("/forum/%s" % slug(forum_id))
return "/forum/%s" % slug(forum_id)
@http.route('/forum/notification_read', type='json', auth="user", methods=['POST'], website=True)
def notification_read(self, **kwargs):
+1 -1
View File
@@ -62,7 +62,7 @@ In simple words does Odoo became CMS+ERP platform?</p>]]></field>
<record id="answer_1" model="forum.post">
<field name="forum_id" ref="website_forum.forum_help"/>
<field name="content"><![CDATA[
<p><span class="oe_social_share" data-social="twitter linkedin facebook" data-hashtags="#odoo #CMS">Odoo v8 (next release) provides a web module and an e-commerce module: www.odoo.com/page/website-builder</span>
<p>Odoo v8 (next release) provides a web module and an e-commerce module: www.odoo.com/page/website-builder
The CMS editor in Odoo web is nice but I prefer Drupal for customization and there is a Drupal module for Odoo. I think WP is better than Odoo web too.
</p>]]></field>
<field name="parent_id" ref="question_1"/>
@@ -2,14 +2,29 @@ odoo.define('website_forum.editor', function (require) {
"use strict";
var core = require('web.core');
var contentMenu = require('website.contentMenu');
var website = require('website.website');
var wUtils = require('website.utils');
var WebsiteNewMenu = require("website.newMenu");
var _t = core._t;
contentMenu.TopBar.include({
new_forum: function() {
website.prompt({
WebsiteNewMenu.include({
actions: _.extend({}, WebsiteNewMenu.prototype.actions || {}, {
new_forum: '_createNewForum',
}),
//----------------------------------------------------------------------
// Actions
//----------------------------------------------------------------------
/**
* Asks the user information about a new forum to create, then creates it
* and redirects the user to this new forum.
*
* @private
*/
_createNewForum: function () {
var self = this;
wUtils.prompt({
id: "editor_new_forum",
window_title: _t("New Forum"),
input: "Forum Name",init: function () {
@@ -27,12 +42,16 @@ contentMenu.TopBar.include({
}
}).then(function (forum_name, field, $dialog) {
var add_menu = ($dialog.find('input[type="checkbox"]').is(':checked'));
website.form('/forum/new', 'POST', {
forum_name: forum_name,
add_menu: add_menu || ""
self._rpc({
route: '/forum/new',
params: {
forum_name: forum_name,
add_menu: add_menu || "",
},
}).then(function (url) {
window.location.href = url;
});
});
},
});
});
@@ -1,32 +1,32 @@
odoo.define('website_forum.website_forum', function (require) {
'use strict';
'use strict';
var ajax = require('web.ajax');
var core = require('web.core');
var website = require('website.website');
require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var _t = core._t;
var _t = core._t;
var lastsearch;
var lastsearch;
if(!$('.website_forum').length) {
return $.Deferred().reject("DOM doesn't contain '.website_forum'");
}
if (!$('.website_forum').length) {
return $.Deferred().reject("DOM doesn't contain '.website_forum'");
}
$("[data-toggle='popover']").popover();
$('.karma_required').on('click', function (ev) {
var karma = $(ev.currentTarget).data('karma');
if (karma) {
ev.preventDefault();
var msg = karma + ' ' + _t(' karma is required to perform this action. You can earn karma by having your answers upvoted by the community.')
var msg = karma + ' ' + _t(' karma is required to perform this action. You can earn karma by having your answers upvoted by the community.');
if ($('a[href*="/login"]').length) {
msg = _t('Sorry you must be logged in to perform this action');
};
}
var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="karma_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
msg + '</div>');
var vote_alert = $(ev.currentTarget).parent().find("#vote_alert");
if (vote_alert.length == 0) {
if (vote_alert.length === 0) {
$(ev.currentTarget).parent().append($warning);
}
}
@@ -48,7 +48,7 @@ if(!$('.website_forum').length) {
$('.o_forum_file_upload').on('change', function() {
if (this.files.length) {
var $form = $(this).closest('form');
var reader = new FileReader();
var reader = new window.FileReader();
reader.onload = function(ev) {
$form.find('.o_forum_avatar_img').attr('src', ev.target.result);
};
@@ -91,17 +91,17 @@ if(!$('.website_forum').length) {
.then(function (data) {
if(data.error) {
var $warning;
if(data.error == 'anonymous_user') {
if(data.error === 'anonymous_user') {
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('Sorry you must be logged to flag a post') +
'</div>');
} else if(data.error == 'post_already_flagged') {
} else if(data.error === 'post_already_flagged') {
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('This post is already flagged') +
'</div>');
} else if(data.error == 'post_non_flaggable') {
} else if(data.error === 'post_non_flaggable') {
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="flag_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('This post can not be flagged') +
@@ -113,12 +113,12 @@ if(!$('.website_forum').length) {
}
} else if(data.success) {
var elem = $link;
if(data.success == 'post_flagged_moderator') {
if(data.success === 'post_flagged_moderator') {
elem.html(' Flagged');
var c = parseInt($('#count_flagged_posts').html(), 10);
c++;
$('#count_flagged_posts').html(c);
} else if(data.success == 'post_flagged_non_moderator') {
} else if(data.success === 'post_flagged_non_moderator') {
elem.html(' Flagged');
var forum_answer = elem.closest('.forum_answer');
forum_answer.fadeIn(1000);
@@ -134,13 +134,14 @@ if(!$('.website_forum').length) {
ajax.jsonRpc($link.data('href'), 'call', {})
.then(function (data) {
if (data.error){
if (data.error == 'own_post'){
var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">'+
var $warning;
if (data.error === 'own_post'){
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('Sorry, you cannot vote for your own posts') +
'</div>');
} else if (data.error == 'anonymous_user'){
var $warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">'+
} else if (data.error === 'anonymous_user'){
$warning = $('<div class="alert alert-danger alert-dismissable oe_forum_alert" id="vote_alert">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('Sorry you must be logged to vote') +
'</div>');
@@ -189,14 +190,14 @@ if(!$('.website_forum').length) {
var $link = $(ev.currentTarget);
ajax.jsonRpc($link.data('href'), 'call', {}).then(function (data) {
if (data.error) {
if (data.error == 'anonymous_user') {
if (data.error === 'anonymous_user') {
var $warning = $('<div class="alert alert-danger alert-dismissable" id="correct_answer_alert" style="position:absolute; margin-top: -30px; margin-left: 90px;">'+
'<button type="button" class="close notification_close" data-dismiss="alert" aria-hidden="true">&times;</button>'+
_t('Sorry, anonymous users cannot choose correct answer.') +
'</div>');
}
var correct_answer_alert = $link.parent().find("#correct_answer_alert");
if (correct_answer_alert.length == 0) {
if (correct_answer_alert.length === 0) {
$link.parent().append($warning);
}
} else {
@@ -214,11 +215,7 @@ if(!$('.website_forum').length) {
ev.preventDefault();
var $link = $(ev.currentTarget);
ajax.jsonRpc($link.data('href'), 'call', {}).then(function (data) {
if (data) {
$link.addClass("forum_favourite_question")
} else {
$link.removeClass("forum_favourite_question")
}
$link.toggleClass("forum_favourite_question", !!data);
});
});
@@ -418,4 +415,4 @@ if(!$('.website_forum').length) {
});
});
});
});
@@ -1,72 +1,75 @@
odoo.define('website_forum.share', function (require) {
'use strict';
var ajax = require('web.ajax');
require('web.dom_ready');
var core = require('web.core');
var base = require('web_editor.base');
var SocialShare = require('website.share');
var website = require('website.website');
var sAnimation = require('website.content.snippets.animation');
var qweb = core.qweb;
var tmpl = ajax.loadXML('/website_forum/static/src/xml/website_forum_share_templates.xml', qweb);
if(!$('.website_forum').length) {
if (!$('.website_forum').length) {
return $.Deferred().reject("DOM doesn't contain '.website_forum'");
}
// FIXME There is no reason to inherit from socialShare here
var ForumShare = sAnimation.registry.socialShare.extend({
xmlDependencies: sAnimation.registry.socialShare.prototype.xmlDependencies
.concat(['/website_forum/static/src/xml/website_forum_share_templates.xml']),
read_events: {},
var ForumShare = SocialShare.extend({
init: function (parent, target_type) {
this.target_type = target_type;
this._super(parent);
},
bind_events: function () {
init: function (parent, editableMode, targetType) {
this._super.apply(this, arguments);
$('.oe_share_bump').click($.proxy(this.post_bump, this));
this.targetType = targetType;
},
renderElement: function () {
if (! this.target_type) {
this._super();
}
else if (this.target_type == 'social-alert') {
$('.row .question').before(qweb.render('website.social_alert', {medias: this.social_list}));
}
else {
this.template = 'website.social_modal';
$('body').append(qweb.render(this.template, {medias: this.social_list, target_type: this.target_type}));
start: function () {
var def = this._super.apply(this, arguments);
this._onMouseEnter();
return def;
},
_bindSocialEvent: function () {
this._super.apply(this, arguments);
$('.oe_share_bump').click($.proxy(this._postBump, this));
},
_render: function () {
if (!this.targetType) {
this._super.apply(this, arguments);
} else if (this.targetType === 'social-alert') {
$('.row .question').before(qweb.render('website.social_alert', {medias: this.socialList}));
} else {
$('body').append(qweb.render('website.social_modal', {medias: this.socialList, target_type: this.targetType}));
$('#oe_social_share_modal').modal('show');
}
},
post_bump: function () {
ajax.jsonRpc('/forum/post/bump', 'call', {
'post_id': this.element.data('id'),
_postBump: function () {
this._rpc({ // FIXME
route: '/forum/post/bump',
params: {
post_id: this.element.data('id'),
},
});
}
},
});
$.when(base.ready(), tmpl).done(function() {
base.ready().then(function () {
// Store social share data to display modal on next page
$(document.body).on('click', ':not(.karma_required).oe_social_share_call', function() {
var social_data = {};
social_data['target_type'] = $(this).data('social-target-type');
sessionStorage.setItem('social_share', JSON.stringify(social_data));
$(document.body).on('click', ':not(.karma_required).oe_social_share_call', function () {
sessionStorage.setItem('social_share', JSON.stringify({
targetType: $(this).data('social-target-type'),
}));
});
// Retrieve stored social data
if(sessionStorage.getItem('social_share')){
var social_data = JSON.parse(sessionStorage.getItem('social_share'));
new ForumShare($(this), social_data['target_type']);
if (sessionStorage.getItem('social_share')) {
var socialData = JSON.parse(sessionStorage.getItem('social_share'));
(new ForumShare(null, false, socialData.targetType)).attachTo($(document.body));
sessionStorage.removeItem('social_share');
}
// Display an alert if post has no reply and is older than 10 days
if ($('.oe_js_bump').length) {
var $question_container = $('.oe_js_bump');
new ForumShare($question_container, 'social-alert');
var $questionContainer = $('.oe_js_bump');
if ($questionContainer.length) {
new ForumShare(null, false, 'social-alert').attachTo($questionContainer);
}
});
return {};
});
+1 -1
View File
@@ -1561,7 +1561,7 @@
<!-- User Navbar -->
<template id="content_new_forum" inherit_id="website.user_navbar">
<xpath expr="//div[@id='o_website_add_page_modal']//ul" position="inside">
<xpath expr="//div[@id='o_new_content_menu_choices']//ul" position="inside">
<li groups="website.group_website_designer"><a href="#" data-action="new_forum"><i class="fa fa-comment"/><p>New Forum</p></a></li>
</xpath>
</template>
@@ -3,15 +3,19 @@ odoo.define('website_gengo.website_gengo', function (require) {
var ajax = require('web.ajax');
var core = require('web.core');
var Dialog = require('web.Dialog');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var editor = require('web_editor.editor');
var weContext = require('web_editor.context');
require('web_editor.editor');
var translate = require('web_editor.translate');
var website = require('website.website');
var websiteNavbarData = require('website.navbar');
var WebsiteNavbar = websiteNavbarData.WebsiteNavbar;
var qweb = core.qweb;
var _t = core._t;
if (!translate.edit_translations) {
if (!weContext.getExtra().edit_translations) {
// Temporary hack until the editor bar is moved to the web client
return;
}
@@ -23,20 +27,23 @@ translate.Class.include({
'click a[data-action=translation_gengo_post]': 'translation_gengo_post',
'click a[data-action=translation_gengo_info]': 'translation_gengo_info',
}),
edit:function () {
this.gengo_translate = true;
var self = this;
start: function () {
var def = this._super.apply(this, arguments);
var gengo_langs = ["ar_SY","id_ID","nl_NL","fr_CA","pl_PL","zh_TW","sv_SE","ko_KR","pt_PT","en_US","ja_JP","es_ES","zh_CN","de_DE","fr_FR","fr_BE","ru_RU","it_IT","pt_BR","pt_BR","th_TH","nb_NO","ro_RO","tr_TR","bg_BG","da_DK","en_GB","el_GR","vi_VN","he_IL","hu_HU","fi_FI"];
if (gengo_langs.indexOf(base.get_context()['lang']) !== -1){
self.$('.gengo_post,.gengo_wait,.gengo_inprogress,.gengo_info').remove();
self.$('button[data-action=save]')
.after(qweb.render('website.ButtonGengoTranslator'));
if (gengo_langs.indexOf(weContext.get().lang) >= 0) {
this.$('.gengo_post,.gengo_wait,.gengo_inprogress,.gengo_info').remove();
this.$('button[data-action=save]')
.after(qweb.render('website.ButtonGengoTranslator'));
}
this._super.apply(this, arguments);
this.translation_gengo_display();
return def;
},
translation_gengo_display:function(){
translation_gengo_display: function () {
var self = this;
if($('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').length === 0){
if ($('[data-oe-translation-state="to_translate"], [data-oe-translation-state="None"]').length === 0){
self.$el.find('.gengo_post').addClass("hidden");
self.$el.find('.gengo_inprogress').removeClass("hidden");
}
@@ -49,8 +56,9 @@ translate.Class.include({
});
ajax.jsonRpc('/website/check_gengo_set', 'call', {
}).then(function (res) {
var dialog;
if (res === 0){
var dialog = new GengoTranslatorPostDialog(self.new_words);
dialog = new GengoTranslatorPostDialog(self.new_words);
dialog.appendTo($(document.body));
dialog.on('service_level', this, function () {
var gengo_service_level = dialog.$el.find(".form-control").val();
@@ -72,16 +80,16 @@ translate.Class.include({
});
ajax.jsonRpc('/website_gengo/set_translations', 'call', {
'data': trans,
'lang': base.get_context()['lang'],
'lang': weContext.get().lang,
}).then(function () {
ajax.jsonRpc('/website/post_gengo_jobs', 'call', {});
self.save_and_reload();
}).fail(function () {
alert("Could not Post translation");
Dialog.alert(null, _t("Could not Post translation"));
});
});
}else{
var dialog = new GengoApiConfigDialog(res);
} else {
dialog = new GengoApiConfigDialog(res);
dialog.appendTo($(document.body));
dialog.on('set_config', this, function () {
dialog.$el.modal('hide');
@@ -90,15 +98,14 @@ translate.Class.include({
});
},
translation_gengo_info: function () {
var repr = $(document.documentElement).data('mainObject');
var translated_ids = [];
$('[data-oe-translation-state="translated"]').each(function () {
translated_ids.push($(this).attr('data-oe-translation-id'));
});
ajax.jsonRpc('/website/get_translated_length', 'call', {
'translated_ids': translated_ids,
'lang': base.get_context()['lang'],
}).done(function(res){
'lang': weContext.get().lang,
}).done(function (res){
var dialog = new GengoTranslatorStatisticDialog(res);
dialog.appendTo($(document.body));
});
@@ -106,14 +113,14 @@ translate.Class.include({
});
var GengoTranslatorPostDialog = Widget.extend({
events: _.extend({}, website.TopBar.prototype.events, {
events: _.extend({}, WebsiteNavbar.prototype.events, {
'hidden.bs.modal': 'destroy',
'click button[data-action=service_level]': function () {
this.trigger('service_level');
},
}),
template: 'website.GengoTranslatorPostDialog',
init:function(new_words){
init: function (new_words){
this.new_words = new_words;
return this._super.apply(this, arguments);
},
@@ -123,11 +130,11 @@ var GengoTranslatorPostDialog = Widget.extend({
});
var GengoTranslatorStatisticDialog = Widget.extend({
events: _.extend({}, website.TopBar.prototype.events, {
events: _.extend({}, WebsiteNavbar.prototype.events, {
'hidden.bs.modal': 'destroy',
}),
template: 'website.GengoTranslatorStatisticDialog',
init:function(res){
init: function (res) {
var self = this;
this.inprogess = 0;
this.new_words = 0;
@@ -147,19 +154,19 @@ var GengoTranslatorStatisticDialog = Widget.extend({
});
var GengoApiConfigDialog = Widget.extend({
events: _.extend({}, website.TopBar.prototype.events, {
events: _.extend({}, WebsiteNavbar.prototype.events, {
'hidden.bs.modal': 'destroy',
'click button[data-action=set_config]': 'set_config'
}),
template: 'website.GengoApiConfigDialog',
init:function(company_id){
init:function (company_id){
this.company_id = company_id;
return this._super.apply(this, arguments);
},
start: function (res) {
this.$el.modal(this.res);
},
set_config:function(){
set_config: function () {
var self = this;
var public_key = this.$el.find("#gengo_public_key")[0].value;
var private_key = this.$el.find("#gengo_private_key")[0].value;
@@ -167,25 +174,25 @@ var GengoApiConfigDialog = Widget.extend({
var sandbox = this.$el.find("#gengo_sandbox")[0].checked;
var pub_el = this.$el.find(".gengo_group_public")[0];
var pri_el = this.$el.find(".gengo_group_private")[0];
if(! public_key){
if (! public_key){
$(pub_el).addClass("has-error");
}
else{
else {
$(pub_el).removeClass("has-error");
}
if(! private_key){
if (! private_key){
$(pri_el).addClass("has-error");
}
else{
else {
$(pri_el).removeClass("has-error");
}
if(public_key && private_key){
if (public_key && private_key){
ajax.jsonRpc('/website/set_gengo_config', 'call', {
'config': {'gengo_public_key':public_key,'gengo_private_key':private_key,'gengo_auto_approve':auto_approve,'gengo_sandbox':sandbox},
}).then(function () {
self.trigger('set_config');
}).fail(function () {
alert("Could not submit ! Try Again");
Dialog.alert(null, _t("Could not submit ! Try Again"));
});
}
}
@@ -6,8 +6,8 @@
<a class="btn btn-danger hidden gengo_inprogress disabled" href="#"> <i class="fa fa-clock-o"></i> Translation in Progress</a>
<a class="btn btn-link gengo_info" data-action="translation_gengo_info">Count Words</a>
</t>
<t t-name="website.TranslatorDialog">
<div class="modal fade oe_website_translator" tabindex="-1" role="dialog">
<t t-name="web_editor.TranslatorInfoDialog">
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -21,7 +21,7 @@
Here are the visuals used to help you translate efficiently:
<ul class="oe_translate_examples">
<li style="background:#ffffb6;">
Content to translate or you can post them to <b><a href="http://gengo.com/" >Gengo</a></b> for translation.
Content to translate or you can post them to <b><a href="http://gengo.com/" >Gengo</a></b> for translation.
</li>
<li data-oe-translation-state="inprogress">
Translation in process (Gengo)
@@ -54,7 +54,7 @@
</t>
<t t-name="website.GengoTranslatorPostDialog">
<div class="modal fade oe_website_translator" tabindex="-1" role="dialog">
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -81,7 +81,7 @@
</t>
<t t-name="website.GengoTranslatorStatisticDialog">
<div class="modal fade oe_website_translator" tabindex="-1" role="dialog">
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -106,7 +106,7 @@
</div>
</t>
<t t-name="website.GengoApiConfigDialog">
<div class="modal fade oe_website_translator" tabindex="-1" role="dialog">
<div class="modal fade" tabindex="-1" role="dialog">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
@@ -123,7 +123,7 @@
<label class="control-label">Public key</label>
</h4>
<input type="text" class="form-control url url-source" id="gengo_public_key" placeholder="Paste public key here"/>
</li>
<li class="list-group-item form-group gengo_group_private">
<h4 class="list-group-item-heading">
@@ -507,7 +507,7 @@
<!-- User Navbar -->
<template id="content_new_job_offer" inherit_id="website.user_navbar">
<xpath expr="//div[@id='o_website_add_page_modal']//ul" position="inside">
<xpath expr="//div[@id='o_new_content_menu_choices']//ul" position="inside">
<li groups="hr_recruitment.group_hr_recruitment_manager"><a href="/jobs/add" class="js_link2post"><i class="fa fa-briefcase"/><p>New Job Offer</p></a></li>
</xpath>
</template>
@@ -1,12 +1,12 @@
odoo.define('website_links.website_links', function (require) {
'use strict';
require('web.dom_ready');
var ajax = require('web.ajax');
var core = require('web.core');
var rpc = require('web.rpc');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var website = require('website.website');
var qweb = core.qweb;
var _t = core._t;
@@ -14,27 +14,27 @@ var ZeroClipboard = window.ZeroClipboard;
var exports = {};
if(!$('.o_website_links_create_tracked_url').length) {
if (!$('.o_website_links_create_tracked_url').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_links_create_tracked_url'");
}
var SelectBox = Widget.extend({
init: function(obj) {
init: function (obj) {
this.obj = obj;
},
start: function(element, placeholder) {
start: function (element, placeholder) {
var self = this;
this.element = element;
this.placeholder = placeholder;
this.fetch_objects().then(function(results) {
this.fetch_objects().then(function (results) {
self.objects = results;
element.select2({
placeholder: self.placeholder,
allowClear: true,
createSearchChoice:function(term) {
if(self.object_exists(term)) { return null; }
createSearchChoice: function (term) {
if (self.object_exists(term)) { return null; }
return {id:term, text:_.str.sprintf("Create '%s'", term)};
},
@@ -43,33 +43,33 @@ if(!$('.o_website_links_create_tracked_url').length) {
data: self.objects,
});
element.on('change', function(e) {
element.on('change', function (e) {
self.on_change(e);
});
});
},
fetch_objects: function() {
fetch_objects: function () {
return rpc.query({
model: this.obj,
method: 'search_read',
})
.then(function(result) {
return _.map(result, function(val) {
.then(function (result) {
return _.map(result, function (val) {
return {id: val.id, text:val.name};
});
});
},
object_exists: function(query) {
return _.find(this.objects, function(val) {
object_exists: function (query) {
return _.find(this.objects, function (val) {
return val.text.toLowerCase() === query.toLowerCase();
}) !== undefined;
},
on_change: function(e) {
if(e.added && _.isString(e.added.id)) {
on_change: function (e) {
if (e.added && _.isString(e.added.id)) {
this.create_object(e.added.id);
}
},
create_object: function(name) {
create_object: function (name) {
var self = this;
return rpc.query({
@@ -77,43 +77,43 @@ if(!$('.o_website_links_create_tracked_url').length) {
method: 'create',
args: [{name:name}],
})
.then(function(record) {
.then(function (record) {
self.element.attr('value', record);
self.objects.push({'id': record, 'text': name});
});
},
});
var RecentLinkBox = Widget.extend({
template: 'website_links.RecentLink',
events: {
'click .btn_shorten_url_clipboard':'toggle_copy_button',
'click .o_website_links_edit_code':'edit_code',
'click .o_website_links_ok_edit':function(e) {
'click .o_website_links_ok_edit':function (e) {
e.preventDefault();
this.submit_code();
},
'click .o_website_links_cancel_edit':function(e) {
'click .o_website_links_cancel_edit': function (e) {
e.preventDefault();
this.cancel_edit();
},
'submit #o_website_links_edit_code_form':function(e) {
'submit #o_website_links_edit_code_form': function (e) {
e.preventDefault();
this.submit_code();
},
},
init: function(parent, link_obj) {
init: function (parent, link_obj) {
this._super(parent);
this.link_obj = link_obj;
this.animating_copy = false;
},
start: function() {
start: function () {
new ZeroClipboard(this.$('.btn_shorten_url_clipboard'));
},
toggle_copy_button: function() {
toggle_copy_button: function () {
var self = this;
if(!this.animating_copy) {
if (!this.animating_copy) {
this.animating_copy = true;
var top = this.$('.o_website_links_short_url').position().top;
this.$('.o_website_links_short_url').clone()
@@ -127,19 +127,19 @@ if(!$('.o_website_links_create_tracked_url').length) {
.animate({
opacity: 0,
top: "-=20",
}, 500, function() {
}, 500, function () {
self.$('.animated-link').remove();
self.animating_copy = false;
});
}
},
remove: function() {
remove: function () {
this.getParent().remove_link(this);
},
notification: function(message) {
notification: function (message) {
this.$('.notification').append('<strong>' + message + '</strong>');
},
edit_code: function() {
edit_code: function () {
var init_code = this.$('#o_website_links_code').html();
this.$('#o_website_links_code').html("<form style='display:inline;' id='o_website_links_edit_code_form'><input type='hidden' id='init_code' value='" + init_code + "'/><input type='text' id='new_code' value='" + init_code + "'/></form>");
@@ -147,7 +147,7 @@ if(!$('.o_website_links_create_tracked_url').length) {
this.$('.copy-to-clipboard').hide();
this.$('.o_website_links_edit_tools').show();
},
cancel_edit: function() {
cancel_edit: function () {
this.$('.o_website_links_edit_code').show();
this.$('.copy-to-clipboard').show();
this.$('.o_website_links_edit_tools').hide();
@@ -159,13 +159,13 @@ if(!$('.o_website_links_create_tracked_url').length) {
this.$('#code-error').remove();
this.$('#o_website_links_code form').remove();
},
submit_code: function() {
submit_code: function () {
var self = this;
var init_code = this.$('#o_website_links_edit_code_form #init_code').val();
var new_code = this.$('#o_website_links_edit_code_form #new_code').val();
if(new_code === '') {
if (new_code === '') {
self.$('.o_website_links_code_error').html("The code cannot be left empty");
self.$('.o_website_links_code_error').show();
return;
@@ -183,22 +183,22 @@ if(!$('.o_website_links_create_tracked_url').length) {
// Update button copy to clipboard
self.$('.btn_shorten_url_clipboard').attr('data-clipboard-text', host + new_code);
// Show action again
self.$('.o_website_links_edit_code').show();
self.$('.copy-to-clipboard').show();
self.$('.o_website_links_edit_tools').hide();
}
if(init_code == new_code) {
if (init_code === new_code) {
show_new_code(new_code);
}
else {
ajax.jsonRpc('/website_links/add_code', 'call', {'init_code':init_code, 'new_code':new_code})
.then(function(result) {
.then(function (result) {
show_new_code(result[0].code);
})
.fail(function() {
.fail(function () {
self.$('.o_website_links_code_error').show();
self.$('.o_website_links_code_error').html("This code is already taken");
}) ;
@@ -207,44 +207,44 @@ if(!$('.o_website_links_create_tracked_url').length) {
});
var RecentLinks = Widget.extend({
init: function() {
init: function () {
this._super();
},
get_recent_links: function(filter) {
get_recent_links: function (filter) {
var self = this;
ajax.jsonRpc('/website_links/recent_links', 'call', {'filter':filter, 'limit':20})
.then(function(result) {
_.each(result.reverse(), function(link) {
.then(function (result) {
_.each(result.reverse(), function (link) {
self.add_link(link);
});
self.update_notification();
})
.fail(function() {
.fail(function () {
var message = _t("Unable to get recent links");
self.$el.append("<div class='alert alert-danger'>" + message + "</div>");
});
});
},
add_link: function(link) {
add_link: function (link) {
var nb_links = this.getChildren().length;
var recent_link_box = new RecentLinkBox(this, link);
recent_link_box.prependTo(this.$el);
$('.link-tooltip').tooltip();
if(nb_links === 0) {
if (nb_links === 0) {
this.update_notification();
}
},
remove_links: function() {
remove_links: function () {
_.invoke(this.getChildren(), 'remove');
},
remove_link: function(link) {
remove_link: function (link) {
link.destroy();
},
update_notification: function() {
if(this.getChildren().length === 0) {
update_notification: function () {
if (this.getChildren().length === 0) {
var message = _t("You don't have any recent links.");
$('.o_website_links_recent_links_notification').html("<div class='alert alert-info'>" + message + "</div>");
}
@@ -256,9 +256,9 @@ if(!$('.o_website_links_create_tracked_url').length) {
ajax.loadXML('/website_links/static/src/xml/recent_link.xml', qweb);
base.ready().done(function() {
base.ready().done(function () {
ZeroClipboard.config({swfPath: location.origin + "/website_links/static/lib/zeroclipboard/ZeroClipboard.swf" });
ZeroClipboard.config({swfPath: window.location.origin + "/website_links/static/lib/zeroclipboard/ZeroClipboard.swf" });
// UTMS selects widgets
var campaign_select = new SelectBox('utm.campaign');
@@ -275,33 +275,33 @@ if(!$('.o_website_links_create_tracked_url').length) {
recent_links.appendTo($("#o_website_links_recent_links"));
recent_links.get_recent_links('newest');
$('#filter-newest-links').click(function() {
$('#filter-newest-links').click(function () {
recent_links.remove_links();
recent_links.get_recent_links('newest');
});
$('#filter-most-clicked-links').click(function() {
$('#filter-most-clicked-links').click(function () {
recent_links.remove_links();
recent_links.get_recent_links('most-clicked');
});
$('#filter-recently-used-links').click(function() {
$('#filter-recently-used-links').click(function () {
recent_links.remove_links();
recent_links.get_recent_links('recently-used');
});
// Clipboard Library
var client = new ZeroClipboard($("#btn_shorten_url"));
$("#generated_tracked_link a").click(function() {
// Clipboard Library
new ZeroClipboard($("#btn_shorten_url"));
$("#generated_tracked_link a").click(function () {
$("#generated_tracked_link a").text("Copied").removeClass("btn-primary").addClass("btn-success");
setTimeout(function() {
setTimeout(function () {
$("#generated_tracked_link a").text("Copy").removeClass("btn-success").addClass("btn-primary");
}, '5000');
});
$('#url').on('keyup', function(e) {
if($('#btn_shorten_url').hasClass('btn-copy') && e.which != 13) {
$('#url').on('keyup', function (e) {
if ($('#btn_shorten_url').hasClass('btn-copy') && e.which !== 13) {
$('#btn_shorten_url').removeClass('btn-success btn-copy').addClass('btn-primary').html('Get tracked link');
$('#generated_tracked_link').css('display', 'none');
$('.o_website_links_utm_forms').show();
@@ -309,9 +309,9 @@ if(!$('.o_website_links_create_tracked_url').length) {
});
var url_copy_animating = false;
$('#btn_shorten_url').click(function() {
if($('#btn_shorten_url').hasClass('btn-copy')) {
if(!url_copy_animating) {
$('#btn_shorten_url').click(function () {
if ($('#btn_shorten_url').hasClass('btn-copy')) {
if (!url_copy_animating) {
url_copy_animating = true;
$('#generated_tracked_link').clone()
@@ -325,18 +325,18 @@ if(!$('.o_website_links_create_tracked_url').length) {
.animate({
opacity: 0,
bottom: "+=20",
}, 500, function() {
}, 500, function () {
$('.url-animated-link').remove();
url_copy_animating = false;
});
}
}
});
// Add the RecentLinkBox widget and send the form when the user generate the link
$("#o_website_links_link_tracker_form").submit(function(event) {
if($('#btn_shorten_url').hasClass('btn-copy')) {
// Add the RecentLinkBox widget and send the form when the user generate the link
$("#o_website_links_link_tracker_form").submit(function (event) {
if ($('#btn_shorten_url').hasClass('btn-copy')) {
event.preventDefault();
return;
}
@@ -345,27 +345,26 @@ if(!$('.o_website_links_create_tracked_url').length) {
event.stopPropagation();
// Get URL and UTMs
var url = $("#url").val();
var campaign_id = $('#campaign-select').attr('value');
var medium_id = $('#channel-select').attr('value');
var source_id = $('#source-select').attr('value');
var params = {};
params.url = $("#url").val();
if(campaign_id !== '') { params.campaign_id = parseInt(campaign_id); }
if(medium_id !== '') { params.medium_id = parseInt(medium_id); }
if(source_id !== '') { params.source_id = parseInt(source_id); }
if (campaign_id !== '') { params.campaign_id = parseInt(campaign_id); }
if (medium_id !== '') { params.medium_id = parseInt(medium_id); }
if (source_id !== '') { params.source_id = parseInt(source_id); }
$('#btn_shorten_url').text(_t('Generating link...'));
ajax.jsonRpc("/website_links/new", 'call', params)
.then(function (result) {
if('error' in result) {
if ('error' in result) {
// Handle errors
if(result.error === 'empty_url') {
if (result.error === 'empty_url') {
$('.notification').html("<div class='alert alert-danger'>The URL is empty.</div>");
}
else if(result.error == 'url_not_found') {
else if (result.error === 'url_not_found') {
$('.notification').html("<div class='alert alert-danger'>URL not found (404)</div>");
}
else {
@@ -1,28 +1,28 @@
odoo.define('website_links.charts', function (require) {
'use strict';
require('web.dom_ready');
var rpc = require('web.rpc');
var Widget = require('web.Widget');
var base = require('web_editor.base');
var core = require('web.core');
var website = require('website.website');
var _t = core._t;
var exports = {};
if(!$('.o_website_links_chart').length) {
if (!$('.o_website_links_chart').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_links_chart'");
}
var BarChart = Widget.extend({
init: function($element, begin_date, end_date, dates) {
init: function ($element, begin_date, end_date, dates) {
this.$element = $element;
this.begin_date = begin_date;
this.end_date = end_date;
this.number_of_days = this.end_date.diff(this.begin_date, 'days') + 2;
this.dates = dates;
},
start: function() {
start: function () {
var self = this;
// Accessor functions
@@ -34,7 +34,7 @@ if(!$('.o_website_links_chart').length) {
var nb_values = ticks.length;
var keep_one_of = Math.max(1, Math.floor(nb_values / nb_desired_ticks));
return _.filter(ticks, function(d, i) {
return _.filter(ticks, function (d, i) {
return i % keep_one_of === 0;
});
}
@@ -42,14 +42,14 @@ if(!$('.o_website_links_chart').length) {
// Fill data for each day (with 0 click for days without data)
var clicks_array = [];
var begin_date_copy = this.begin_date;
for(var i = 0 ; i < this.number_of_days ; i++) {
for (var i = 0 ; i < this.number_of_days ; i++) {
var date_key = begin_date_copy.format('YYYY-MM-DD');
clicks_array.push([date_key, (date_key in this.dates) ? this.dates[date_key] : 0]);
begin_date_copy.add(1, 'days');
}
// Set title
var nb_clicks = _.reduce(clicks_array, function(total, val) { return total + val[1] ; }, 0);
var nb_clicks = _.reduce(clicks_array, function (total, val) { return total + val[1] ; }, 0);
$(this.$element + ' .title').html(nb_clicks + _t(' clicks'));
// Fit data into the NVD3 scheme
@@ -57,10 +57,10 @@ if(!$('.o_website_links_chart').length) {
chart_data[0]['key'] = _t('# of clicks');
chart_data[0]['values'] = clicks_array;
nv.addGraph(function() {
nv.addGraph(function () {
var chart = nv.models.lineChart()
.x(function(d) { return getDate(d); })
.y(function(d) { return getNbClicks(d); })
.x(function (d) { return getDate(d); })
.y(function (d) { return getNbClicks(d); })
.showYAxis(true)
.showXAxis(true);
@@ -68,8 +68,8 @@ if(!$('.o_website_links_chart').length) {
var tick_values = getPrunedTickValues(chart_data[0]['values'], 10);
chart.xAxis
.tickFormat(function(d) { return d3.time.format("%d/%m/%y")(new Date(d)); })
.tickValues(_.map(tick_values, function(d) { return getDate(d).getTime(); }))
.tickFormat(function (d) { return d3.time.format("%d/%m/%y")(new Date(d)); })
.tickValues(_.map(tick_values, function (d) { return getDate(d).getTime(); }))
.rotateLabels(55);
chart.yAxis
@@ -86,16 +86,16 @@ if(!$('.o_website_links_chart').length) {
});
var PieChart = Widget.extend({
init: function($element, data) {
init: function ($element, data) {
this.data = data;
this.$element = $element;
},
start: function() {
start: function () {
var self = this;
// Process country data to fit into the NVD3 scheme
var processed_data = [];
for(var i = 0 ; i < this.data.length ; i++) {
for (var i = 0 ; i < this.data.length ; i++) {
var country_name = this.data[i]['country_id'] ? this.data[i]['country_id'][1] : _t('Undefined');
processed_data.push({'label':country_name + ' (' + this.data[i]['country_id_count'] + ')', 'value':this.data[i]['country_id_count']});
}
@@ -103,10 +103,10 @@ if(!$('.o_website_links_chart').length) {
// Set title
$(this.$element + ' .title').html(this.data.length + _t(' countries'));
nv.addGraph(function() {
nv.addGraph(function () {
var chart = nv.models.pieChart()
.x(function(d) { return d.label; })
.y(function(d) { return d.value; })
.x(function (d) { return d.label; })
.y(function (d) { return d.value; })
.showLabels(false);
d3.select(self.$element + ' svg')
@@ -119,7 +119,7 @@ if(!$('.o_website_links_chart').length) {
},
});
base.ready().done(function() {
base.ready().done(function () {
// Resize the chart when a tab is opened, because NVD3 automatically reduce the size
// of the chart to 5px width when the bootstrap tab is closed.
var charts = {};
@@ -134,7 +134,7 @@ if(!$('.o_website_links_chart').length) {
var links_domain = ['link_id', '=', parseInt(link_id)];
var total_clicks = function() {
var total_clicks = function () {
return rpc.query({
model: 'link.tracker.click',
method: 'search_count',
@@ -142,7 +142,7 @@ if(!$('.o_website_links_chart').length) {
});
};
var clicks_by_day = function() {
var clicks_by_day = function () {
return rpc.query({
model: 'link.tracker.click',
method: 'read_group',
@@ -151,7 +151,7 @@ if(!$('.o_website_links_chart').length) {
});
};
var clicks_by_country = function() {
var clicks_by_country = function () {
return rpc.query({
model: 'link.tracker.click',
method: 'read_group',
@@ -160,7 +160,7 @@ if(!$('.o_website_links_chart').length) {
});
};
var last_week_clicks_by_country = function() {
var last_week_clicks_by_country = function () {
var interval = moment().subtract(7, 'days').format("YYYY-MM-DD");
return rpc.query({
model: 'link.tracker.click',
@@ -170,7 +170,7 @@ if(!$('.o_website_links_chart').length) {
});
};
var last_month_clicks_by_country = function() {
var last_month_clicks_by_country = function () {
var interval = moment().subtract(30, 'days').format("YYYY-MM-DD");
return rpc.query({
model: 'link.tracker.click',
@@ -180,20 +180,20 @@ if(!$('.o_website_links_chart').length) {
});
};
$.when(total_clicks(),
$.when(total_clicks(),
clicks_by_day(),
clicks_by_country(),
last_week_clicks_by_country(),
last_month_clicks_by_country())
.done(function(total_clicks, clicks_by_day, clicks_by_country, last_week_clicks_by_country, last_month_clicks_by_country) {
.done(function (total_clicks, clicks_by_day, clicks_by_country, last_week_clicks_by_country, last_month_clicks_by_country) {
if(total_clicks) {
if (total_clicks) {
var formatted_clicks_by_day = {};
var begin_date, end_date;
for(var i = 0 ; i < clicks_by_day.length ; i++) {
for (var i = 0 ; i < clicks_by_day.length ; i++) {
var date = moment(clicks_by_day[i]['create_date:day'], "DD MMMM YYYY");
if (i === 0) { begin_date = date; }
if (i == clicks_by_day.length - 1) { end_date = date; }
if (i === clicks_by_day.length - 1) { end_date = date; }
formatted_clicks_by_day[date.format("YYYY-MM-DD")] = clicks_by_day[i]['create_date_count'];
}
@@ -233,16 +233,16 @@ if(!$('.o_website_links_chart').length) {
});
// Copy to clipboard link
ZeroClipboard.config({swfPath: location.origin + "/website_links/static/lib/zeroclipboard/ZeroClipboard.swf" });
ZeroClipboard.config({swfPath: window.location.origin + "/website_links/static/lib/zeroclipboard/ZeroClipboard.swf" });
new ZeroClipboard($('.copy-to-clipboard'));
var animating_copy = false;
$('.copy-to-clipboard').on('click', function(e) {
$('.copy-to-clipboard').on('click', function (e) {
e.preventDefault();
if(!animating_copy) {
if (!animating_copy) {
animating_copy = true;
$('.o_website_links_short_url').clone()
@@ -256,7 +256,7 @@ if(!$('.o_website_links_chart').length) {
.animate({
opacity: 0,
bottom: "+=20",
}, 500, function() {
}, 500, function () {
$('.animated-link').remove();
animating_copy = false;
});
@@ -1,17 +1,15 @@
odoo.define('website_links.code_editor', function (require) {
'use strict';
require('web.dom_ready');
var ajax = require('web.ajax');
var base = require('web_editor.base');
var website = require('website.website');
if(!$('.o_website_links_edit_code').length) {
if (!$('.o_website_links_edit_code').length) {
return $.Deferred().reject("DOM doesn't contain '.o_website_links_edit_code'");
}
// Edit the short URL code
$('.o_website_links_edit_code').on('click', function(e) {
$('.o_website_links_edit_code').on('click', function (e) {
e.preventDefault();
var init_code = $('#o_website_links_code').html();
@@ -21,13 +19,13 @@ if(!$('.o_website_links_edit_code').length) {
$('.copy-to-clipboard').hide();
$('.o_website_links_edit_tools').show();
$('.o_website_links_cancel_edit').on('click', function(e) {
$('.o_website_links_cancel_edit').on('click', function (e) {
e.preventDefault();
$('.o_website_links_edit_code').show();
$('.copy-to-clipboard').show();
$('.o_website_links_edit_tools').hide();
$('.o_website_links_code_error').hide();
$('.o_website_links_edit_tools').hide();
$('.o_website_links_code_error').hide();
var old_code = $('#edit-code-form #init_code').val();
$('#o_website_links_code').html(old_code);
@@ -40,7 +38,7 @@ if(!$('.o_website_links_edit_code').length) {
var init_code = $('#edit-code-form #init_code').val();
var new_code = $('#edit-code-form #new_code').val();
if(new_code === '') {
if (new_code === '') {
self.$('.o_website_links_code_error').html("The code cannot be left empty");
self.$('.o_website_links_code_error').show();
return;
@@ -58,38 +56,36 @@ if(!$('.o_website_links_edit_code').length) {
// Update button copy to clipboard
$('.copy-to-clipboard').attr('data-clipboard-text', host + new_code);
// Show action again
$('.o_website_links_edit_code').show();
$('.copy-to-clipboard').show();
$('.o_website_links_edit_tools').hide();
}
if(init_code == new_code) {
if (init_code === new_code) {
show_new_code(new_code);
}
else {
ajax.jsonRpc('/website_links/add_code', 'call', {'init_code':init_code, 'new_code':new_code})
.then(function(result) {
.then(function (result) {
show_new_code(result[0].code);
})
.fail(function() {
.fail(function () {
$('.o_website_links_code_error').show();
$('.o_website_links_code_error').html("This code is already taken");
}) ;
}
}
$('#edit-code-form').submit(function(e) {
$('#edit-code-form').submit(function (e) {
e.preventDefault();
submit_code();
});
$('.o_website_links_ok_edit').click(function(e) {
$('.o_website_links_ok_edit').click(function (e) {
e.preventDefault();
submit_code();
});
});
});
+24 -19
View File
@@ -1,17 +1,20 @@
odoo.define('website_mail.follow', function (require) {
'use strict';
var ajax = require('web.ajax');
var animation = require('web_editor.snippets.animation');
var sAnimation = require('website.content.snippets.animation');
animation.registry.follow = animation.Class.extend({
selector: ".js_follow",
start: function (editable_mode) {
sAnimation.registry.follow = sAnimation.Class.extend({
selector: '.js_follow',
start: function () {
var self = this;
this.is_user = false;
ajax.jsonRpc('/website_mail/is_follower', 'call', {
model: this.$target.data('object'),
res_id: this.$target.data('id'),
this._rpc({
route: '/website_mail/is_follower',
params: {
model: this.$target.data('object'),
res_id: this.$target.data('id'),
},
}).always(function (data) {
self.is_user = data.is_user;
self.email = data.email;
@@ -20,16 +23,16 @@ animation.registry.follow = animation.Class.extend({
});
// not if editable mode to allow designer to edit
if (!editable_mode) {
if (!this.editableMode) {
$('.js_follow > .input-group-btn.hidden').removeClass("hidden");
this.$target.find('.js_follow_btn, .js_unfollow_btn').on('click', function (event) {
event.preventDefault();
self.on_click();
self._onClick();
});
}
return;
return this._super.apply(this, arguments);
},
on_click: function () {
_onClick: function () {
var self = this;
var $email = this.$target.find(".js_follow_email");
@@ -41,17 +44,20 @@ animation.registry.follow = animation.Class.extend({
var email = $email.length ? $email.val() : false;
if (email || this.is_user) {
ajax.jsonRpc('/website_mail/follow', 'call', {
'id': +this.$target.data('id'),
'object': this.$target.data('object'),
'message_is_follower': this.$target.attr("data-follow") || "off",
'email': email,
this._rpc({
route: '/website_mail/follow',
params: {
'id': +this.$target.data('id'),
'object': this.$target.data('object'),
'message_is_follower': this.$target.attr("data-follow") || "off",
'email': email,
},
}).then(function (follow) {
self.toggle_subscription(follow, email);
});
}
},
toggle_subscription: function(follow, email) {
toggle_subscription: function (follow, email) {
follow = follow || (!email && this.$target.attr('data-unsubscribe'));
if (follow) {
this.$target.find(".js_follow_btn").addClass("hidden");
@@ -67,5 +73,4 @@ animation.registry.follow = animation.Class.extend({
this.$target.attr("data-follow", follow ? 'on' : 'off');
},
});
});

Some files were not shown because too many files have changed in this diff Show More