[FIX] website snippet: fix banner and add a clean_for_save method on snippet editor
[odoo/odoo.git] / addons / website / static / src / js / website.snippets.js
1 (function () {
2     'use strict';
3
4     var website = openerp.website;
5     website.templates.push('/website/static/src/xml/website.snippets.xml');
6
7     website.EditorBar.include({
8         start: function () {
9             var self = this;
10             $("[data-oe-model]").on('click', function (event) {
11                 var $this = $(event.srcElement);
12                 var tag = $this[0].tagName.toLowerCase();
13                 if (!(tag === 'a' || tag === "button") && !$this.parents("a, button").length) {
14                     self.$('[data-action="edit"]').parent().effect('bounce', {distance: 18, times: 5}, 250);
15                 }
16             });
17             return this._super();
18         },
19         edit: function () {
20             $("body").off('click');
21             window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
22             this.snippets.appendTo(this.$el);
23             return this._super.apply(this, arguments);
24         },
25         save: function () {
26             this.snippets.make_active(false);
27             this.snippets.clean_for_save();
28             remove_added_snippet_id();
29             this._super();
30         },
31     });
32
33
34     $(document).ready(function () {
35         hack_to_add_snippet_id();
36         $("[data-snippet-id]").each(function() {
37                 var $snipped_id = $(this);
38                 if (typeof $snipped_id.data("snippet-view") === 'undefined' &&
39                         website.snippet.animationRegistry[$snipped_id.data("snippet-id")]) {
40                     $snipped_id.data("snippet-view", new website.snippet.animationRegistry[$snipped_id.data("snippet-id")]($snipped_id));
41                 }
42             });
43     });
44
45     /* ----- SNIPPET SELECTOR ---- */
46     
47     website.snippet = {};
48
49
50     // puts $el at the same absolute position as $target
51     function hack_to_add_snippet_id () {
52         _.each(website.snippet.selector, function (val) {
53             $(val[0]).each(function() {
54                 if (!$(this).is("[data-snippet-id]") && $(this).parents("[data-oe-model]").length) {
55                     $(this).attr("data-snippet-id", val[1]);
56                 }
57             });
58         });
59     }
60     function remove_added_snippet_id () {
61         _.each(website.snippet.selector, function (val) {
62             $(val[0]).each(function() {
63                 if ($(this).data("snippet-id") === val[1]) {
64                     $(this).removeAttr("data-snippet-id");
65                 }
66             });
67         });
68     }
69
70     website.snippet.selector = [];
71     website.snippet.BuildingBlock = openerp.Widget.extend({
72         template: 'website.snippets',
73         activeSnippets: [],
74         init: function (parent) {
75             this.parent = parent;
76             this._super.apply(this, arguments);
77             if(!$('#oe_manipulators').length){
78                 $("<div id='oe_manipulators'></div>").appendTo('body');
79             }
80             this.$active_snipped_id = false;
81             hack_to_add_snippet_id();
82             this.snippets = [];
83             $("body").on('DOMNodeInserted', hack_to_add_snippet_id);
84         },
85         dom_filter: function (dom, sibling) {
86             if (typeof dom === "string") {
87                 var include = "[data-oe-model]";
88                 var sdom = dom.split(',');
89                 dom = "";
90                 _.each(sdom, function (val) {
91                     val = val.replace(/^\s+|\s+$/g, '');
92                     dom += include + " " + val + ", ";
93                     if (!sibling) {
94                         val = val.split(" ");
95                         dom += val.shift() + include + val.join(" ") + ", ";
96                     }
97                 });
98                 dom = dom.replace(/,\s*$/g, '');
99                 return $(dom);
100             } else {
101                 return (!sibling && $(dom).is("[data-oe-model]")) || $(dom).parents("[data-oe-model]").length ? $(dom) : $("");
102             }
103         },
104         start: function() {
105             var self = this;
106
107             var $ul = this.parent.$("#website-top-edit ul");
108
109             var $button = $(openerp.qweb.render('website.snippets_button')).prependTo($ul);
110             $button.find('button').click(function () {
111                 self.make_active(false);
112                 self.$el.toggleClass("hidden");
113             });
114
115             this.fetch_snippet_templates();
116
117             this.bind_snippet_click_editor();
118
119             this.$el.addClass("hidden");
120
121             this.$modal = $(openerp.qweb.render('website.snippets_modal'));
122             this.$modal.appendTo("body");
123         },
124         fetch_snippet_templates: function () {
125             var self = this;
126             this.style_templates = {};
127
128             openerp.jsonRpc("/website/snippets", 'call', {})
129                 .then(function (html) {
130                     var $html = $(html);
131
132                     var $styles = $html.find("#snippet_styles");
133                     $styles.find("> [data-snippet-id]").each(function () {
134                         var $style = $(this);
135                         var snipped_id = $style.data('snippet-id');
136                         self.style_templates[snipped_id] = {
137                             'snipped-id' : snipped_id,
138                             'selector': $style.data('selector'),
139                             'class': $style.find(".oe_snippet_class").text(),
140                             'label': $style.find(".oe_snippet_label").text()
141                         };
142                     });
143                     $styles.remove();
144
145                     self.$snippets = $html.find(".tab-content > div > div").addClass("oe_snippet");
146                     self.$el.append($html);
147
148                     self.make_snippet_draggable(self.$snippets);
149                 });
150         },
151         cover_target: function ($el, $target){
152             var pos = $target.offset();
153             var mt = parseInt($target.css("margin-top") || 0);
154             var mb = parseInt($target.css("margin-bottom") || 0);
155             $el.css({
156                 'position': 'absolute',
157                 'width': $target.outerWidth(),
158                 'height': $target.outerHeight() + mt + mb,
159                 'top': pos.top - mt,
160                 'left': pos.left
161             });
162         },
163         show: function () {
164             this.$el.removeClass("hidden");
165         },
166         hide: function () {
167             this.$el.addClass("hidden");
168         },
169
170         bind_snippet_click_editor: function () {
171             var self = this;
172             var snipped_event_flag = false;
173             $("body").on('click', "[data-oe-model] [data-snippet-id], [data-oe-model][data-snippet-id]", function (event) {
174                     if (snipped_event_flag) {
175                         return;
176                     }
177                     snipped_event_flag = true;
178                     setTimeout(function () {snipped_event_flag = false;}, 0);
179                     var $target = $(event.currentTarget);
180                     if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
181                         return;
182                     }
183                     self.make_active($target);
184                 });
185             $("[data-oe-model]").on('click', function (ev) {
186                     if (!snipped_event_flag && self.$active_snipped_id && !self.$active_snipped_id.parents("[data-snippet-id]:first")) {
187                         self.make_active(false);
188                     }
189                 });
190         },
191         snippet_blur: function ($snipped_id) {
192             if ($snipped_id) {
193                 if ($snipped_id.data("snippet-editor")) {
194                     $snipped_id.data("snippet-editor").onBlur();
195                 }
196                 if ($snipped_id.data("snippet-view")) {
197                     $snipped_id.data("snippet-view").onBlurEdit();
198                 }
199             }
200         },
201         snippet_focus: function ($snipped_id) {
202             if ($snipped_id) {
203                 if ($snipped_id.data("snippet-view")) {
204                     $snipped_id.data("snippet-view").onFocusEdit();
205                 }
206                 if ($snipped_id.data("snippet-editor")) {
207                     $snipped_id.data("snippet-editor").onFocus();
208                 }
209             }
210         },
211         clean_for_save: function () {
212             for (var k in this.snippets) {
213                 $(this.snippets[k]).data("snippet-editor").clean_for_save();
214             }
215         },
216         make_active: function ($snipped_id) {
217             if ($snipped_id && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snipped_id.get(0)) {
218                 return;
219             }
220             if (this.$active_snipped_id) {
221                 this.snippet_blur(this.$active_snipped_id);
222             }
223             if ($snipped_id) {
224                 if(_.indexOf(this.snippets, $snipped_id.get(0)) === -1) {
225                     this.snippets.push($snipped_id.get(0));
226                 }
227                 this.$active_snipped_id = $snipped_id;
228                 this.create_overlay(this.$active_snipped_id);
229                 this.snippet_focus($snipped_id);
230             } else {
231                 self.$active_snipped_id = false;
232             }
233         },
234         create_overlay: function ($snipped_id) {
235             if (typeof $snipped_id.data("snippet-editor") === 'undefined') {
236                 var $targets = this.activate_overlay_zones($snipped_id);
237                 if (!$targets.length) return;
238                 var editor = website.snippet.editorRegistry[$snipped_id.data("snippet-id")] || website.snippet.editorRegistry.resize;
239                 $snipped_id.data("snippet-editor", new editor(this, $snipped_id));
240             }
241             this.cover_target($snipped_id.data('overlay'), $snipped_id);
242         },
243
244         path_eval: function(path){
245             var obj = window;
246             path = path.split('.');
247             do{
248                 obj = obj[path.shift()];
249             }while(path.length && obj);
250             return obj;
251         },
252
253         // activate drag and drop for the snippets in the snippet toolbar
254         make_snippet_draggable: function($snippets){
255             var self = this;
256             var $tumb = $snippets.find(".oe_snippet_thumbnail:first");
257             var left = $tumb.outerWidth()/2;
258             var top = $tumb.outerHeight()/2;
259             var $toInsert, dropped, $snippet, action, snipped_id;
260
261             $snippets.draggable({
262                 greedy: true,
263                 helper: 'clone',
264                 zIndex: '1000',
265                 appendTo: 'body',
266                 cursor: "move",
267                 handle: ".oe_snippet_thumbnail",
268                 cursorAt: {
269                     'left': left,
270                     'top': top
271                 },
272                 start: function(){
273                     self.hide();
274                     dropped = false;
275                     $snippet = $(this);
276                     snipped_id = $snippet.data('snippet-id');
277                     action = $snippet.find('.oe_snippet_body').size() ? 'insert' : 'mutate';
278                     if( action === 'insert'){
279                         if (!$snippet.data('selector-siblings') && !$snippet.data('selector-children') && !$snippet.data('selector-vertical-children')) {
280                             console.debug($snippet.data("snippet-id") + " have oe_snippet_body class and have not for insert action"+
281                                 "data-selector-siblings, data-selector-children or data-selector-vertical-children tag for mutate action");
282                             return;
283                         }
284                         self.activate_insertion_zones({
285                             siblings: $snippet.data('selector-siblings'),
286                             children:   $snippet.data('selector-children'),
287                             vertical_children:   $snippet.data('selector-vertical-children')
288                         });
289
290                         $toInsert = $snippet.find('.oe_snippet_body').clone();
291                         $toInsert.removeClass('oe_snippet_body');
292                         $toInsert.attr('data-snippet-id', snipped_id);
293
294                     } else if( action === 'mutate' ){
295                         if (!$snippet.data('selector')) {
296                             console.debug($snippet.data("snippet-id") + " have not oe_snippet_body class and have not data-selector tag");
297                             return;
298                         }
299                         var $targets = self.activate_overlay_zones($snippet.data('selector'));
300                         $targets.each(function(){
301                             var $clone = $(this).data('overlay').clone();
302                              $clone.addClass("oe_drop_zone").data('target', $(this));
303                             $(this).data('overlay').after($clone);
304                         });
305
306                     }
307
308                     $('.oe_drop_zone').droppable({
309                         over:   function(){
310                             if( action === 'insert'){
311                                 dropped = true;
312                                 $(this).first().after($toInsert);
313                             }
314                         },
315                         out:    function(){
316                             if( action === 'insert'){
317                                 dropped = false;
318                                 $toInsert.detach();
319                             }
320                         },
321                         drop:   function(){
322                             dropped = true;
323                         },
324                     });
325                 },
326                 stop: function(ev, ui){
327                     if (action === 'insert' && ! dropped) {
328                         var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first()
329                         if (el) {
330                             el.after($toInsert)
331                             dropped = true;
332                         }
333                     }
334
335                     $('.oe_drop_zone').droppable('destroy').remove();
336                     if (dropped) {
337                         var $target = false;
338                         if(action === 'insert'){
339                             $target = $toInsert;
340
341                             if (website.snippet.animationRegistry[snipped_id]) {
342                                 new website.snippet.animationRegistry[snipped_id]($target);
343                             }
344
345                             self.create_overlay($target);
346                             $target.data("snippet-editor").build_snippet($target);
347
348                         } else {
349                             $target = $(this).data('target');
350
351                             self.create_overlay($target);
352                             if (website.snippet.editorRegistry[snipped_id]) {
353                                 var snippet = new website.snippet.editorRegistry[snipped_id](self, $target);
354                                 snippet.build_snippet($target);
355                             }
356                         }
357                         setTimeout(function () {self.make_active($target);},0);
358                     } else {
359                         $toInsert.remove();
360                         if (self.$modal.find('input:not(:checked)').length) {
361                             self.$modal.modal('toggle');
362                         }
363                     }
364                 },
365             });
366         },
367
368         // return the original snippet in the editor bar from a snippet id (string)
369         get_snippet_from_id: function(id){
370             return $('.oe_snippet').filter(function(){
371                     return $(this).data('snippet-id') === id;
372                 }).first();
373         },
374
375         // Create element insertion drop zones. two css selectors can be provided
376         // selector.children -> will insert drop zones as direct child of the selected elements
377         //   in case the selected elements have children themselves, dropzones will be interleaved
378         //   with them.
379         // selector.siblings -> will insert drop zones after and before selected elements
380         activate_insertion_zones: function(selector){
381             var self = this;
382             var child_selector = selector.children;
383             var sibling_selector = selector.siblings;
384             var vertical_child_selector   =  selector.vertical_children;
385
386             var zone_template = "<div class='oe_drop_zone oe_insert'></div>";
387
388             if(child_selector){
389                 self.dom_filter(child_selector).each(function (){
390                     var $zone = $(this);
391                     $zone.find('> *:not(.oe_drop_zone):visible').after(zone_template);
392                     $zone.prepend(zone_template);
393                 });
394             }
395
396             if(vertical_child_selector){
397                 self.dom_filter(vertical_child_selector).each(function (){
398                     var $zone = $(this);
399                     var $template = $(zone_template).addClass("oe_vertical");
400                     var nb = 0;
401                     var $lastinsert = false;
402                     var left = 0;
403                     var temp_left = 0;
404                     $zone.find('> *:not(.oe_drop_zone):visible').each(function () {
405                         var $col = $(this);
406                         $template.css('height', ($col.outerHeight() + parseInt($col.css("margin-top")) + parseInt($col.css("margin-bottom")))+'px');
407                         $lastinsert = $template.clone();
408                         $(this).after($lastinsert);
409
410                         temp_left = $col.position().left;
411                         if (left === temp_left) {
412                             $col.prev(".oe_drop_zone.oe_vertical").remove();
413                             $col.before($template.clone().css("clear", "left"));
414                         }
415                         else if (!nb) {
416                             $col.before($template.clone());
417                         }
418                         left = temp_left;
419                         nb ++;
420                     });
421                     if (!nb) {
422                         $zone.prepend($template.css('height', $zone.outerHeight()+'px'));
423                     }
424                 });
425             }
426
427             if(sibling_selector){
428                 self.dom_filter(sibling_selector, true).each(function (){
429                     var $zone = $(this);
430                     if($zone.prev('.oe_drop_zone:visible').length === 0){
431                         $zone.before(zone_template);
432                     }
433                     if($zone.next('.oe_drop_zone:visible').length === 0){
434                         $zone.after(zone_template);
435                     }
436                 });
437             }
438
439             var count;
440             do {
441                 count = 0;
442                 // var $zones = $('.oe_drop_zone + .oe_drop_zone');    // no two consecutive zones
443                 // count += $zones.length;
444                 // $zones.remove();
445
446                 $zones = $('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove();   // no recusrive zones
447                 count += $zones.length;
448                 $zones.remove();
449             } while (count > 0);
450
451             // Cleaning up zones placed between floating or inline elements. We do not like these kind of zones.
452             var $zones = $('.oe_drop_zone:not(.oe_vertical)');
453             $zones.each(function (){
454                 var zone = $(this);
455                 var prev = zone.prev();
456                 var next = zone.next();
457                 var float_prev = zone.prev().css('float')   || 'none';
458                 var float_next = zone.next().css('float')   || 'none';
459                 var disp_prev  = zone.prev().css('display') ||  null;
460                 var disp_next  = zone.next().css('display') ||  null;
461                 if(     (float_prev === 'left' || float_prev === 'right')
462                     &&  (float_next === 'left' || float_next === 'right')  ){
463                     zone.remove();
464                     return;
465                 }else if( !( disp_prev === null
466                           || disp_next === null
467                           || disp_prev === 'block'
468                           || disp_next === 'block' )){
469                     zone.remove();
470                 }
471             });
472         },
473
474         // generate drop zones covering the elements selected by the selector
475         // we generate overlay drop zones only to get an idea of where the snippet are, the drop
476         activate_overlay_zones: function(selector){
477             var $targets = this.dom_filter(selector || '[data-snippet-id]');
478             var self = this;
479
480             if (typeof selector !== 'string' && !$targets.length) {
481                 console.debug( "A good node must have a [data-oe-model] attribute or must have at least one parent with [data-oe-model] attribute.");
482                 console.debug( "Wrong node(s): ", selector);
483             }
484             
485             function is_visible($el){
486                 return     $el.css('display')    != 'none'
487                         && $el.css('opacity')    != '0'
488                         && $el.css('visibility') != 'hidden';
489             }
490
491             // filter out invisible elements
492             $targets = $targets.filter(function(){ return is_visible($(this)); });
493
494             // filter out elements with invisible parents
495             $targets = $targets.filter(function(){
496                 var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
497                 return parents.length === 0;
498             });
499             
500             $targets.each(function () {
501                 var $target = $(this);
502                 if (!$target.data('overlay')) {
503                     var $zone = $(openerp.qweb.render('website.snippet_overlay'));
504                     $zone.appendTo('#oe_manipulators');
505                     $zone.data('target',$target);
506                     $target.data('overlay',$zone);
507
508                     $target.on("DOMNodeInserted DOMNodeRemoved DOMSubtreeModified", function () {
509                         self.cover_target($zone, $target);
510                     });
511                     $('body').on("resize", function () {
512                         self.cover_target($zone, $target);
513                     });
514                 }
515                 self.cover_target($target.data('overlay'), $target);
516             });
517             return $targets;
518         }
519     });
520
521
522     website.snippet.animationRegistry = {};
523     website.snippet.Animation = openerp.Class.extend({
524         $: function () {
525             return this.$el.find.apply(this.$el, arguments);
526         },
527         init: function (dom) {
528             this.$el = this.$target = $(dom);
529             this.start();
530         },
531         /*
532         *  start
533         *  This method is called after init
534         */
535         start: function () {
536         },
537         /* onFocusEdit
538         *  if they are an editor for this data-snippet-id 
539         *  Called before onFocus of snippet editor
540         */
541         onFocusEdit : function () {},
542
543         /* onBlurEdit
544         *  if they are an editor for this data-snippet-id 
545         *  Called after onBlur of snippet editor
546         */
547         onBlurEdit : function () {},
548     });
549
550     website.snippet.editorRegistry = {};
551     website.snippet.Editor = openerp.Class.extend({
552         init: function (parent, dom) {
553             this.parent = parent;
554             this.$target = $(dom);
555             this.$overlay = this.$target.data('overlay');
556             this.snippet_id = this.$target.data("snippet-id");
557             this._readXMLData();
558             this.load_style_options();
559             this.get_parent_block();
560             this.start();
561         },
562
563         /*
564         *  _readXMLData
565         *  Read data XML and set value into:
566         *  this.$el :
567         *       all xml data
568         *  this.$overlay :
569         *       Dom hover the $target who content options
570         *  this.$editor :
571         *       content of .oe_snippet_options
572         *       Displayed into the overlay options on focus
573         */
574         _readXMLData: function() {
575             this.$el = this.parent.$snippets.siblings("[data-snippet-id='"+this.snippet_id+"']").clone();
576             this.$editor = this.$el.find(".oe_snippet_options");
577             var $options = this.$overlay.find(".oe_overlay_options");
578             this.$editor.prependTo($options.find(".oe_options ul"));
579             if ($options.find(".oe_options ul li").length) {
580                 $options.find(".oe_options").removeClass("hidden");
581             }
582         },
583
584
585         // activate drag and drop for the snippets in the snippet toolbar
586         _drag_and_drop: function(){
587             var self = this;
588             this.dropped = false;
589             this.$overlay.draggable({
590                 greedy: true,
591                 appendTo: 'body',
592                 cursor: "move",
593                 handle: ".oe_snippet_move",
594                 cursorAt: {
595                     left: 18,
596                     top: 14
597                 },
598                 helper: function() {
599                     var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
600                     $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
601                     $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
602                     $clone.appendTo("body").removeClass("hidden");
603                     return $clone;
604                 },
605                 start: _.bind(self._drag_and_drop_start, self),
606                 stop: _.bind(self._drag_and_drop_stop, self)
607             });
608         },
609         _drag_and_drop_after_insert_dropzone: function (){},
610         _drag_and_drop_active_drop_zone: function ($zones){
611             var self = this;
612             $zones.droppable({
613                 over:   function(){
614                     $(".oe_drop_zone.hide").removeClass("hide");
615                     $(this).addClass("hide").first().after(self.$target);
616                     self.dropped = true;
617                 },
618                 out:    function(){
619                     $(this).removeClass("hide");
620                     self.$target.detach();
621                     self.dropped = false;
622                 },
623             });
624         },
625         _drag_and_drop_start: function (){
626             var self = this;
627             self.parent.hide();
628             self.parent.editor_busy = true;
629             self.size = {
630                 width: self.$target.width(),
631                 height: self.$target.height()
632             };
633             self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
634             self.$target.detach();
635             self.$overlay.addClass("hidden");
636
637             self.parent.activate_insertion_zones({
638                 siblings: self.$el ? self.$el.data('selector-siblings') : false,
639                 children:   self.$el ? self.$el.data('selector-children') : false,
640                 vertical_children: self.$el ? self.$el.data('selector-vertical-children') : false,
641             });
642
643             $("body").addClass('move-important');
644             
645             self._drag_and_drop_after_insert_dropzone();
646             self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
647         },
648         _drag_and_drop_stop: function (){
649             var self = this;
650             if (!self.dropped) {
651                 $(".oe_drop_clone").after(self.$target);
652             }
653             self.$overlay.removeClass("hidden");
654             $("body").removeClass('move-important');
655             $('.oe_drop_zone').droppable('destroy').remove();
656             $(".oe_drop_clone, .oe_drop_to_remove").remove();
657             self.parent.editor_busy = false;
658             self.get_parent_block();
659             setTimeout(function () {self.parent.create_overlay(self.$target);},0);
660         },
661
662         _clone: function () {
663             var self = this;
664             this.$overlay.on('click', '.oe_snippet_clone', function () {
665                 var $clone = self.$target.clone(false);
666                 self.$target.after($clone);
667                 return false;
668             });
669         },
670
671         load_style_options: function () {
672             var self = this;
673             var $styles = this.$overlay.find('.oe_options');
674             var $ul = $styles.find('ul:first');
675             _.each(this.parent.style_templates, function (val, key) {
676                 if (!self.parent.dom_filter(val.selector).is(self.$target)) {
677                     return;
678                 }
679                 var $li = $("<li class='oe_style'/>").data(val);
680                 $li.append($('<a/>').text(val.label));
681                 $ul.append($li);
682                 if (self.$target.hasClass( "oe_snippet_" + $li.data("snipped-id") )) {
683                     $li.addClass("active");
684                 }
685                 $styles.removeClass("hidden");
686             });
687             $styles.on('click', 'li.oe_style a', _.bind(this.change_style, this));
688         },
689         change_style: function (event) {
690             var $li = $(event.currentTarget).parent();
691             var snipped_id = $li.data("snipped-id");
692             var active = $li.hasClass("active");
693
694             if (website.snippet.editorRegistry[snipped_id]) {
695                 var snippet = new website.snippet.editorRegistry[snipped_id](this, this.$target);
696                 snippet.build_snippet(this.$target);
697             }
698             var _class = "oe_snippet_" + snipped_id + " " + ($li.data("class") || "");
699             if (active) {
700                 this.$target.removeClass(_class);
701             } else {
702                 this.$target.addClass(_class);
703             }
704             $li.toggleClass("active");
705         },
706
707         get_parent_block: function () {
708             var self = this;
709             var $button = this.$overlay.find('.oe_snippet_parent');
710             var $parent = this.$target.parents("[data-snippet-id]:first");
711             if ($parent.length) {
712                 $button.removeClass("hidden");
713                 $button.off("click").on('click', function (event) {
714                     event.preventDefault();
715                     setTimeout(function () {
716                         self.parent.make_active($parent);
717                     }, 0);
718                 });
719             } else {
720                 $button.addClass("hidden");
721             }
722         },
723
724         /*
725         *  start
726         *  This method is called after init and _readXMLData
727         */
728         start: function () {
729             var self = this;
730             this.$overlay.on('click', '.oe_snippet_remove', function () {
731                 self.$target.detach();
732                 self.onBlur();
733                 self.$target.remove();
734                 return false;
735             });
736             this._drag_and_drop();
737             this._clone();
738         },
739
740         /*
741         *  build_snippet
742         *  This method is called just after that a thumbnail is drag and droped into a drop zone
743         *  (after the insertion of this.$body, if this.$body exists)
744         */
745         build_snippet: function ($target) {
746         },
747
748         /* onFocus
749         *  This method is called when the user click inside the snippet in the dom
750         */
751         onFocus : function () {
752             this.$overlay.addClass('oe_active');
753         },
754
755         /* onFocus
756         *  This method is called when the user click outide the snippet in the dom, after a focus
757         */
758         onBlur : function () {
759             this.$overlay.removeClass('oe_active');
760         },
761
762         /* clean_for_save
763         *  function called just before save vue
764         */
765         clean_for_save: function () {
766
767         },
768
769         change_background: function (bg, ul_options) {
770             var self = this;
771             this.set_options_background(bg, ul_options);
772             var $ul = this.$editor.find(ul_options);
773
774             // bind envent on options
775             var $li = $ul.find("li");
776             $li.on('click', function (event) {
777                     if ($(this).data("value")) {
778                         $li.removeClass("active");
779                         $(this).addClass("active");
780                         self.$editor.find('input').val("");
781                     } else {
782                         var editor = new website.editor.ImageDialog();
783                         editor.on('start', self, function (o) {o.url = bg_value;});
784                         editor.on('save', self, function (o) {
785                             var $bg = typeof bg === 'string' ? self.$target.find(bg) : $(bg);
786                             $bg.css("background-image", "url(" + o.url + ")");
787                         });
788                         editor.appendTo($('body'));
789                     }
790                 })
791                 .on('mouseover', function (event) {
792                     if ($(this).data("value")) {
793                         var src = $(this).data("value");
794                         var $bg = typeof bg === 'string' ? self.$target.find(bg) : $(bg);
795                         $bg.css("background-image", "url(" + src + ")");
796                     }
797                 })
798                 .on('mouseout', function (event) {
799                     var src = $ul.find('li.active').data("value");
800                     var $bg = typeof bg === 'string' ? self.$target.find(bg) : $(bg);
801                     $bg.css("background-image", "url(" + src + ")");
802                 });
803         },
804
805         set_options_background: function (bg, ul_options) {
806             var $ul = this.$editor.find(ul_options);
807             var bg_value = (typeof bg === 'string' ? this.$target.find(bg) : $(bg)).css("background-image").replace(/url\(['"]*|['"]*\)/g, "");
808
809             // select in ul options
810             $ul.find("li").removeClass("active");
811             var selected = $ul.find('[data-value="' + bg_value + '"], [data-value="' + bg_value.replace(/.*:\/\/[^\/]+/, '') + '"]');
812             selected.addClass('active');
813             if (!selected.length) {
814                 $ul.find('.oe_custom_bg b').html(bg_value);
815             }
816         },
817     });
818
819
820     website.snippet.editorRegistry.resize = website.snippet.Editor.extend({
821         start: function () {
822             var self = this;
823             this._super();
824             var $box = $(openerp.qweb.render("website.snippets.resize"));
825
826             var resize_values = this.getSize();
827             if (!resize_values.n) $box.find(".oe_handle.n").remove();
828             if (!resize_values.s) $box.find(".oe_handle.s").remove();
829             if (!resize_values.e) $box.find(".oe_handle.e").remove();
830             if (!resize_values.w) $box.find(".oe_handle.w").remove();
831             
832             this.$overlay.append($box.find(".oe_handles").html());
833
834             this.$overlay.find(".oe_handle").on('mousedown', function (event){
835                     event.preventDefault();
836
837                     var $handle = $(this);
838
839                     var resize_values = self.getSize();
840                     var compass = false;
841                     var XY = false;
842                     if ($handle.hasClass('n')) {
843                         compass = 'n';
844                         XY = 'Y';
845                     }
846                     else if ($handle.hasClass('s')) {
847                         compass = 's';
848                         XY = 'Y';
849                     }
850                     else if ($handle.hasClass('e')) {
851                         compass = 'e';
852                         XY = 'X';
853                     }
854                     else if ($handle.hasClass('w')) {
855                         compass = 'w';
856                         XY = 'X';
857                     }
858
859                     var resize = resize_values[compass];
860                     if (!resize) return;
861
862                     var current = resize[2] || 0;
863                     _.each(resize[0], function (val, key) {
864                         if (self.$target.hasClass(val)) {
865                             current = key;
866                         }
867                     });
868
869                     self.parent.editor_busy = true;
870
871                     var xy = event['page'+XY];
872                     var begin = current;
873                     var beginClass = self.$target.attr("class");
874                     var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
875
876                     var cursor = $handle.css("cursor")+'-important';
877                     $("body").addClass(cursor);
878
879                     var body_mousemove = function (event){
880                         event.preventDefault();
881                         var dd = event['page'+XY] - xy + resize[1][begin];
882                         var next = current+1 === resize[1].length ? current : (current+1);
883                         var prev = current ? (current-1) : 0;
884
885                         var change = false;
886                         if (dd > (2*resize[1][next] + resize[1][current])/3) {
887                             self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
888                             self.$target.addClass(resize[0][next]);
889                             current = next;
890                             change = true;
891                         }
892                         if (prev != current && dd < (2*resize[1][prev] + resize[1][current])/3) {
893                             self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
894                             self.$target.addClass(resize[0][prev]);
895                             current = prev;
896                             change = true;
897                         }
898
899                         if (change) {
900                             self.on_resize(compass, beginClass, current);
901                             self.parent.cover_target(self.$overlay, self.$target);
902                         }
903                     };
904                     $('body').mousemove(body_mousemove);
905
906                     var body_mouseup = function(){
907                         $('body').unbind('mousemove', body_mousemove);
908                         $('body').unbind('mouseup', body_mouseup);
909                         $("body").removeClass(cursor);
910                         self.parent.editor_busy = false;
911                     };
912                     $('body').mouseup(body_mouseup);
913                 });
914         },
915         getSize: function () {
916             var grid = [0,4,8,16,32,48,64,92,128];
917             this.grid = {
918                 n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
919                 s: [_.map(grid, function (v) {return 'mb'+v;}), grid]
920             };
921             return this.grid;
922         },
923
924         /* on_resize
925         *  called when the box is resizing and the class change, before the cover_target
926         *  @compass: resize direction : 'n', 's', 'e', 'w'
927         *  @beginClass: attributes class at the begin
928         *  @current: curent increment in this.grid
929         */
930         on_resize: function (compass, beginClass, current) {
931
932         }
933     });
934
935     website.snippet.editorRegistry.colmd = website.snippet.editorRegistry.resize.extend({
936         getSize: function () {
937             this.grid = this._super();
938             var width = this.$target.parents(".row:first").first().outerWidth();
939
940             var grid = [1,2,3,4,5,6,7,8,9,10,11,12];
941             this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})];
942
943             var grid = [-12,-11,-10,-9,-8,-7,-6,-5,-4,-3,-2,-1,0,1,2,3,4,5,6,7,8,9,10,11];
944             this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12];
945
946             return this.grid;
947         },
948
949         _drag_and_drop_after_insert_dropzone: function(){
950             var self = this;
951             var $zones = $(".row:has(> .oe_drop_zone)").each(function () {
952                 var $row = $(this);
953                 var width = $row.innerWidth();
954                 var pos = 0;
955                 while (width > pos + self.size.width) {
956                     var $last = $row.find("> .oe_drop_zone:last");
957                     $last.each(function () {
958                         pos = $(this).position().left;
959                     });
960                     if (width > pos + self.size.width) {
961                         $row.append("<div class='col-md-1 oe_drop_to_remove'/>");
962                         var $add_drop = $last.clone();
963                         $row.append($add_drop);
964                         self._drag_and_drop_active_drop_zone($add_drop);
965                     }
966                 }
967             });
968         },
969         _drag_and_drop_start: function () {
970             this._super();
971             this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''));
972         },
973         _drag_and_drop_stop: function () {
974             this.$target.addClass("col-md-offset-" + this.$target.prevAll(".oe_drop_to_remove").length);
975             this._super();
976         },
977
978         on_resize: function (compass, beginClass, current) {
979             if (compass !== 'w')
980                 return;
981
982             // don't change the rigth border position when we change the offset (replace col size)
983             var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0);
984             var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0);
985             var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0);
986             if (offset < 0) {
987                 offset = 0;
988             }
989             var colSize = beginCol - (offset - beginOffset);
990             if (colSize <= 0) {
991                 colSize = 1;
992                 offset = beginOffset + beginCol - 1;
993             }
994             this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, ''));
995
996             this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize));
997             if (offset > 0) {
998                 this.$target.addClass('col-md-offset-' + offset);
999             }
1000         },
1001     });
1002
1003     website.snippet.animationRegistry.carousel = website.snippet.Animation.extend({
1004         start: function () {
1005             this.$target.carousel();
1006         },
1007     });
1008     website.snippet.editorRegistry.carousel = website.snippet.editorRegistry.resize.extend({
1009         build_snippet: function() {
1010             var id = 0;
1011             $("body .carousel").each(function () {
1012                 var _id = +$(this).attr("id").replace(/^myCarousel/, '');
1013                 if (id <= _id) {
1014                     id = _id + 1;
1015                 }
1016             });
1017             this.$target.attr("id", "myCarousel" + id);
1018             this.$target.find(".carousel-control").attr("href", "#myCarousel" + id);
1019             this.$target.find("[data-target='#myCarousel']").attr("data-target", "#myCarousel" + id);
1020             this.rebind_event();
1021         },
1022         onFocus: function () {
1023             this._super();
1024             this.$target.carousel('pause');
1025         },
1026         onBlur: function () {
1027             this._super();
1028             this.$target.carousel('cycle');
1029         },
1030         clean_for_save: function () {
1031             this._super();
1032             this.$target.find(".item.left, .item.next").removeClass("next left");
1033             if(!this.$target.find(".item.active").length) {
1034                 this.$target.find(".item:first").addClass("active");
1035             }
1036         },
1037         start : function () {
1038             this._super();
1039
1040             this.id = this.$target.attr("id");
1041
1042             this.$inner = this.$target.find('.carousel-inner');
1043             this.$indicators = this.$target.find('.carousel-indicators');
1044
1045             this.$editor.find(".js_add").on('click', _.bind(this.on_add, this));
1046             this.$editor.find(".js_remove").on('click', _.bind(this.on_remove, this));
1047
1048             this.change_background(".item.active", 'ul[name="carousel-background"]');
1049             this.change_style();
1050             this.change_size();
1051             this.set_options_style();
1052             this.$target.carousel();
1053
1054             var self = this;
1055             this.$target.on('slide.bs.carousel', function () {
1056                 self.set_options_style();
1057                 self.set_options_background(".item.active", 'ul[name="carousel-background"]');
1058                 self.$target.carousel();
1059             });
1060
1061             this.$target.on('dblclick', '.item.active .carousel-image img', function (event) {
1062                 $('#oe_rte_toolbar .cke_button__image .cke_button__image_icon').click();
1063             });
1064
1065             this.rebind_event();
1066         },
1067         // rebind event to active carousel on edit mode
1068         rebind_event: function () {
1069             var self = this;
1070             this.$target.off('click').on('click', '.carousel-control', function () {
1071                 self.$target.carousel($(this).data('slide')); });
1072             this.$target.off('click').on('click', '.carousel-indicators [data-target]', function () {
1073                 self.$target.carousel(+$(this).data('slide-to')); });
1074         },
1075         on_add: function (e) {
1076             e.preventDefault();
1077             var cycle = this.$inner.find('.item').length;
1078             var $active = this.$inner.find('.item.active');
1079             var index = $active.index();
1080             this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden");
1081             this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
1082             
1083             var $clone = this.$el.find(".item.active").clone();
1084             var bg = this.$editor.find('ul[name="carousel-background"] li:not([data-value="'+ $active.css("background-image").replace(/.*:\/\/[^\/]+|\)$/g, '') +'"]):first').data("value");
1085             $clone.css("background-image", "url('"+ bg +"')");
1086             $clone.removeClass('active').insertAfter($active);
1087             this.$target.carousel().carousel(++index);
1088             this.rebind_event();
1089         },
1090         on_remove: function (e) {
1091             e.preventDefault();
1092             var self = this;
1093             var new_index = 0;
1094             var cycle = this.$inner.find('.item').length - 1;
1095             var index = this.$inner.find('.item.active').index();
1096             if (cycle > 0) {
1097                 this.$inner.find('.item.active').fadeOut(1000, function () {
1098                     $(this).remove();
1099                     self.$indicators.find('[data-target]:last').remove();
1100                     self.$indicators.find("[data-slide-to]").removeClass("active");
1101                     self.$indicators.find("[data-slide-to='" + new_index + "']").addClass("active");
1102                 });
1103                 setTimeout(function () {
1104                     new_index = index % cycle;
1105                     self.$target.carousel().carousel( new_index + 1 );
1106                     self.rebind_event();
1107                 }, 500);
1108             } else {
1109                 this.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
1110             }
1111         },
1112         set_options_style: function () {
1113             var style = false;
1114             var $el = this.$inner.find('.item.active');
1115             var $ul = this.$editor.find('ul[name="carousel-style"]');
1116             var $li = $ul.find("li");
1117
1118             if ($el.hasClass('text_only'))
1119                 style = 'text_only';
1120             if ($el.hasClass('image_text'))
1121                 style = 'image_text';
1122             if ($el.hasClass('text_image'))
1123                 style = 'text_image';
1124
1125             $ul.find('[data-value="' + style + '"]').addClass('active');
1126         },
1127         change_style: function () {
1128             var self = this;
1129             var $ul = this.$editor.find('ul[name="carousel-style"]');
1130             var $li = $ul.find("li");
1131
1132             $li.on('click', function (event) {
1133                     $li.removeClass("active");
1134                     $(this).addClass("active");
1135                 })
1136                 .on('mouseover', function (event) {
1137                     var $el = self.$inner.find('.item.active');
1138                     $el.removeClass('image_text text_image text_only');
1139                     $el.addClass($(event.currentTarget).data("value"));
1140                 })
1141                 .on('mouseout', function (event) {
1142                     var $el = self.$inner.find('.item.active');
1143                     $el.removeClass('image_text text_image text_only');
1144                     $el.addClass($ul.find('li.active').data("value"));
1145                 });
1146         },
1147         change_size: function () {
1148             var self = this;
1149             var $el = this.$target;
1150
1151             var size = 'oe_big';
1152             if (this.$target.hasClass('oe_small'))
1153                 size = 'oe_small';
1154             else if (this.$target.hasClass('oe_medium'))
1155                 size = 'oe_medium';
1156
1157             var $ul = this.$editor.find('ul[name="carousel-size"]');
1158             var $li = $ul.find("li");
1159
1160             $ul.find('[data-value="' + size + '"]').addClass('active');
1161
1162             $li.on('click', function (event) {
1163                     $li.removeClass("active");
1164                     $(this).addClass("active");
1165                 })
1166                 .on('mouseover', function (event) {
1167                     $el.removeClass('oe_big oe_small oe_medium');
1168                     $el.addClass($(event.currentTarget).data("value"));
1169                 })
1170                 .on('mouseout', function (event) {
1171                     $el.removeClass('oe_big oe_small oe_medium');
1172                     $el.addClass($ul.find('li.active').data("value"));
1173                 });
1174         }
1175     });
1176
1177     website.snippet.editorRegistry.parallax = website.snippet.editorRegistry.resize.extend({
1178         start : function () {
1179             this._super();
1180             this.change_background(this.$target, 'ul[name="parallax-background"]');
1181         },
1182     });
1183
1184     /*
1185     * data-snippet-id automatically setted
1186     * Don't need to add data-snippet-id="..." into the views
1187     */
1188
1189     website.snippet.selector.push([".row > div[class*='col-md-']", 'colmd']);
1190     website.snippet.selector.push(['hr', 'hr']);
1191
1192 })();