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