1 openerp.website = function(instance) {
2 var _lt = instance.web._lt;
3 instance.website.EditorBar = instance.web.Widget.extend({
4 template: 'Website.EditorBar',
6 'click button[data-action=edit]': 'edit',
7 'click button[data-action=save]': 'save',
8 'click button[data-action=cancel]': 'cancel',
9 'click button[data-action=snippet]': 'snippet',
13 this._super.apply(this, arguments);
14 this.saving_mutex = new $.Mutex();
19 this.$('button[data-action]').prop('disabled', true)
22 edit: this.$('button[data-action=edit]'),
23 save: this.$('button[data-action=save]'),
24 cancel: this.$('button[data-action=cancel]'),
25 snippet: this.$('button[data-action=snippet]'),
27 this.$buttons.edit.prop('disabled', false).parent().show();
31 this.rte = new instance.website.RTE(this);
32 this.rte.on('change', this, this.proxy('rte_changed'));
35 this._super.apply(this, arguments),
36 this.rte.insertBefore(this.$buttons.snippet.parent())
40 this.$buttons.edit.prop('disabled', true).parent().hide();
41 this.$buttons.cancel.add(this.$buttons.snippet).prop('disabled', false)
42 .add(this.$buttons.save)
44 // TODO: span edition changing edition state (save button)
45 var $editables = $('[data-oe-model]')
47 // FIXME: propagation should make "meta" blocks non-editable in the first place...
48 .not('.oe_snippet_editor')
49 .prop('contentEditable', true)
50 .addClass('oe_editable');
51 var $rte_ables = $editables.filter('div, p, li, section, header, footer').not('[data-oe-type]');
52 var $raw_editables = $editables.not($rte_ables);
54 // temporary fix until we fix ckeditor
55 $raw_editables.each(function () {
56 $(this).parents().add($(this).find('*')).on('click', function(ev) {
62 this.rte.start_edition($rte_ables);
63 $raw_editables.on('keydown keypress cut paste', function (e) {
64 var $target = $(e.target);
65 if ($target.hasClass('oe_dirty')) {
69 $target.addClass('oe_dirty');
70 this.$buttons.save.prop('disabled', false);
73 rte_changed: function () {
74 this.$buttons.save.prop('disabled', false);
79 $('.oe_dirty').each(function (i, v) {
81 // TODO: Add a queue with concurrency limit in webclient
82 // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
83 var def = self.saving_mutex.exec(function () {
84 return self.saveElement($el).then(function () {
85 $el.removeClass('oe_dirty');
87 var data = $el.data();
88 console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
93 return $.when.apply(null, defs).then(function () {
94 window.location.reload();
97 saveElement: function ($el) {
98 var data = $el.data();
99 var html = $el.html();
100 var xpath = data.oeXpath;
102 var $w = $el.clone();
103 $w.removeClass('oe_dirty');
104 _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
106 .removeClass('oe_editable')
107 .prop('contentEditable', false);
108 html = $w.wrap('<div>').parent().html();
110 return (new instance.web.DataSet(this, 'ir.ui.view')).call('save', [data.oeModel, data.oeId, data.oeField, html, xpath]);
112 cancel: function () {
113 window.location.reload();
115 setup_droppable: function () {
117 $('.oe_snippet_drop').remove();
118 var droppable = '<div class="oe_snippet_drop"></div>';
119 var $zone = $('*:not(.oe_snippet) > .container');
120 $zone.before(droppable).after(droppable);
122 $(".oe_snippet_drop").droppable({
123 hoverClass: 'oe_accepting',
124 drop: function( event, ui ) {
125 console.log(event, ui, "DROP");
127 $(event.target).replaceWith($(ui.draggable).html());
128 $('.oe_selected').remove();
129 self.setup_droppable();
133 snippet_start: function () {
134 this.setup_droppable();
136 $('.oe_snippet').draggable().click(function(ev) {
137 $(".oe_snippet_drop").show();
138 $('.oe_selected').removeClass('oe_selected');
139 $(ev.currentTarget).addClass('oe_selected');
143 snippet: function (ev) {
144 $('.oe_snippet_editor').toggle();
148 instance.website.Action = instance.web.Widget.extend({
153 events: { click: 'perform' },
154 init: function (parent, name) {
159 this.$el.text(this.name);
160 return this._super();
166 toggle: function (to) {
167 this.$el.prop('disabled', !to);
170 var Style = instance.website.Style = instance.website.Action.extend({
171 init: function (parent, name, style) {
172 this._super(parent, name);
175 perform: function () {
176 this.getParent().with_editor(function (editor) {
177 editor.applyStyle(new CKEDITOR.style(this.style))
181 var Command = instance.website.Command = instance.website.Action.extend({
182 init: function (parent, name, command) {
183 this._super(parent, name);
184 this.command = command;
186 perform: function () {
187 this.getParent().with_editor(function (editor) {
188 switch (typeof this.command) {
189 case 'string': editor.execCommand(this.command); break;
190 case 'function': this.command(editor); break;
195 var Group = instance.website.ActionGroup = instance.website.Action.extend({
196 template: 'Website.ActionGroup',
197 events: { 'click > button': 'perform' },
199 init: function (parent, name, actions) {
200 this._super(parent, name);
201 this.actions = _(actions).map(this.getParent().proxy('init_command'));
204 this.instances.push(this);
205 var $ul = this.$('ul');
206 return $.when.apply(null, _(this.actions).map(function (action) {
207 var $li = $('<li>').appendTo($ul);
208 return action.appendTo($li);
211 destroy: function () {
212 this.instances = _(this.instances).without(this);
213 return this._super();
215 perform: function () {
216 this.getParent().with_editor(function () {
217 _(this.instances).chain()
220 .invoke('removeClass', 'open');
221 // JS part of bootstrap dropdown does really weird stuff which
222 // interacts quite badly with the RTE thing, so bypass it
223 this.$el.toggleClass('open');
224 }.bind(this), false);
227 toggle: function (to) {
228 this.$('> button').prop('disabled', !to);
232 instance.website.RTE = instance.web.Widget.extend({
234 className: 'oe_right oe_rte_toolbar',
236 [Command, "\uf032", 'bold'],
237 [Command, "\uf033", 'italic'],
238 [Command, "\uf0cd", 'underline'],
239 [Command, "\uf0cc", 'strike'],
240 [Command, "\uf12b", 'superscript'],
241 [Command, "\uf12c", 'subscript'],
242 [Command, "\uf0c1", 'link'],
243 [Command, "\uf127", 'unlink'],
244 [Command, "\uf10d", 'blockquote'],
245 // 'image' uses either filebrowserImageUploadUrl or
246 // filebrowserUploadUrl, and provides a `link` tab. imagebutton only
247 // uses filebrowserImageUploadUrl and does not provide a `link` tab to
248 // hotlink an image from the internets.
249 [Command, "\uf03e", 'image'],
250 // [Command, "\uf030", 'imagebutton'],
252 [Command, "\uf0ca", 'bulletedlist'],
253 [Command, "\uf0cb", 'numberedlist'],
254 [Command, "\uf03b", 'outdent'],
255 [Command, "\uf03c", 'indent']
257 [Group, _lt("Heading"), [
258 [Style, _lt('H1'), { element: 'h1' }],
259 [Style, _lt('H2'), { element: 'h2', }],
260 [Style, _lt('H3'), { element: 'h3', }],
261 [Style, _lt('H4'), { element: 'h4', }],
262 [Style, _lt('H5'), { element: 'h5', }],
263 [Style, _lt('H6'), { element: 'h6', }]
266 [Command, "\uf039", 'justifyblock'],
267 [Command, "\uf036", 'justifyleft'],
268 [Command, "\uf038", 'justifyright'],
269 [Command, "\uf037", 'justifycenter']
272 // editor.ui.items -> possible commands &al
273 // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
278 null, _(this.commands).map(this.proxy('start_command')));
280 init_command: function (command) {
281 var type = command[0], args = command.slice(1);
283 var F = function (args) {
284 return type.apply(this, args);
286 F.prototype = type.prototype;
290 start_command: function (command) {
291 return this.init_command(command).appendTo(this.$el);
293 start_edition: function ($elements) {
297 this.snippet_carousel();
298 CKEDITOR.on('currentInstance', this.proxy('_change_focused_editor'));
300 .not('span, [data-oe-type]')
303 CKEDITOR.inline(this, self._config()).on('change', function () {
304 $this.addClass('oe_dirty');
305 self.trigger('change', this, null);
311 * @param {Function} fn
312 * @param {Boolean} [snapshot=true]
313 * @returns {$.Deferred}
315 with_editor: function (fn, snapshot) {
316 var editor = this._current_editor();
317 if (snapshot !== false) { editor.fire('saveSnapshot'); }
318 return $.when(fn(editor)).then(function () {
319 if (snapshot !== false) { editor.fire('saveSnapshot'); }
325 toggle: function (to) {
326 _(this.getChildren()).chain()
327 .filter(function (child) { return child instanceof instance.website.Action })
328 .invoke('toggle', to);
330 disable: function () {
334 _current_editor: function () {
335 return CKEDITOR.currentInstance;
337 _change_focused_editor: function () {
338 this.toggle(!!CKEDITOR.currentInstance);
340 _config: function () {
342 // Don't load ckeditor's style rules
344 // Remove toolbar entirely, also custom context menu
345 removePlugins: 'toolbar,elementspath,resize,contextmenu,tabletools,liststyle',
347 // Ensure no config file is loaded
350 allowedContent: true,
351 // Don't insert paragraphs around content in e.g. <li>
352 autoParagraph: false,
353 filebrowserImageUploadUrl: "/website/attach",
357 snippet_carousel: function () {
359 $carousels = $("<div/>");
360 $carousels.css({'position': 'absolute', 'top': 0, 'white-space': 'nowrap'});
361 $carousels.insertAfter(self.$el);
363 $(".carousel").each(function() {
364 var $carousel = new instance.website.snippet.carousel(self, this);
365 $carousel.appendTo($carousels);
367 $(document).on("scroll", function () {
368 $carousels.css("top", (-self.$el.offset().top+2) + 'px');
374 instance.website.snippet = {};
375 instance.website.snippet.carousel = instance.web.Widget.extend({
376 template: 'Website.Snipped.carousel',
378 'click .add': 'add_page',
379 'click .remove': 'remove_page',
382 init: function (parent, carousel) {
384 this.parent = parent;
385 var index = instance.website.snippet.carousel.index || 0;
386 instance.website.snippet.carousel.index = index++;
388 $(carousel).addClass("carousel-index-"+index);
389 this.offset = $(carousel).offset();
393 this.$el.css({position: 'absolute', top: this.offset.top+'px', left: this.offset.left+'px'});
395 destroy: function () {
396 return this._super();
398 get_carousel: function() {
399 return $(".carousel.carousel-index-"+this.index);
401 add_page: function() {
402 var $c = this.get_carousel();
403 var cycle = $c.find(".carousel-inner .item").size();
404 $c.find(".carousel-inner").append(this.$(".item").clone());
407 remove_page: function() {
408 var $c = this.get_carousel();
409 var cycle = $c.find(".carousel-inner .item.active").remove();
410 $c.find(".carousel-inner .item:first").addClass("active");
412 this.parent.trigger('change', this.parent, null);
418 function make_static(){
419 $('.oe_snippet_demo').removeClass('oe_new');
420 $('.oe_page *').off('mouseover mouseleave');
421 $('.oe_page .oe_selected').removeClass('oe_selected');
424 var selected_snippet = null;
425 function snippet_click(event){
426 if(selected_snippet){
427 selected_snippet.removeClass('oe_selected');
428 if(selected_snippet[0] === $(this)[0]){
429 selected_snippet = null;
430 event.preventDefault();
435 $(this).addClass('oe_selected');
436 selected_snippet = $(this);
438 event.preventDefault();
440 //$('.oe_snippet').click(snippet_click);
442 var hover_element = null;
444 function make_editable( constraint_after, constraint_inside ){
445 if(selected_snippet && selected_snippet.hasClass('oe_new')){
446 $('.oe_snippet_demo').addClass('oe_new');
448 $('.oe_snippet_demo').removeClass('oe_new');
451 $('.oe_page *').off('mouseover');
452 $('.oe_page *').off('mouseleave');
453 $('.oe_page *').mouseover(function(event){
454 console.log('hover:',this);
456 hover_element.removeClass('oe_selected');
457 hover_element.off('click');
459 $(this).addClass('oe_selected');
460 $(this).click(append_snippet);
461 hover_element = $(this);
462 event.stopPropagation();
464 $('.oe_page *').mouseleave(function(){
465 if(hover_element && $(this) === hover_element){
466 hover_element = null;
467 $(this).removeClass('oe_selected');
472 function append_snippet(event){
473 console.log('click',this,event.button);
474 if(event.button === 0){
475 if(selected_snippet){
476 if(selected_snippet.hasClass('oe_new')){
477 var new_snippet = $("<div class='oe_snippet'></div>");
478 new_snippet.append($(this).clone());
479 new_snippet.click(snippet_click);
480 $('.oe_snippet.oe_selected').before(new_snippet);
482 $(this).after($('.oe_snippet.oe_selected').contents().clone());
484 selected_snippet.removeClass('oe_selected');
485 selected_snippet = null;
488 }else if(event.button === 1){
491 event.preventDefault();
497 instance.web.ActionRedirect = function(parent, action) {
498 var url = $.deparam(window.location.href).url;
500 window.location.href = url;
503 instance.web.client_actions.add("redirect", "instance.web.ActionRedirect");
505 instance.web.GoToWebsite = function(parent, action) {
506 window.location.href = window.location.href.replace(/[?#].*/, '').replace(/\/admin[\/]?$/, '');
508 instance.web.client_actions.add("website.gotowebsite", "instance.web.GoToWebsite");