dc1c78251a23b0b660b08c5f846f20ab908d78c2
[odoo/odoo.git] / addons / website / static / src / js / website.snippets.editor.js
1 (function () {
2     'use strict';
3
4 /*  Building block / Snippet Editor
5  
6     The building blocks appear in the edit bar website. These prebuilt html block
7     allowing the designer to easily generate content on a page (drag and drop).
8     Options allow snippets to add customizations part html code according to their
9     selector (jQuery) and javascript object.
10     
11     How to create content?
12
13     Designers can add their own html block in the "snippets" (/website/views/snippets.xml).
14     The block must be added in one of four menus (structure, content, feature or effect).
15     Structure:
16         <div>
17             <div class="oe_snippet_thumbnail">
18                 <img class="oe_snippet_thumbnail_img" src="...image src..."/>
19                 <span class="oe_snippet_thumbnail_title">...Block Name...</span>
20             </div>
21             <div class="oe_snippet_body">
22                 ...
23                 <!-- 
24                     The block with class 'oe_snippet_body' is inserted in the page.
25                     This class is removed when the block is dropped.
26                     The block can be made of any html tag and content. -->
27             </div>
28         </div>
29
30     How to create options?
31
32     Designers can add their own html block in the "snippet_options" (/website/views/snippets.xml).
33     Structure:
34
35         <div data-snippet-option-id='...'           <!-- Required: javascript object id (but javascript
36                                                         for this option object is not required) -->
37             data-selector="..."                     <!-- Required: jQuery selector.
38                                                         Apply options on all The part of html who 
39                                                         match with this jQuery selector.
40                                                         E.g.: If the selector is div, all div will be selected
41                                                         and can be highlighted and assigned an editor.  -->
42             data-selector-siblings="..."            <!-- Optional: jQuery selector.
43                                                         The html part can be insert or move beside
44                                                         the selected html block -->
45             data-selector-children="..."            <!-- Optional: jQuery selector.
46                                                         The html part can be insert or move inside
47                                                         the selected html block -->
48             data-selector-vertical-children='...'>  <!-- Optional: jQuery selector.
49                                                         The html part can be insert or move inside
50                                                         the selected html block. The drop zone is
51                                                         displayed vertically -->
52                 ...
53                 <li><a href="#">...</a></li>        <!-- Optional: html li list.
54                                                         List of menu items displayed in customize
55                                                         menu. If the li tag have 'data-class', the
56                                                         class is automaticcally added or removed to
57                                                         the html content when the user select this item. -->
58                 ...
59                 <li class="dropdown-submenu"                <!-- Optional: html li list exemple. !-->
60                     data-required="true">                   <!-- Optional: if only one item can be selected
61                                                                 and can't be unselect. !-->
62                     <a tabindex="-1" href="#">...</a>       <!-- bootstrap dropdown button !-->
63                     <ul class="dropdown-menu">
64                         <li data-value="text_only"><a>...</a></li>      <!-- by default data-value is apply
65                                                                             like a class to html block !-->
66                     </ul>
67                 </li>
68         </div>
69
70         How to create a javascript object for an options?
71
72         openerp.website.snippet.options["...option-id..."] = website.snippet.Option.extend({
73             // start is called when the user click into a block or when the user drop a block 
74             // into the page (just after the init method).
75             // start is usually used to bind event.
76             //
77             // this.$target: block html inserted inside the page
78             // this.$el: html li list of this options
79             // this.$overlay: html editor overlay who content resize bar, customize menu...
80             start: function () {},
81
82
83             // onFocus is called when the user click inside the block inserted in page
84             // and when the user drop on block into the page
85             onFocus : function () {},
86
87
88             // onBlur is called when the user click outside the block inserted in page, if
89             // the block is focused
90             onBlur : function () {},
91
92
93             // on_clone is called when the snippet is duplicate
94             // @variables: $clone is allready inserted is the page
95             on_clone: function ($clone) {},
96
97
98             // on_remove is called when the snippet is removed (dom is removing after this tigger)
99             on_remove: function () {},
100
101
102             // drop_and_build_snippet is called just after that a thumbnail is drag and dropped
103             // into a drop zone. The content is already inserted in the page.
104             drop_and_build_snippet: function () {},
105
106             // select is called when a user select an item in the li list of options
107             // By default, if the li item have a data-value attribute, the data-vlue it's apply
108             // like a class to the html block (this.$target)
109             // @variables: next_previous = {$next, $prev}
110             //      $next = next item selected or false
111             //      $prev = previous item selected or false
112             select: function (event, next_previous) {}
113
114             // preview is called when a user is on mouse over or mouse out of an item
115             // variables: next_previous = {$next, $prev}
116             //      $next = next item selected or false
117             //      $prev = previous item selected or false
118             preview: function (event, next_previous) {}
119
120             // clean_for_save
121             // clean_for_save is called just before to save the vue
122             // Sometime it's important to remove or add some datas (contentEditable, added 
123             // classes to a running animation...)
124             clean_for_save: function () {}
125         });
126
127
128     // 'snippet-dropped' is triggered on '#oe_snippets' whith $target as attribute when a snippet is dropped
129     // 'snippet-activated' is triggered on '#oe_snippets' (and on snippet) when a snippet is activated
130
131 */
132
133     var dummy = function () {};
134
135     var website = openerp.website;
136     website.add_template_file('/website/static/src/xml/website.snippets.xml');
137
138     website.EditorBar.include({
139         start: function () {
140             var self = this;
141             $("[data-oe-model]").on('click', function (event) {
142                 var $this = $(event.srcElement);
143                 var tag = $this[0] && $this[0].tagName.toLowerCase();
144                 if (!(tag === 'a' || tag === "button") && !$this.parents("a, button").length) {
145                     self.$('[data-action="edit"]').parent().effect('bounce', {distance: 18, times: 5}, 250);
146                 }
147             });
148             return this._super();
149         },
150         edit: function () {
151             var self = this;
152             $("[data-oe-model] *, [data-oe-type=html] *").off('click');
153             window.snippets = this.snippets = new website.snippet.BuildingBlock(this);
154             this.snippets.appendTo(this.$el);
155             website.snippet.stop_animation();
156             this.on('rte:ready', this, function () {
157                 self.snippets.$button.removeClass("hidden");
158                 website.snippet.start_animation();
159                 $("#wrapwrap *").off('mousedown mouseup click');
160             });
161
162             return this._super.apply(this, arguments);
163         },
164         save: function () {
165             this.snippets.clean_for_save();
166             this._super();
167         },
168     });
169
170     /* ----- SNIPPET SELECTOR ---- */
171
172     var observer = new website.Observer(function (mutations) {
173         if (!_(mutations).find(function (m) {
174                     return m.type === 'childList' && m.addedNodes.length > 0;
175                 })) {
176             return;
177         }
178     });
179
180     if (!website.snippet) website.snippet = {};
181     website.snippet.templateOptions = {};
182     website.snippet.globalSelector = "";
183     website.snippet.selector = [];
184     website.snippet.BuildingBlock = openerp.Widget.extend({
185         template: 'website.snippets',
186         activeSnippets: [],
187         init: function (parent) {
188             this.parent = parent;
189             this._super.apply(this, arguments);
190             if(!$('#oe_manipulators').length){
191                 $("<div id='oe_manipulators'></div>").appendTo('body');
192             }
193             this.$active_snipped_id = false;
194             this.snippets = [];
195
196             observer.observe(document.body, {
197                 childList: true,
198                 subtree: true,
199             });
200         },
201         dom_filter: function (dom, sibling) {
202             if (typeof dom === "string") {
203                 var include = "[data-oe-model]";
204                 var sdom = dom.split(',');
205                 dom = "";
206                 _.each(sdom, function (val) {
207                     val = val.replace(/^\s+|\s+$/g, '');
208                     dom += include + " " + val + ", ";
209                     if (!sibling) {
210                         val = val.split(" ");
211                         dom += val.shift() + include + val.join(" ") + ", ";
212                     }
213                 });
214                 dom = dom.replace(/,\s*$/g, '');
215                 return $(dom);
216             } else {
217                 return (!sibling && $(dom).is("[data-oe-model]")) || $(dom).parents("[data-oe-model]").length ? $(dom) : $("");
218             }
219         },
220         start: function() {
221             var self = this;
222
223             this.$button = $(openerp.qweb.render('website.snippets_button'))
224                 .prependTo(this.parent.$("#website-top-edit ul"))
225                 .find("button");
226
227             this.$button.click(function () {
228                 self.make_active(false);
229                 self.$el.toggleClass("hidden");
230             });
231             $("#wrapwrap").click(function () {
232                 self.$el.addClass("hidden");
233             });
234
235             this.fetch_snippet_templates();
236
237             this.bind_snippet_click_editor();
238
239             this.$el.addClass("hidden");
240
241             $(document).on('click', '.dropdown-submenu a[tabindex]', function (e) {
242                 e.preventDefault();
243             });
244
245             this.getParent().on('change:height', this, function (editor) {
246                 self.$el.css('top', editor.get('height'));
247             });
248             self.$el.css('top', this.parent.get('height'));
249         },
250         _get_snippet_url: function () {
251             return '/website/snippets';
252         },
253         fetch_snippet_templates: function () {
254             var self = this;
255
256             openerp.jsonRpc(this._get_snippet_url(), 'call', {})
257                 .then(function (html) {
258                     var $html = $(html);
259
260                     var selector = [];
261                     var $styles = $html.find("[data-snippet-option-id]");
262                     $styles.each(function () {
263                         var $style = $(this);
264                         var style_id = $style.data('snippet-option-id');
265                         website.snippet.templateOptions[style_id] = {
266                             'snippet-option-id' : style_id,
267                             'selector': $style.data('selector'),
268                             '$el': $style,
269                             'selector-siblings': $style.data('selector-siblings'),
270                             'selector-children': $style.data('selector-children'),
271                             'selector-vertical-children': $style.data('selector-vertical-children'),
272                             'data': $style.data()
273                         };
274                         selector.push($style.data('selector'));
275                     });
276                     $styles.addClass("hidden");
277                     website.snippet.globalSelector = selector.join(",");
278
279                     self.$snippets = $html.find(".tab-content > div > div").addClass("oe_snippet");
280                     self.$el.append($html);
281
282
283                     var snippets = 0;
284                     self.$snippets.each(function () {
285                         if (self.snippet_have_dropzone($(this)))
286                             snippets++;
287                     });
288                     if (!snippets) self.$button.css("display", "none");
289
290                     self.make_snippet_draggable(self.$snippets);
291                 });
292         },
293         cover_target: function ($el, $target){
294             var pos = $target.offset();
295             var mt = parseInt($target.css("margin-top") || 0);
296             var mb = parseInt($target.css("margin-bottom") || 0);
297             $el.css({
298                 'width': $target.outerWidth(),
299                 'top': pos.top - mt - 5,
300                 'left': pos.left
301             });
302             $el.find(".oe_handle.e,.oe_handle.w").css({'height': $target.outerHeight() + mt + mb+1});
303             $el.find(".oe_handle.s").css({'top': $target.outerHeight() + mt + mb});
304             $el.find(".oe_handle.size").css({'top': $target.outerHeight() + mt});
305             $el.find(".oe_handle.s,.oe_handle.n").css({'width': $target.outerWidth()-2});
306         },
307         show: function () {
308             this.$el.removeClass("hidden");
309         },
310         hide: function () {
311             this.$el.addClass("hidden");
312         },
313
314         snippet_have_dropzone: function ($snippet) {
315             return (($snippet.data('selector-siblings') && this.dom_filter($snippet.data('selector-siblings')).size() > 0) ||
316                     ($snippet.data('selector-children') && this.dom_filter($snippet.data('selector-children')).size() > 0) ||
317                     ($snippet.data('selector-vertical-children') && this.dom_filter($snippet.data('selector-vertical-children')).size() > 0));
318         },
319
320         bind_snippet_click_editor: function () {
321             var self = this;
322             var snipped_event_flag;
323             $("#wrapwrap").on('click', function (event) {
324                 var srcElement = event.srcElement || (event.originalEvent && (event.originalEvent.originalTarget || event.originalEvent.target));
325                 if (snipped_event_flag || !srcElement) {
326                     return;
327                 }
328                 snipped_event_flag = true;
329
330                 setTimeout(function () {snipped_event_flag = false;}, 0);
331                 var $target = $(srcElement);
332
333                 if ($target.parents(".oe_overlay").length) {
334                     return;
335                 }
336
337                 if (!$target.is(website.snippet.globalSelector)) {
338                     $target = $target.parents(website.snippet.globalSelector).first();
339                 }
340
341                 if (!self.dom_filter($target).length) {
342                     $target = false;
343                 }
344                 if (self.$active_snipped_id && self.$active_snipped_id.is($target)) {
345                     return;
346                 }
347                 self.make_active($target);
348             });
349         },
350         snippet_blur: function ($snippet) {
351             if ($snippet) {
352                 if ($snippet.data("snippet-editor")) {
353                     $snippet.data("snippet-editor").onBlur();
354                 }
355             }
356         },
357         snippet_focus: function ($snippet) {
358             if ($snippet) {
359                 if ($snippet.data("snippet-editor")) {
360                     $snippet.data("snippet-editor").onFocus();
361                 }
362             }
363         },
364         clean_for_save: function () {
365             var self = this;
366             var options = website.snippet.options;
367             var template = website.snippet.templateOptions;
368             for (var k in options) {
369                 if (template[k] && options[k].prototype.clean_for_save !== dummy) {
370                     var $snippet = this.dom_filter(template[k].selector);
371                     $snippet.each(function () {
372                         new options[k](self, null, $(this), k).clean_for_save();
373                     });
374                 }
375             }
376             $("*[contentEditable], *[attributeEditable]")
377                 .removeAttr('contentEditable')
378                 .removeAttr('attributeEditable');
379         },
380         make_active: function ($snippet) {
381             if ($snippet && this.$active_snipped_id && this.$active_snipped_id.get(0) === $snippet.get(0)) {
382                 return;
383             }
384             if (this.$active_snipped_id) {
385                 this.snippet_blur(this.$active_snipped_id);
386                 this.$active_snipped_id = false;
387             }
388             if ($snippet && $snippet.length) {
389                 if(_.indexOf(this.snippets, $snippet.get(0)) === -1) {
390                     this.snippets.push($snippet.get(0));
391                 }
392                 this.$active_snipped_id = $snippet;
393                 this.create_overlay(this.$active_snipped_id);
394                 this.snippet_focus($snippet);
395             }
396             $("#oe_snippets").trigger('snippet-activated', $snippet);
397             if ($snippet) {
398                 $snippet.trigger('snippet-activated', $snippet);
399             }
400         },
401         create_overlay: function ($snippet) {
402             if (typeof $snippet.data("snippet-editor") === 'undefined') {
403                 var $targets = this.activate_overlay_zones($snippet);
404                 if (!$targets.length) return;
405                 $snippet.data("snippet-editor", new website.snippet.Editor(this, $snippet));
406             }
407             this.cover_target($snippet.data('overlay'), $snippet);
408         },
409
410         path_eval: function(path){
411             var obj = window;
412             path = path.split('.');
413             do{
414                 obj = obj[path.shift()];
415             }while(path.length && obj);
416             return obj;
417         },
418
419         // activate drag and drop for the snippets in the snippet toolbar
420         make_snippet_draggable: function($snippets){
421             var self = this;
422             var $tumb = $snippets.find(".oe_snippet_thumbnail:first");
423             var left = $tumb.outerWidth()/2;
424             var top = $tumb.outerHeight()/2;
425             var $toInsert, dropped, $snippet, action, snipped_id;
426
427             $snippets.draggable({
428                 greedy: true,
429                 helper: 'clone',
430                 zIndex: '1000',
431                 appendTo: 'body',
432                 cursor: "move",
433                 handle: ".oe_snippet_thumbnail",
434                 cursorAt: {
435                     'left': left,
436                     'top': top
437                 },
438                 start: function(){
439                     self.hide();
440                     dropped = false;
441                     // snippet_selectors => to get selector-siblings, selector-children, selector-vertical-children
442                     $snippet = $(this);
443                     $toInsert = $snippet.find('.oe_snippet_body').clone();
444
445                     var selector = [];
446                     var selector_siblings = [];
447                     var selector_children = [];
448                     var selector_vertical_children = [];
449                     for (var k in website.snippet.templateOptions) {
450                         if ($toInsert.is(website.snippet.templateOptions[k].selector)) {
451                             selector.push(website.snippet.templateOptions[k].selector);
452                             if (website.snippet.templateOptions[k]['selector-siblings'])
453                                 selector_siblings.push(website.snippet.templateOptions[k]['selector-siblings']);
454                             if (website.snippet.templateOptions[k]['selector-children'])
455                                 selector_children.push(website.snippet.templateOptions[k]['selector-children']);
456                             if (website.snippet.templateOptions[k]['selector-vertical-children'])
457                                 selector_vertical_children.push(website.snippet.templateOptions[k]['selector-vertical-children']);
458                         }
459                     }
460
461                     action = $snippet.find('.oe_snippet_body').size() ? 'insert' : 'mutate';
462
463                     if( action === 'insert'){
464                         if (!selector_siblings.length && !selector_children.length && !selector_vertical_children.length) {
465                             console.debug($snippet.data("snippet-id") + " have oe_snippet_body class and have not for insert action"+
466                                 "data-selector-siblings, data-selector-children or data-selector-vertical-children tag for mutate action");
467                             return;
468                         }
469                         self.activate_insertion_zones({
470                             siblings: selector_siblings.join(","),
471                             children: selector_children.join(","),
472                             vertical_children: selector_vertical_children.join(","),
473                         });
474
475                     } else if( action === 'mutate' ){
476                         if (!$snippet.data('selector')) {
477                             console.debug($snippet.data("snippet-id") + " have not oe_snippet_body class and have not data-selector tag");
478                             return;
479                         }
480                         var $targets = self.activate_overlay_zones(selector_children.join(","));
481                         $targets.each(function(){
482                             var $clone = $(this).data('overlay').clone();
483                              $clone.addClass("oe_drop_zone").data('target', $(this));
484                             $(this).data('overlay').after($clone);
485                         });
486
487                     }
488
489                     $('.oe_drop_zone').droppable({
490                         over:   function(){
491                             if( action === 'insert'){
492                                 dropped = true;
493                                 $(this).first().after($toInsert);
494                             }
495                         },
496                         out:    function(){
497                             var prev = $toInsert.prev();
498                             if( action === 'insert' && this === prev[0]){
499                                 dropped = false;
500                                 $toInsert.detach();
501                             }
502                         }
503                     });
504                 },
505                 stop: function(ev, ui){
506                     $toInsert.removeClass('oe_snippet_body');
507                     
508                     if (action === 'insert' && ! dropped && $('.oe_drop_zone') && ui.position.top > 3) {
509                         var el = $('.oe_drop_zone').nearest({x: ui.position.left, y: ui.position.top}).first();
510                         if (el.length) {
511                             el.after($toInsert);
512                             dropped = true;
513                         }
514                     }
515
516                     $('.oe_drop_zone').droppable('destroy').remove();
517                     
518                     if (dropped) {
519                         var $target = false;
520                         $target = $toInsert;
521
522                         setTimeout(function () {
523                             $("#oe_snippets").trigger('snippet-dropped', $target);
524
525                             website.snippet.start_animation(true, $target);
526                             // drop_and_build_snippet
527                             self.create_overlay($target);
528                             if ($target.data("snippet-editor")) {
529                                 $target.data("snippet-editor").drop_and_build_snippet();
530                             }
531                             for (var k in website.snippet.templateOptions) {
532                                 $target.find(website.snippet.templateOptions[k].selector).each(function () {
533                                     var $snippet = $(this);
534                                     self.create_overlay($snippet);
535                                     if ($snippet.data("snippet-editor")) {
536                                         $snippet.data("snippet-editor").drop_and_build_snippet();
537                                     }
538                                 });
539                             }
540
541                             // reset snippet for rte
542                             $target.removeData("snippet-editor");
543                             if ($target.data("overlay")) {
544                                 $target.data("overlay").remove();
545                                 $target.removeData("overlay");
546                             }
547                             $target.find(website.snippet.globalSelector).each(function () {
548                                 var $snippet = $(this);
549                                 $snippet.removeData("snippet-editor");
550                                 if ($snippet.data("overlay")) {
551                                     $snippet.data("overlay").remove();
552                                     $snippet.removeData("overlay");
553                                 }
554                             });
555                             self.create_overlay($target);
556                             // end
557
558                             self.make_active($target);
559                         },0);
560                     } else {
561                         $toInsert.remove();
562                     }
563                 },
564             });
565         },
566
567         // return the original snippet in the editor bar from a snippet id (string)
568         get_snippet_from_id: function(id){
569             return $('.oe_snippet').filter(function(){
570                     return $(this).data('snippet-id') === id;
571                 }).first();
572         },
573
574         // Create element insertion drop zones. two css selectors can be provided
575         // selector.children -> will insert drop zones as direct child of the selected elements
576         //   in case the selected elements have children themselves, dropzones will be interleaved
577         //   with them.
578         // selector.siblings -> will insert drop zones after and before selected elements
579         activate_insertion_zones: function(selector){
580             var self = this;
581             var child_selector = selector.children;
582             var sibling_selector = selector.siblings;
583             var vertical_child_selector   =  selector.vertical_children;
584
585             var zone_template = "<div class='oe_drop_zone oe_insert'></div>";
586
587             if(child_selector){
588                 self.dom_filter(child_selector).each(function (){
589                     var $zone = $(this);
590                     $zone.find('> *:not(.oe_drop_zone):visible').after(zone_template);
591                     $zone.prepend(zone_template);
592                 });
593             }
594
595             if(vertical_child_selector){
596                 self.dom_filter(vertical_child_selector).each(function (){
597                     var $zone = $(this);
598                     var $template = $(zone_template).addClass("oe_vertical");
599                     var nb = 0;
600                     var $lastinsert = false;
601                     var left = 0;
602                     var temp_left = 0;
603                     $zone.find('> *:not(.oe_drop_zone):visible').each(function () {
604                         var $col = $(this);
605                         $template.css('height', ($col.outerHeight() + parseInt($col.css("margin-top")) + parseInt($col.css("margin-bottom")))+'px');
606                         $lastinsert = $template.clone();
607                         $(this).after($lastinsert);
608
609                         temp_left = $col.position().left;
610                         if (left === temp_left) {
611                             $col.prev(".oe_drop_zone.oe_vertical").remove();
612                             $col.before($template.clone().css("clear", "left"));
613                         }
614                         else if (!nb) {
615                             $col.before($template.clone());
616                         }
617                         left = temp_left;
618                         nb ++;
619                     });
620                     if (!nb) {
621                         $zone.prepend($template.css('height', $zone.outerHeight()+'px'));
622                     }
623                 });
624             }
625
626             if(sibling_selector){
627                 self.dom_filter(sibling_selector, true).each(function (){
628                     var $zone = $(this);
629                     if($zone.prev('.oe_drop_zone:visible').length === 0){
630                         $zone.before(zone_template);
631                     }
632                     if($zone.next('.oe_drop_zone:visible').length === 0){
633                         $zone.after(zone_template);
634                     }
635                 });
636             }
637
638             var count;
639             do {
640                 count = 0;
641                 // var $zones = $('.oe_drop_zone + .oe_drop_zone');    // no two consecutive zones
642                 // count += $zones.length;
643                 // $zones.remove();
644
645                 $zones = $('.oe_drop_zone > .oe_drop_zone:not(.oe_vertical)').remove();   // no recursive zones
646                 count += $zones.length;
647                 $zones.remove();
648             } while (count > 0);
649
650             // Cleaning up zones placed between floating or inline elements. We do not like these kind of zones.
651             var $zones = $('.oe_drop_zone:not(.oe_vertical)');
652             $zones.each(function (){
653                 var zone = $(this);
654                 var prev = zone.prev();
655                 var next = zone.next();
656                 var float_prev = prev.css('float')   || 'none';
657                 var float_next = next.css('float')   || 'none';
658                 var disp_prev  = prev.css('display') ||  null;
659                 var disp_next  = next.css('display') ||  null;
660                 if(     (float_prev === 'left' || float_prev === 'right')
661                     &&  (float_next === 'left' || float_next === 'right')  ){
662                     zone.remove();
663                 }else if( !( disp_prev === null
664                           || disp_next === null
665                           || disp_prev === 'block'
666                           || disp_next === 'block' )){
667                     zone.remove();
668                 }
669             });
670         },
671
672         // generate drop zones covering the elements selected by the selector
673         // we generate overlay drop zones only to get an idea of where the snippet are, the drop
674         activate_overlay_zones: function(selector){
675             var $targets = this.dom_filter(selector);
676             var self = this;
677
678             if (typeof selector !== 'string' && !$targets.length) {
679                 console.debug( "A good node must have a [data-oe-model] attribute or must have at least one parent with [data-oe-model] attribute.");
680                 console.debug( "Wrong node(s): ", selector);
681             }
682
683             function is_visible($el){
684                 return     $el.css('display')    != 'none'
685                         && $el.css('opacity')    != '0'
686                         && $el.css('visibility') != 'hidden';
687             }
688
689             // filter out invisible elements
690             $targets = $targets.filter(function(){ return is_visible($(this)); });
691
692             // filter out elements with invisible parents
693             $targets = $targets.filter(function(){
694                 var parents = $(this).parents().filter(function(){ return !is_visible($(this)); });
695                 return parents.length === 0;
696             });
697
698             $targets.each(function () {
699                 var $target = $(this);
700                 if (!$target.data('overlay')) {
701                     var $zone = $(openerp.qweb.render('website.snippet_overlay'));
702
703                     // fix for pointer-events: none with ie9
704                     if (document.body && document.body.addEventListener) {
705                         $zone.on("click mousedown mousedown", function passThrough(event) {
706                             event.preventDefault();
707                             $target.each(function() {
708                                // check if clicked point (taken from event) is inside element
709                                 event.srcElement = this;
710                                 $(this).trigger(event.type);
711                             });
712                             return false;
713                         });
714                     }
715
716                     $zone.appendTo('#oe_manipulators');
717                     $zone.data('target',$target);
718                     $target.data('overlay',$zone);
719
720                     $target.on("DOMNodeInserted DOMNodeRemoved DOMSubtreeModified", function () {
721                         self.cover_target($zone, $target);
722                     });
723                     $('body').on("resize", function () {
724                         self.cover_target($zone, $target);
725                     });
726                 }
727                 self.cover_target($target.data('overlay'), $target);
728             });
729             return $targets;
730         }
731     });
732
733
734     website.snippet.options = {};
735     website.snippet.Option = openerp.Class.extend({
736         // initialisation (don't overwrite)
737         init: function (BuildingBlock, editor, $target, snippet_id) {
738             this.BuildingBlock = BuildingBlock;
739             this.editor = editor;
740             this.$target = $target;
741             var styles = this.$target.data("snippet-option-ids") || {};
742             styles[snippet_id] = this;
743             this.$target.data("snippet-option-ids", styles);
744             this.$overlay = this.$target.data('overlay') || $('<div>');
745             this['snippet-option-id'] = snippet_id;
746             var $option = website.snippet.templateOptions[snippet_id].$el;
747             this.$el = $option.find(">li").clone();
748             this.data = $option.data();
749
750             this.required = this.$el.data("required");
751
752             this.set_active();
753             this.$el.find('li[data-value] a').on('mouseenter mouseleave click', _.bind(this._mouse, this));
754             this.$el.not(':not([data-value])').find("a").on('mouseenter mouseleave click', _.bind(this._mouse, this));
755             this.$target.on('snippet-style-reset', _.bind(this.set_active, this));
756
757             this.start();
758         },
759         _mouse: function (event) {
760             var self = this;
761
762             if (event.type === 'mouseleave') {
763                 if (!this.over) return;
764                 this.over = false;
765             } else if (event.type === 'click') {
766                 this.over = false;
767             }else {
768                 this.over = true;
769             }
770
771             var $prev, $next;
772             if (event.type === 'mouseleave') {
773                 $prev = $(event.currentTarget).parent();
774                 $next = this.$el.find("li[data-value].active");
775             } else {
776                 $prev = this.$el.find("li[data-value].active");
777                 $next = $(event.currentTarget).parent();
778             }
779             if (!$prev.length) {
780                 $prev = false;
781             }
782             if ($prev && $prev[0] === $next[0]) {
783                 $next = false;
784                 if (this.required) {
785                     return;
786                 }
787             }
788
789             var np = {'$next': $next, '$prev': $prev};
790
791             if (event.type === 'click') {
792                 setTimeout(function () {
793                     self.set_active();
794                     self.$target.trigger("snippet-style-change", [self, np]);
795                 },0);
796                 this.select({'$next': $next, '$prev': $prev});
797             } else {
798                 setTimeout(function () {
799                     self.$target.trigger("snippet-style-preview", [self, np]);
800                 },0);
801                 this.preview(np);
802             }
803         },
804         /* set_active
805         *  select and set item active or not (add highlight item and his parents)
806         *  called before start
807         */
808         set_active: function () {
809             var self = this;
810             this.$el.find('li').removeClass("active");
811             var $active = this.$el.find('li[data-value]')
812                 .filter(function () {
813                     var $li = $(this);
814                     return  ($li.data('value') && self.$target.hasClass($li.data('value')));
815                 })
816                 .first()
817                 .addClass("active");
818             this.$el.find('li:has(li[data-value].active)').addClass("active");
819         },
820
821         start: function () {
822         },
823
824         onFocus : function () {
825         },
826
827         onBlur : function () {
828         },
829
830         on_clone: function ($clone) {
831         },
832
833         on_remove: function () {
834         },
835
836         drop_and_build_snippet: function () {
837         },
838
839         select: function (np) {
840             var self = this;
841             // add or remove html class
842             if (np.$prev && this.required) {
843                 this.$target.removeClass(np.$prev.data('value' || ""));
844             }
845             if (np.$next) {
846                 this.$target.addClass(np.$next.data('value') || "");
847             }
848         },
849
850         preview: function (np) {
851             var self = this;
852
853             // add or remove html class
854             if (np.$prev) {
855                 this.$target.removeClass(np.$prev.data('value') || "");
856             }
857             if (np.$next) {
858                 this.$target.addClass(np.$next.data('value') || "");
859             }
860         },
861
862         clean_for_save: dummy
863     });
864
865     website.snippet.options.background = website.snippet.Option.extend({
866         _get_bg: function () {
867             return this.$target.css("background-image").replace(/url\(['"]*|['"]*\)|^none$/g, "");
868         },
869         _set_bg: function (src) {
870             this.$target.css("background-image", src && src !== "" ? 'url(' + src + ')' : "");
871         },
872         start: function () {
873             this._super();
874             var src = this._get_bg();
875             this.$el.find("li[data-value].active.oe_custom_bg").data("src", src);
876         },
877         select: function(np) {
878             var self = this;
879             this._super(np);
880             if (np.$next) {
881                 if (np.$next.hasClass("oe_custom_bg")) {
882                     var $image = $('<img class="hidden"/>');
883                     $image.attr("src", np.$prev ? np.$prev.data("src") : '');
884                     $image.appendTo(self.$target);
885
886                     self.element = new CKEDITOR.dom.element($image[0]);
887                     var editor = new website.editor.MediaDialog(self, self.element);
888                     editor.appendTo(document.body);
889                     editor.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
890
891                     $image.on('saved', self, function (o) {
892                         var src = $image.attr("src");
893                         self._set_bg(src);
894                         np.$next.data("src", src);
895                         self.$target.trigger("snippet-style-change", [self, np]);
896                         $image.remove();
897                     });
898                     editor.on('cancel', self, function () {
899                         if (!np.$prev || np.$prev.data("src") === "") {
900                             self.$target.removeClass(np.$next.data("value"));
901                             self.$target.trigger("snippet-style-change", [self, np]);
902                         }
903                         $image.remove();
904                     });
905                 } else {
906                     this._set_bg(np.$next.data("src"));
907                 }
908             } else {
909                 this._set_bg(false);
910                 this.$target.removeClass(np.$prev.data("value"));
911             }
912         },
913         preview: function (np) {
914             this._super(np);
915             if (np.$next) {
916                 this._set_bg(np.$next.data("src"));
917             }
918         },
919         set_active: function () {
920             var self = this;
921             var bg = self.$target.css("background-image");
922             this.$el.find('li').removeClass("active");
923             this.$el.find('li').removeClass("btn-primary");
924             var $active = this.$el.find('li[data-value]')
925                 .filter(function () {
926                     var $li = $(this);
927                     return  ($li.data('src') && bg.indexOf($li.data('src')) >= 0) ||
928                             (!$li.data('src') && self.$target.hasClass($li.data('value')));
929                 })
930                 .first();
931             if (!$active.length) {
932                 $active = this.$target.css("background-image") !== 'none' ?
933                     this.$el.find('li[data-value].oe_custom_bg') :
934                     this.$el.find('li[data-value=""]');
935             }
936
937             //don't set active on an OpenDialog link, else it not possible to click on it again after.
938             // TODO in Saas-4 - Once bootstrap is in less
939             //      - add a class active-style to get the same display but without the active behaviour used by bootstrap in JS.
940             var classStr = _.string.contains($active[0].className, "oe_custom_bg") ? "btn-primary" : "active";
941             $active.addClass(classStr);
942             this.$el.find('li:has(li[data-value].active)').addClass(classStr);
943         }
944     });
945
946     website.snippet.options.slider = website.snippet.Option.extend({
947         unique_id: function () {
948             var id = 0;
949             $(".carousel").each(function () {
950                 var cid = 1 + parseInt($(this).attr("id").replace(/[^0123456789]/g, ''),10);
951                 if (id < cid) id = cid;
952             });
953             return "myCarousel" + id;
954         },
955         drop_and_build_snippet: function() {
956             this.id = this.unique_id();
957             this.$target.attr("id", this.id);
958             this.$target.find("[data-slide]").attr("data-cke-saved-href", "#" + this.id);
959             this.$target.find("[data-target]").attr("data-target", "#" + this.id);
960             this.rebind_event();
961         },
962         on_clone: function ($clone) {
963             var id = this.unique_id();
964             $clone.attr("id", id);
965             $clone.find("[data-slide]").attr("href", "#" + id);
966             $clone.find("[data-slide-to]").attr("data-target", "#" + id);
967         },
968         // rebind event to active carousel on edit mode
969         rebind_event: function () {
970             var self = this;
971             this.$target.find('.carousel-indicators [data-slide-to]').off('click').on('click', function () {
972                 self.$target.carousel(+$(this).data('slide-to')); });
973
974             this.$target.attr('contentEditable', 'false');
975             this.$target.find('.oe_structure, .content>.row, [data-slide]').attr('contentEditable', 'true');
976         },
977         clean_for_save: function () {
978             this._super();
979             this.$target.find(".item").removeClass("next prev left right active");
980             this.$target.find(".item:first").addClass("active");
981             this.$indicators.find('li').removeClass('active');
982             this.$indicators.find('li:first').addClass('active');
983         },
984         start : function () {
985             var self = this;
986             this._super();
987             this.$target.carousel({interval: false});
988             this.id = this.$target.attr("id");
989             this.$inner = this.$target.find('.carousel-inner');
990             this.$indicators = this.$target.find('.carousel-indicators');
991
992             this.$el.find(".js_add").on('click', function () {self.on_add_slide(); return false;});
993             this.$el.find(".js_remove").on('click', function () {self.on_remove_slide(); return false;});
994
995             this.$target.carousel('pause');
996             this.rebind_event();
997         },
998         on_add_slide: function () {
999             var self = this;
1000             var cycle = this.$inner.find('.item').length;
1001             var $active = this.$inner.find('.item.active, .item.prev, .item.next').first();
1002             var index = $active.index();
1003             this.$target.find('.carousel-control, .carousel-indicators').removeClass("hidden");
1004             this.$indicators.append('<li data-target="#' + this.id + '" data-slide-to="' + cycle + '"></li>');
1005
1006             // clone the best candidate from template to use new features
1007             var $snippets = this.BuildingBlock.$snippets.find('.oe_snippet_body.carousel');
1008             var point = 0;
1009             var selection;
1010             var className = _.compact(this.$target.attr("class").split(" "));
1011             $snippets.each(function () {
1012                 var len = _.intersection(_.compact(this.className.split(" ")), className).length;
1013                 if (len > point) {
1014                     point = len;
1015                     selection = this;
1016                 }
1017             });
1018             var $clone = $(selection).find('.item:first').clone();
1019
1020             // insert
1021             $clone.removeClass('active').insertAfter($active);
1022             setTimeout(function() {
1023                 self.$target.carousel().carousel(++index);
1024                 self.rebind_event();
1025             },0);
1026             return $clone;
1027         },
1028         on_remove_slide: function () {
1029             if (this.remove_process) {
1030                 return;
1031             }
1032             var self = this;
1033             var new_index = 0;
1034             var cycle = this.$inner.find('.item').length - 1;
1035             var index = this.$inner.find('.item.active').index();
1036
1037             if (cycle > 0) {
1038                 this.remove_process = true;
1039                 var $el = this.$inner.find('.item.active');
1040                 self.$target.on('slid.bs.carousel', function (event) {
1041                     $el.remove();
1042                     self.$indicators.find("li:last").remove();
1043                     self.$target.off('slid.bs.carousel');
1044                     self.rebind_event();
1045                     self.remove_process = false;
1046                     if (cycle == 1) {
1047                         self.on_remove_slide(event);
1048                     }
1049                 });
1050                 setTimeout(function () {
1051                     self.$target.carousel( index > 0 ? --index : cycle );
1052                 }, 500);
1053             } else {
1054                 this.$target.find('.carousel-control, .carousel-indicators').addClass("hidden");
1055             }
1056         },
1057     });
1058     website.snippet.options.carousel = website.snippet.options.slider.extend({
1059         getSize: function () {
1060             this.grid = this._super();
1061             this.grid.size = 8;
1062             return this.grid;
1063         },
1064         clean_for_save: function () {
1065             this._super();
1066             this.$target.css("background-image", "");
1067             this.$target.removeClass(this._class);
1068         },
1069         load_style_options : function () {
1070             this._super();
1071             $(".snippet-style-size li[data-value='']").remove();
1072         },
1073         start : function () {
1074             var self = this;
1075             this._super();
1076
1077             // set background and prepare to clean for save
1078             var add_class = function (c){
1079                 if (c) self._class = (self._class || "").replace(new RegExp("[ ]+" + c.replace(" ", "|[ ]+")), '') + ' ' + c;
1080                 return self._class || "";
1081             };
1082             this.$target.on('snippet-style-change snippet-style-preview', function (event, style, np) {
1083                 var $active = self.$target.find(".item.active");
1084                 if (style['snippet-option-id'] === "size") return;
1085                 if (style['snippet-option-id'] === "background") {
1086                     $active.css("background-image", self.$target.css("background-image"));
1087                 }
1088                 if (np.$prev) {
1089                     $active.removeClass(np.$prev.data("value"));
1090                 }
1091                 if (np.$next) {
1092                     $active.addClass(np.$next.data("value"));
1093                     add_class(np.$next.data("value"));
1094                 }
1095             });
1096             this.$target.on('slid', function () { // slide.bs.carousel
1097                 var $active = self.$target.find(".item.active");
1098                 self.$target
1099                     .css("background-image", $active.css("background-image"))
1100                     .removeClass(add_class($active.attr("class")))
1101                     .addClass($active.attr("class"))
1102                     .trigger("snippet-style-reset");
1103
1104                 self.$target.carousel("pause");
1105             });
1106             this.$target.trigger('slid');
1107         },
1108         on_add_slide: function () {
1109             var $clone = this._super();
1110
1111             // choose an other background
1112             var bg = this.$target.data("snippet-option-ids").background;
1113             if (!bg) return $clone;
1114
1115             var $styles = bg.$el.find("li[data-value]:not(.oe_custom_bg)");
1116             var styles_index = $styles.index($styles.filter(".active")[0]);
1117             $styles.removeClass("active");
1118             var $select = $($styles[styles_index >= $styles.length-1 ? 0 : styles_index+1]);
1119             $select.addClass("active");
1120             $clone.css("background-image", $select.data("src") ? "url('"+ $select.data("src") +"')" : "");
1121             $clone.addClass($select.data("value") || "");
1122
1123             return $clone;
1124         },
1125         // rebind event to active carousel on edit mode
1126         rebind_event: function () {
1127             var self = this;
1128             this.$target.find('.carousel-control').off('click').on('click', function () {
1129                 self.$target.carousel( $(this).data('slide')); });
1130             this._super();
1131
1132             /* Fix: backward compatibility saas-3 */
1133             this.$target.find('.item.text_image .container').find('> .carousel-caption > div, > img.carousel-image').attr('contentEditable', 'true');
1134         },
1135     });
1136
1137     website.snippet.options.marginAndResize = website.snippet.Option.extend({
1138         start: function () {
1139             var self = this;
1140             this._super();
1141
1142             var resize_values = this.getSize();
1143             if (resize_values.n) this.$overlay.find(".oe_handle.n").removeClass("readonly");
1144             if (resize_values.s) this.$overlay.find(".oe_handle.s").removeClass("readonly");
1145             if (resize_values.e) this.$overlay.find(".oe_handle.e").removeClass("readonly");
1146             if (resize_values.w) this.$overlay.find(".oe_handle.w").removeClass("readonly");
1147             if (resize_values.size) this.$overlay.find(".oe_handle.size").removeClass("readonly");
1148
1149             this.$overlay.find(".oe_handle:not(.size), .oe_handle.size .size").on('mousedown', function (event){
1150                 event.preventDefault();
1151
1152                 var $handle = $(this);
1153
1154                 var resize_values = self.getSize();
1155                 var compass = false;
1156                 var XY = false;
1157                 if ($handle.hasClass('n')) {
1158                     compass = 'n';
1159                     XY = 'Y';
1160                 }
1161                 else if ($handle.hasClass('s')) {
1162                     compass = 's';
1163                     XY = 'Y';
1164                 }
1165                 else if ($handle.hasClass('e')) {
1166                     compass = 'e';
1167                     XY = 'X';
1168                 }
1169                 else if ($handle.hasClass('w')) {
1170                     compass = 'w';
1171                     XY = 'X';
1172                 }
1173                 else if ($handle.hasClass('size')) {
1174                     compass = 'size';
1175                     XY = 'Y';
1176                 }
1177
1178                 var resize = resize_values[compass];
1179                 if (!resize) return;
1180
1181
1182                 if (compass === 'size') {
1183                     var offset = self.$target.offset().top;
1184                     if (self.$target.css("background").match(/rgba\(0, 0, 0, 0\)/)) {
1185                         self.$target.addClass("resize_editor_busy");
1186                     }
1187                 } else {
1188                     var xy = event['page'+XY];
1189                     var current = resize[2] || 0;
1190                     _.each(resize[0], function (val, key) {
1191                         if (self.$target.hasClass(val)) {
1192                             current = key;
1193                         }
1194                     });
1195                     var begin = current;
1196                     var beginClass = self.$target.attr("class");
1197                     var regClass = new RegExp("\\s*" + resize[0][begin].replace(/[-]*[0-9]+/, '[-]*[0-9]+'), 'g');
1198                 }
1199
1200                 self.BuildingBlock.editor_busy = true;
1201
1202                 var cursor = $handle.css("cursor")+'-important';
1203                 var $body = $(document.body);
1204                 $body.addClass(cursor);
1205
1206                 var body_mousemove = function (event){
1207                     event.preventDefault();
1208                     if (compass === 'size') {
1209                         var dy = event.pageY-offset;
1210                         dy = dy - dy%resize;
1211                         if (dy <= 0) dy = resize;
1212                         self.$target.css("height", dy+"px");
1213                         self.$target.css("overflow", "hidden");
1214                         self.on_resize(compass, null, dy);
1215                         self.BuildingBlock.cover_target(self.$overlay, self.$target);
1216                         return;
1217                     }
1218                     var dd = event['page'+XY] - xy + resize[1][begin];
1219                     var next = current+1 === resize[1].length ? current : (current+1);
1220                     var prev = current ? (current-1) : 0;
1221
1222                     var change = false;
1223                     if (dd > (2*resize[1][next] + resize[1][current])/3) {
1224                         self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
1225                         self.$target.addClass(resize[0][next]);
1226                         current = next;
1227                         change = true;
1228                     }
1229                     if (prev != current && dd < (2*resize[1][prev] + resize[1][current])/3) {
1230                         self.$target.attr("class", (self.$target.attr("class")||'').replace(regClass, ''));
1231                         self.$target.addClass(resize[0][prev]);
1232                         current = prev;
1233                         change = true;
1234                     }
1235
1236                     if (change) {
1237                         self.on_resize(compass, beginClass, current);
1238                         self.BuildingBlock.cover_target(self.$overlay, self.$target);
1239                     }
1240                 };
1241
1242                 var body_mouseup = function(){
1243                     $body.unbind('mousemove', body_mousemove);
1244                     $body.unbind('mouseup', body_mouseup);
1245                     $body.removeClass(cursor);
1246                     self.BuildingBlock.editor_busy = false;
1247                     self.$target.removeClass("resize_editor_busy");
1248                 };
1249                 $body.mousemove(body_mousemove);
1250                 $body.mouseup(body_mouseup);
1251             });
1252             this.$overlay.find(".oe_handle.size .auto_size").on('click', function (event){
1253                 self.$target.css("height", "");
1254                 self.$target.css("overflow", "");
1255                 self.BuildingBlock.cover_target(self.$overlay, self.$target);
1256                 return false;
1257             });
1258         },
1259         getSize: function () {
1260             this.grid = {};
1261             return this.grid;
1262         },
1263
1264         onFocus : function () {
1265             this._super();
1266             this.change_cursor();
1267         },
1268
1269         change_cursor : function () {
1270             var _class = this.$target.attr("class") || "";
1271
1272             var col = _class.match(/col-md-([0-9-]+)/i);
1273             col = col ? +col[1] : 0;
1274
1275             var offset = _class.match(/col-md-offset-([0-9-]+)/i);
1276             offset = offset ? +offset[1] : 0;
1277
1278             var overlay_class = this.$overlay.attr("class").replace(/(^|\s+)block-[^\s]*/gi, '');
1279             if (col+offset >= 12) overlay_class+= " block-e-right";
1280             if (col === 1) overlay_class+= " block-w-right block-e-left";
1281             if (offset === 0) overlay_class+= " block-w-left";
1282
1283             var mb = _class.match(/mb([0-9-]+)/i);
1284             mb = mb ? +mb[1] : 0;
1285             if (mb >= 128) overlay_class+= " block-s-bottom";
1286             else if (!mb) overlay_class+= " block-s-top";
1287
1288             var mt = _class.match(/mt([0-9-]+)/i);
1289             mt = mt ? +mt[1] : 0;
1290             if (mt >= 128) overlay_class+= " block-n-top";
1291             else if (!mt) overlay_class+= " block-n-bottom";
1292
1293             this.$overlay.attr("class", overlay_class);
1294         },
1295         
1296         /* on_resize
1297         *  called when the box is resizing and the class change, before the cover_target
1298         *  @compass: resize direction : 'n', 's', 'e', 'w'
1299         *  @beginClass: attributes class at the begin
1300         *  @current: curent increment in this.grid
1301         */
1302         on_resize: function (compass, beginClass, current) {
1303             this.change_cursor();
1304         }
1305     });
1306     website.snippet.options["margin-y"] = website.snippet.options.marginAndResize.extend({
1307         getSize: function () {
1308             this.grid = this._super();
1309             var grid = [0,4,8,16,32,48,64,92,128];
1310             this.grid = {
1311                 // list of class (Array), grid (Array), default value (INT)
1312                 n: [_.map(grid, function (v) {return 'mt'+v;}), grid],
1313                 s: [_.map(grid, function (v) {return 'mb'+v;}), grid],
1314                 // INT if the user can resize the snippet (resizing per INT px)
1315                 size: null
1316             };
1317             return this.grid;
1318         },
1319     });
1320     website.snippet.options["margin-x"] = website.snippet.options.marginAndResize.extend({
1321         getSize: function () {
1322             this.grid = this._super();
1323             var width = this.$target.parents(".row:first").first().outerWidth();
1324
1325             var grid = [1,2,3,4,5,6,7,8,9,10,11,12];
1326             this.grid.e = [_.map(grid, function (v) {return 'col-md-'+v;}), _.map(grid, function (v) {return width/12*v;})];
1327
1328             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];
1329             this.grid.w = [_.map(grid, function (v) {return 'col-md-offset-'+v;}), _.map(grid, function (v) {return width/12*v;}), 12];
1330
1331             return this.grid;
1332         },
1333         _drag_and_drop_after_insert_dropzone: function(){
1334             var self = this;
1335             var $zones = $(".row:has(> .oe_drop_zone)").each(function () {
1336                 var $row = $(this);
1337                 var width = $row.innerWidth();
1338                 var pos = 0;
1339                 while (width > pos + self.size.width) {
1340                     var $last = $row.find("> .oe_drop_zone:last");
1341                     $last.each(function () {
1342                         pos = $(this).position().left;
1343                     });
1344                     if (width > pos + self.size.width) {
1345                         $row.append("<div class='col-md-1 oe_drop_to_remove'/>");
1346                         var $add_drop = $last.clone();
1347                         $row.append($add_drop);
1348                         self._drag_and_drop_active_drop_zone($add_drop);
1349                     }
1350                 }
1351             });
1352         },
1353         _drag_and_drop_start: function () {
1354             this._super();
1355             this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, ''));
1356         },
1357         _drag_and_drop_stop: function () {
1358             this.$target.addClass("col-md-offset-" + this.$target.prevAll(".oe_drop_to_remove").length);
1359             this._super();
1360         },
1361         hide_remove_button: function() {
1362             this.$overlay.find('.oe_snippet_remove').toggleClass("hidden", !this.$target.siblings().length);
1363         },
1364         onFocus : function () {
1365             this._super();
1366             this.hide_remove_button();
1367         },
1368         on_clone: function ($clone) {
1369             var _class = $clone.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-)([0-9-]+)/g, '');
1370             $clone.attr("class", _class);
1371             this.hide_remove_button();
1372             return false;
1373         },
1374         on_remove: function () {
1375             if (!this.$target.siblings().length) {
1376                 var $parent = this.$target.parents(".row:first");
1377                 if($parent.find("[class*='col-md']").length > 1) {
1378                     return false;
1379                 } else {
1380                     if (!$parent.data("snippet-editor")) {
1381                         this.BuildingBlock.create_overlay($parent);
1382                     }
1383                     $parent.data("snippet-editor").on_remove();
1384                 }
1385             }
1386             this._super();
1387             this.hide_remove_button();
1388             return false;
1389         },
1390         on_resize: function (compass, beginClass, current) {
1391             if (compass === 'w') {
1392                 // don't change the right border position when we change the offset (replace col size)
1393                 var beginCol = Number(beginClass.match(/col-md-([0-9]+)|$/)[1] || 0);
1394                 var beginOffset = Number(beginClass.match(/col-md-offset-([0-9-]+)|$/)[1] || beginClass.match(/col-lg-offset-([0-9-]+)|$/)[1] || 0);
1395                 var offset = Number(this.grid.w[0][current].match(/col-md-offset-([0-9-]+)|$/)[1] || 0);
1396                 if (offset < 0) {
1397                     offset = 0;
1398                 }
1399                 var colSize = beginCol - (offset - beginOffset);
1400                 if (colSize <= 0) {
1401                     colSize = 1;
1402                     offset = beginOffset + beginCol - 1;
1403                 }
1404                 this.$target.attr("class",this.$target.attr("class").replace(/\s*(col-lg-offset-|col-md-offset-|col-md-)([0-9-]+)/g, ''));
1405
1406                 this.$target.addClass('col-md-' + (colSize > 12 ? 12 : colSize));
1407                 if (offset > 0) {
1408                     this.$target.addClass('col-md-offset-' + offset);
1409                 }
1410             }
1411             this._super(compass, beginClass, current);
1412         },
1413     });
1414
1415     website.snippet.options["resize"] = website.snippet.options.marginAndResize.extend({
1416         getSize: function () {
1417             this.grid = this._super();
1418             this.grid.size = 8;
1419             return this.grid;
1420         },
1421     });
1422
1423     website.snippet.options.parallax = website.snippet.Option.extend({
1424         getSize: function () {
1425             this.grid = this._super();
1426             this.grid.size = 8;
1427             return this.grid;
1428         },
1429         on_resize: function (compass, beginClass, current) {
1430             this.$target.data("snippet-view").set_values();
1431         },
1432         start : function () {
1433             var self = this;
1434             this._super();
1435             if (!self.$target.data("snippet-view")) {
1436                 this.$target.data("snippet-view", new website.snippet.animationRegistry.parallax(this.$target));
1437             }
1438             this.scroll();
1439             this.$target.on('snippet-style-change snippet-style-preview', function () {
1440                 self.$target.data("snippet-view").set_values();
1441             });
1442             this.$target.attr('contentEditable', 'false');
1443
1444             this.$target.find('> div > .oe_structure').attr('contentEditable', 'true'); // saas-3 retro-compatibility
1445
1446             this.$target.find('> div > div:not(.oe_structure) > .oe_structure').attr('contentEditable', 'true');
1447         },
1448         scroll: function () {
1449             var self = this;
1450             var $ul = this.$el.find('ul[name="parallax-scroll"]');
1451             var $li = $ul.find("li");
1452             var speed = this.$target.data('scroll-background-ratio') || 0.6 ;
1453             $ul.find('[data-value="' + speed + '"]').addClass('active');
1454             $li.on('click', function (event) {
1455                 $li.removeClass("active");
1456                 $(this).addClass("active");
1457                 var speed =  $(this).data('value');
1458                 self.$target.attr('data-scroll-background-ratio', speed);
1459                 self.$target.data("snippet-view").set_values();
1460                 return false;
1461             });
1462             this.$target.data("snippet-view").set_values();
1463         },
1464         clean_for_save: function () {
1465             this._super();
1466             this.$target.find(".parallax")
1467                 .css("background-position", '')
1468                 .removeAttr("data-scroll-background-offset");
1469         }
1470     });
1471
1472     website.snippet.options.transform = website.snippet.Option.extend({
1473         start: function () {
1474             var self = this;
1475             this._super();
1476
1477             this.$el.find(".clear-style").click(function (event) {
1478                 self.$target.removeClass("fa-spin").attr("style", "");
1479                 self.resetTransfo();
1480             });
1481
1482             this.$el.find(".style").click(function (event) {
1483                 var settings = self.$target.data("transfo").settings;
1484                 self.$target.transfo({ hide: (settings.hide = !settings.hide) });
1485             });
1486
1487             this.$overlay.find('.oe_snippet_clone, .oe_handles').addClass('hidden');
1488
1489             this.$overlay.find('[data-toggle="dropdown"]')
1490                 .on("mousedown", function () {
1491                     self.$target.transfo("hide");
1492                 });
1493         },
1494         resetTransfo: function () {
1495             var self = this;
1496             this.$target.transfo("destroy");
1497             this.$target.transfo({
1498                 hide: true,
1499                 callback: function () {
1500                     var pos = $(this).data("transfo").$center.offset();
1501                     self.$overlay.css({
1502                         'top': pos.top,
1503                         'left': pos.left,
1504                         'position': 'absolute',
1505                     });
1506                     self.$overlay.find(".oe_overlay_options").attr("style", "width:0; left:0!important; top:0;");
1507                     self.$overlay.find(".oe_overlay_options > .btn-group").attr("style", "width:160px; left:-80px;");
1508                 }});
1509             this.$target.data('transfo').$markup
1510                 .on("mouseover", function () {
1511                     self.$target.trigger("mouseover");
1512                 })
1513                 .mouseover();
1514         },
1515         onFocus : function () {
1516             this.resetTransfo();
1517         },
1518         onBlur : function () {
1519             this.$target.transfo("hide");
1520         },
1521     });
1522
1523     website.snippet.options.media = website.snippet.Option.extend({
1524         start: function () {
1525             var self = this;
1526             this._super();
1527
1528             website.snippet.start_animation(true, this.$target);
1529
1530             $(document.body).on("media-saved", self, function (event, prev , item) {
1531                 self.editor.onBlur();
1532                 self.BuildingBlock.make_active(false);
1533                 if (self.$target.parent().data("oe-field") !== "image") {
1534                     self.BuildingBlock.make_active($(item));
1535                 }
1536             });
1537
1538             this.$el.find(".edition").click(function (event) {
1539                 event.preventDefault();
1540                 event.stopPropagation();
1541                 self.element = new CKEDITOR.dom.element(self.$target[0]);
1542                 new website.editor.MediaDialog(self, self.element).appendTo(document.body);
1543             });
1544         },
1545         onFocus : function () {
1546             var self = this;
1547             if (this.$target.parent().data("oe-field") === "image") {
1548                 this.$overlay.addClass("hidden");
1549                 self.element = new CKEDITOR.dom.element(self.$target[0]);
1550                 new website.editor.MediaDialog(self, self.element).appendTo(document.body);
1551                 self.BuildingBlock.make_active(false);
1552             }
1553             setTimeout(function () {
1554                 self.$target.find(".css_editable_mode_display").removeAttr("_moz_abspos");
1555             },0);
1556         },
1557     });
1558
1559
1560     website.snippet.Editor = openerp.Class.extend({
1561         init: function (BuildingBlock, dom) {
1562             this.BuildingBlock = BuildingBlock;
1563             this.$target = $(dom);
1564             this.$overlay = this.$target.data('overlay');
1565             this.snippet_id = this.$target.data("snippet-id");
1566             this._readXMLData();
1567             this.load_style_options();
1568             this.get_parent_block();
1569             this.start();
1570         },
1571
1572         /*
1573         *  _readXMLData
1574         *  Read data XML and set value into:
1575         *  this.$el :
1576         *       all xml data
1577         *  this.$overlay :
1578         *       Dom hover the $target who content options
1579         */
1580         _readXMLData: function() {
1581             var self = this;
1582             if(this && this.BuildingBlock && this.BuildingBlock.$snippets) {
1583                 this.$el = this.BuildingBlock.$snippets.filter(function () { return $(this).data("snippet-id") == self.snippet_id; }).clone();
1584             }
1585             var $options = this.$overlay.find(".oe_overlay_options");
1586             if ($options.find(".oe_options ul li").length) {
1587                 $options.find(".oe_options").removeClass("hidden");
1588             }
1589         },
1590
1591
1592         // activate drag and drop for the snippets in the snippet toolbar
1593         _drag_and_drop: function(){
1594             var self = this;
1595             this.dropped = false;
1596             this.$overlay.draggable({
1597                 greedy: true,
1598                 appendTo: 'body',
1599                 cursor: "move",
1600                 handle: ".oe_snippet_move",
1601                 cursorAt: {
1602                     left: 18,
1603                     top: 14
1604                 },
1605                 helper: function() {
1606                     var $clone = $(this).clone().css({width: "24px", height: "24px", border: 0});
1607                     $clone.find(".oe_overlay_options >:not(:contains(.oe_snippet_move)), .oe_handle").remove();
1608                     $clone.find(":not(.glyphicon)").css({position: 'absolute', top: 0, left: 0});
1609                     $clone.appendTo("body").removeClass("hidden");
1610                     return $clone;
1611                 },
1612                 start: _.bind(self._drag_and_drop_start, self),
1613                 stop: _.bind(self._drag_and_drop_stop, self)
1614             });
1615         },
1616         _drag_and_drop_after_insert_dropzone: function (){},
1617         _drag_and_drop_active_drop_zone: function ($zones){
1618             var self = this;
1619             $zones.droppable({
1620                 over:   function(){
1621                     $(".oe_drop_zone.hide").removeClass("hide");
1622                     $(this).addClass("hide").first().after(self.$target);
1623                     self.dropped = true;
1624                 },
1625                 out:    function(){
1626                     $(this).removeClass("hide");
1627                     self.$target.detach();
1628                     self.dropped = false;
1629                 },
1630             });
1631         },
1632         _drag_and_drop_start: function (){
1633             var self = this;
1634             self.BuildingBlock.hide();
1635             self.BuildingBlock.editor_busy = true;
1636             self.size = {
1637                 width: self.$target.width(),
1638                 height: self.$target.height()
1639             };
1640             self.$target.after("<div class='oe_drop_clone' style='display: none;'/>");
1641             self.$target.detach();
1642             self.$overlay.addClass("hidden");
1643
1644             self.BuildingBlock.activate_insertion_zones({
1645                 siblings: self.selector_siblings,
1646                 children: self.selector_children,
1647                 vertical_children: self.selector_vertical_children,
1648             });
1649
1650             $("body").addClass('move-important');
1651
1652             self._drag_and_drop_after_insert_dropzone();
1653             self._drag_and_drop_active_drop_zone($('.oe_drop_zone'));
1654         },
1655         _drag_and_drop_stop: function (){
1656             var self = this;
1657             if (!self.dropped) {
1658                 $(".oe_drop_clone").after(self.$target);
1659             }
1660             self.$overlay.removeClass("hidden");
1661             $("body").removeClass('move-important');
1662             $('.oe_drop_zone').droppable('destroy').remove();
1663             $(".oe_drop_clone, .oe_drop_to_remove").remove();
1664             self.BuildingBlock.editor_busy = false;
1665             self.get_parent_block();
1666             setTimeout(function () {self.BuildingBlock.create_overlay(self.$target);},0);
1667         },
1668
1669         load_style_options: function () {
1670             var self = this;
1671             var $styles = this.$overlay.find('.oe_options');
1672             var $ul = $styles.find('ul:first');
1673             this.styles = {};
1674             this.selector_siblings = [];
1675             this.selector_children = [];
1676             this.selector_vertical_children = [];
1677             _.each(website.snippet.templateOptions, function (val) {
1678                 if (!self.$target.is(val.selector)) {
1679                     return;
1680                 }
1681                 if (val['selector-siblings']) self.selector_siblings.push(val['selector-siblings']);
1682                 if (val['selector-children']) self.selector_children.push(val['selector-children']);
1683                 if (val['selector-vertical-children']) self.selector_vertical_children.push(val['selector-vertical-children']);
1684
1685                 var style = val['snippet-option-id'];
1686                 var Editor = website.snippet.options[style] || website.snippet.Option;
1687                 var editor = self.styles[style] = new Editor(self.BuildingBlock, self, self.$target, style);
1688                 $ul.append(editor.$el.addClass("snippet-style-" + style));
1689             });
1690             this.selector_siblings = this.selector_siblings.join(",");
1691             if (this.selector_siblings === "")
1692                 this.selector_siblings = false;
1693             this.selector_children = this.selector_children.join(",");
1694             if (this.selector_children === "")
1695                 this.selector_children = false;
1696             this.selector_vertical_children = this.selector_vertical_children.join(",");
1697             if (this.selector_vertical_children === "")
1698                 this.selector_vertical_children = false;
1699
1700             if (!this.selector_siblings && !this.selector_children && !this.selector_vertical_children) {
1701                 this.$overlay.find(".oe_snippet_move").addClass('hidden');
1702             }
1703
1704
1705             if ($ul.find("li").length) {
1706                 $styles.removeClass("hidden");
1707             }
1708             this.$overlay.find('[data-toggle="dropdown"]').dropdown();
1709         },
1710
1711         get_parent_block: function () {
1712             var self = this;
1713             var $button = this.$overlay.find('.oe_snippet_parent');
1714             var $parent = this.$target.parents(website.snippet.globalSelector).first();
1715             if ($parent.length) {
1716                 $button.removeClass("hidden");
1717                 $button.off("click").on('click', function (event) {
1718                     event.preventDefault();
1719                     setTimeout(function () {
1720                         self.BuildingBlock.make_active($parent);
1721                     }, 0);
1722                 });
1723             } else {
1724                 $button.addClass("hidden");
1725             }
1726         },
1727
1728         /*
1729         *  start
1730         *  This method is called after init and _readXMLData
1731         */
1732         start: function () {
1733             var self = this;
1734             this.$overlay.on('click', '.oe_snippet_clone', _.bind(this.on_clone, this));
1735             this.$overlay.on('click', '.oe_snippet_remove', _.bind(this.on_remove, this));
1736             this._drag_and_drop();
1737         },
1738
1739         on_clone: function () {
1740             var $clone = this.$target.clone(false);
1741             this.$target.after($clone);
1742             for (var i in this.styles){
1743                 this.styles[i].on_clone($clone);
1744             }
1745             return false;
1746         },
1747
1748         on_remove: function () {
1749             this.onBlur();
1750             var index = _.indexOf(this.BuildingBlock.snippets, this.$target.get(0));
1751             for (var i in this.styles){
1752                 this.styles[i].on_remove();
1753             }
1754             delete this.BuildingBlock.snippets[index];
1755             this.$target.remove();
1756             this.$overlay.remove();
1757             return false;
1758         },
1759
1760         /*
1761         *  drop_and_build_snippet
1762         *  This method is called just after that a thumbnail is drag and dropped into a drop zone
1763         *  (after the insertion of this.$body, if this.$body exists)
1764         */
1765         drop_and_build_snippet: function () {
1766             for (var i in this.styles){
1767                 this.styles[i].drop_and_build_snippet();
1768             }
1769         },
1770
1771         /* onFocus
1772         *  This method is called when the user click inside the snippet in the dom
1773         */
1774         onFocus : function () {
1775             this.$overlay.addClass('oe_active');
1776             for (var i in this.styles){
1777                 this.styles[i].onFocus();
1778             }
1779         },
1780
1781         /* onFocus
1782         *  This method is called when the user click outside the snippet in the dom, after a focus
1783         */
1784         onBlur : function () {
1785             for (var i in this.styles){
1786                 this.styles[i].onBlur();
1787             }
1788             this.$overlay.removeClass('oe_active');
1789         },
1790     });
1791
1792 })();