21d8b426ec9ce8eae03699e971b63850c854ddef
[odoo/odoo.git] / addons / website / static / src / js / website.js
1 openerp.website = function(instance) {
2 var _lt = instance.web._lt;
3 instance.website.EditorBar = instance.web.Widget.extend({
4     template: 'Website.EditorBar',
5     events: {
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',
10     },
11     container: 'body',
12     init: function () {
13         this._super.apply(this, arguments);
14         this.saving_mutex = new $.Mutex();
15     },
16     start: function() {
17         var self = this;
18
19         this.$('button[data-action]').prop('disabled', true)
20             .parent().hide();
21         this.$buttons = {
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]'),
26         };
27         this.$buttons.edit.prop('disabled', false).parent().show();
28
29         self.snippet_start();
30
31         this.rte = new instance.website.RTE(this);
32         this.rte.on('change', this, this.proxy('rte_changed'));
33
34         return $.when(
35             this._super.apply(this, arguments),
36             this.rte.insertBefore(this.$buttons.snippet.parent())
37         );
38     },
39     edit: function () {
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)
43             .parent().show();
44         // TODO: span edition changing edition state (save button)
45         var $editables = $('[data-oe-model]')
46                 .not('link, script')
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);
53
54         // temporary fix until we fix ckeditor
55         $raw_editables.each(function () {
56             $(this).parents().add($(this).find('*')).on('click', function(ev) {
57                 ev.preventDefault();
58                 ev.stopPropagation();
59             });
60         });
61
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')) {
66                 return;
67             }
68
69             $target.addClass('oe_dirty');
70             this.$buttons.save.prop('disabled', false);
71         }.bind(this));
72     },
73     rte_changed: function () {
74         this.$buttons.save.prop('disabled', false);
75     },
76     save: function () {
77         var self = this;
78         var defs = [];
79         $('.oe_dirty').each(function (i, v) {
80             var $el = $(this);
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');
86                 }).fail(function () {
87                     var data = $el.data();
88                     console.error(_.str.sprintf('Could not save %s#%d#%s', data.oeModel, data.oeId, data.oeField));
89                 });
90             });
91             defs.push(def);
92         });
93         return $.when.apply(null, defs).then(function () {
94             window.location.reload();
95         });
96     },
97     saveElement: function ($el) {
98         var data = $el.data();
99         var html = $el.html();
100         var xpath = data.oeXpath;
101         if (xpath) {
102             var $w = $el.clone();
103             $w.removeClass('oe_dirty');
104             _.each(['model', 'id', 'field', 'xpath'], function(d) {$w.removeAttr('data-oe-' + d);});
105             $w
106                 .removeClass('oe_editable')
107                 .prop('contentEditable', false);
108             html = $w.wrap('<div>').parent().html();
109         }
110         return (new instance.web.DataSet(this, 'ir.ui.view')).call('save', [data.oeModel, data.oeId, data.oeField, html, xpath]);
111     },
112     cancel: function () {
113         window.location.reload();
114     },
115     setup_droppable: function () {
116         var self = this;
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);
121
122         $(".oe_snippet_drop").droppable({
123             hoverClass: 'oe_accepting',
124             drop: function( event, ui ) {
125                 console.log(event, ui, "DROP");
126
127                 $(event.target).replaceWith($(ui.draggable).html());
128                 $('.oe_selected').remove();
129                 self.setup_droppable();
130             }
131         }).hide();
132     },
133     snippet_start: function () {
134         this.setup_droppable();
135
136         $('.oe_snippet').draggable().click(function(ev) {
137             $(".oe_snippet_drop").show();
138             $('.oe_selected').removeClass('oe_selected');
139             $(ev.currentTarget).addClass('oe_selected');
140         });
141
142     },
143     snippet: function (ev) {
144         $('.oe_snippet_editor').toggle();
145     },
146 });
147
148 instance.website.Action = instance.web.Widget.extend({
149     tagName: 'button',
150     attributes: {
151         type: 'button',
152     },
153     events: { click: 'perform' },
154     init: function (parent, name) {
155         this._super(parent);
156         this.name = name;
157     },
158     start: function () {
159         this.$el.text(this.name);
160         return this._super();
161     },
162     /**
163      * Executes action
164      */
165     perform: null,
166     toggle: function (to) {
167         this.$el.prop('disabled', !to);
168     },
169 });
170 var Style = instance.website.Style = instance.website.Action.extend({
171     init: function (parent, name, style) {
172         this._super(parent, name);
173         this.style = style;
174     },
175     perform: function () {
176         this.getParent().with_editor(function (editor) {
177             editor.applyStyle(new CKEDITOR.style(this.style))
178         }.bind(this));
179     },
180 });
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;
185     },
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;
191             }
192         }.bind(this));
193     },
194 });
195 var Group = instance.website.ActionGroup = instance.website.Action.extend({
196     template: 'Website.ActionGroup',
197     events: { 'click > button': 'perform' },
198     instances: [],
199     init: function (parent, name, actions) {
200         this._super(parent, name);
201         this.actions = _(actions).map(this.getParent().proxy('init_command'));
202     },
203     start: function () {
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);
209         }));
210     },
211     destroy: function () {
212         this.instances = _(this.instances).without(this);
213         return this._super();
214     },
215     perform: function () {
216         this.getParent().with_editor(function () {
217             _(this.instances).chain()
218                 .without(this)
219                 .pluck('$el')
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);
225         return false;
226     },
227     toggle: function (to) {
228         this.$('> button').prop('disabled', !to);
229     },
230 });
231
232 instance.website.RTE = instance.web.Widget.extend({
233     tagName: 'li',
234     className: 'oe_right oe_rte_toolbar',
235     commands: [
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'],
251         [Group, "\uf0ca", [
252             [Command, "\uf0ca", 'bulletedlist'],
253             [Command, "\uf0cb", 'numberedlist'],
254             [Command, "\uf03b", 'outdent'],
255             [Command, "\uf03c", 'indent']
256         ]],
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', }]
264         ]],
265         [Group, "\uf039", [
266             [Command, "\uf039", 'justifyblock'],
267             [Command, "\uf036", 'justifyleft'],
268             [Command, "\uf038", 'justifyright'],
269             [Command, "\uf037", 'justifycenter']
270         ]]
271     ],
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'}));
274     start: function () {
275         this.$el.hide();
276
277         return $.when.apply(
278             null, _(this.commands).map(this.proxy('start_command')));
279     },
280     init_command: function (command) {
281         var type = command[0], args = command.slice(1);
282         args.unshift(this);
283         var F = function (args) {
284             return type.apply(this, args);
285         };
286         F.prototype = type.prototype;
287
288         return new F(args);
289     },
290     start_command: function (command) {
291         return this.init_command(command).appendTo(this.$el);
292     },
293     start_edition: function ($elements) {
294         var self = this;
295         this.$el.show();
296         this.disable();
297         this.snippet_carousel();
298         CKEDITOR.on('currentInstance', this.proxy('_change_focused_editor'));
299         $elements
300             .not('span, [data-oe-type]')
301             .each(function () {
302                 var $this = $(this);
303                 CKEDITOR.inline(this, self._config()).on('change', function () {
304                     $this.addClass('oe_dirty');
305                     self.trigger('change', this, null);
306                 });
307             });
308     },
309
310     /**
311      * @param {Function} fn
312      * @param {Boolean} [snapshot=true]
313      * @returns {$.Deferred}
314      */
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'); }
320             editor.focus();
321         });
322     },
323
324
325     toggle: function (to) {
326         _(this.getChildren()).chain()
327             .filter(function (child) { return child instanceof instance.website.Action })
328             .invoke('toggle', to);
329     },
330     disable: function () {
331         this.toggle(false);
332     },
333
334     _current_editor: function () {
335         return CKEDITOR.currentInstance;
336     },
337     _change_focused_editor: function () {
338         this.toggle(!!CKEDITOR.currentInstance);
339     },
340     _config: function () {
341         return {
342             // Don't load ckeditor's style rules
343             stylesSet: [],
344             // Remove toolbar entirely, also custom context menu
345             removePlugins: 'toolbar,elementspath,resize,contextmenu,tabletools,liststyle',
346             uiColor: '',
347             // Ensure no config file is loaded
348             customConfig: '',
349             // Disable ACF
350             allowedContent: true,
351             // Don't insert paragraphs around content in e.g. <li>
352             autoParagraph: false,
353             filebrowserImageUploadUrl: "/website/attach",
354         };
355     },
356     // TODO clean
357     snippet_carousel: function () {
358         var self = this;
359         $carousels = $("<div/>");
360         $carousels.css({'position': 'absolute', 'top': 0, 'white-space': 'nowrap'});
361         $carousels.insertAfter(self.$el);
362
363         $(".carousel").each(function() {
364             var $carousel = new instance.website.snippet.carousel(self, this);
365             $carousel.appendTo($carousels);
366         });
367         $(document).on("scroll", function () {
368             $carousels.css("top", (-self.$el.offset().top+2) + 'px');
369         });
370     }
371 });
372
373
374 instance.website.snippet = {};
375 instance.website.snippet.carousel = instance.web.Widget.extend({
376     template: 'Website.Snipped.carousel',
377     events: {
378         'click .add': 'add_page',
379         'click .remove': 'remove_page',
380     },
381     instances: [],
382     init: function (parent, carousel) {
383         this._super(parent);
384         this.parent = parent;
385         var index = instance.website.snippet.carousel.index || 0;
386         instance.website.snippet.carousel.index = index++;
387         this.index = index;
388         $(carousel).addClass("carousel-index-"+index);
389         this.offset = $(carousel).offset();
390     },
391     start: function () {
392         var self = this;
393         this.$el.css({position: 'absolute', top: this.offset.top+'px', left: this.offset.left+'px'});
394     },
395     destroy: function () {
396         return this._super();
397     },
398     get_carousel: function() {
399         return $(".carousel.carousel-index-"+this.index);
400     },
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());
405         $c.carousel(cycle);
406     },
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");
411         $c.carousel(0);
412         this.parent.trigger('change', this.parent, null);
413     }
414 });
415
416 $(function(){
417
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');
422     }
423
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();
431                 make_static();
432                 return;
433             }
434         }
435         $(this).addClass('oe_selected');
436         selected_snippet = $(this);
437         make_editable();
438         event.preventDefault();
439     }
440     //$('.oe_snippet').click(snippet_click);
441
442     var hover_element = null;
443
444     function make_editable( constraint_after, constraint_inside ){
445         if(selected_snippet && selected_snippet.hasClass('oe_new')){
446             $('.oe_snippet_demo').addClass('oe_new');
447         }else{
448             $('.oe_snippet_demo').removeClass('oe_new');
449         }
450     
451         $('.oe_page *').off('mouseover');
452         $('.oe_page *').off('mouseleave');
453         $('.oe_page *').mouseover(function(event){
454             console.log('hover:',this);
455             if(hover_element){
456                 hover_element.removeClass('oe_selected');
457                 hover_element.off('click');
458             }
459             $(this).addClass('oe_selected');
460             $(this).click(append_snippet);
461             hover_element = $(this);
462             event.stopPropagation();
463         });
464         $('.oe_page *').mouseleave(function(){
465             if(hover_element && $(this) === hover_element){
466                 hover_element = null;
467                 $(this).removeClass('oe_selected');
468             }
469         });
470     }
471
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);
481                 }else{
482                     $(this).after($('.oe_snippet.oe_selected').contents().clone());
483                 }
484                 selected_snippet.removeClass('oe_selected');
485                 selected_snippet = null;
486                 make_static();
487             }
488         }else if(event.button === 1){
489             $(this).remove();
490         }
491         event.preventDefault();
492     }
493
494 });
495
496
497 instance.web.ActionRedirect = function(parent, action) {
498     var url = $.deparam(window.location.href).url;
499     if (url) {
500         window.location.href = url;
501     }
502 };
503 instance.web.client_actions.add("redirect", "instance.web.ActionRedirect");
504
505 instance.web.GoToWebsite = function(parent, action) {
506     window.location.href = window.location.href.replace(/[?#].*/, '').replace(/\/admin[\/]?$/, '');
507 };
508 instance.web.client_actions.add("website.gotowebsite", "instance.web.GoToWebsite");
509
510 };