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