wrong domain in action_reception_picking_move in addons/stock/stock_view.xml
[odoo/odoo.git] / addons / website / static / src / js / website.editor.js
1 (function () {
2     'use strict';
3
4     var website = openerp.website;
5     var _t = openerp._t;
6
7     website.add_template_file('/website/static/src/xml/website.editor.xml');
8     website.dom_ready.done(function () {
9         var is_smartphone = $(document.body)[0].clientWidth < 767;
10
11         if (!is_smartphone) {
12             website.ready().then(website.init_editor);
13         } else {
14             // remove padding of fake editor bar
15             document.body.style.padding = 0;
16         }
17
18         $(document).on('click', 'a.js_link2post', function (ev) {
19             ev.preventDefault();
20             website.form(this.pathname, 'POST');
21         });
22
23         $(document).on('click', '.cke_editable label', function (ev) {
24             ev.preventDefault();
25         });
26
27         $(document).on('submit', '.cke_editable form', function (ev) {
28             // Disable form submition in editable mode
29             ev.preventDefault();
30         });
31
32         $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
33             // Prevent dropdown closing when a contenteditable children is focused
34             if (ev.originalEvent
35                     && $(ev.target).has(ev.originalEvent.target).length
36                     && $(ev.originalEvent.target).is('[contenteditable]')) {
37                 ev.preventDefault();
38             }
39         });
40     });
41
42     /**
43      * An editing host is an HTML element with @contenteditable=true, or the
44      * child of a document in designMode=on (but that one's not supported)
45      *
46      * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#editing-host
47      */
48     function is_editing_host(element) {
49         return element.getAttribute('contentEditable') === 'true';
50     }
51     /**
52      * Checks that both the element's content *and the element itself* are
53      * editable: an editing host is considered non-editable because its content
54      * is editable but its attributes should not be considered editable
55      */
56     function is_editable_node(element) {
57         return !(element.data('oe-model') === 'ir.ui.view'
58               || element.data('cke-realelement')
59               || (is_editing_host(element) && element.getAttribute('attributeEditable') !== 'true')
60               || element.isReadOnly());
61     }
62
63     function link_dialog(editor) {
64         return new website.editor.RTELinkDialog(editor).appendTo(document.body);
65     }
66     function image_dialog(editor, image) {
67         return new website.editor.MediaDialog(editor, image).appendTo(document.body);
68     }
69
70     // only enable editors manually
71     CKEDITOR.disableAutoInline = true;
72     // EDIT ALL THE THINGS
73     CKEDITOR.dtd.$editable = _.omit(
74         $.extend({}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline),
75         // well maybe not *all* the things
76         'ul', 'ol', 'li', 'table', 'tr', 'th', 'td');
77     // Disable removal of empty elements on CKEDITOR activation. Empty
78     // elements are used for e.g. support of FontAwesome icons
79     CKEDITOR.dtd.$removeEmpty = {};
80
81     website.init_editor = function () {
82         CKEDITOR.plugins.add('customdialogs', {
83             // requires: 'link,image',
84             init: function (editor) {
85                 editor.on('doubleclick', function (evt) {
86                     var element = evt.data.element;
87                     if ((element.is('img') || element.$.className.indexOf(' fa-') != -1) && is_editable_node(element)) {
88                         image_dialog(editor, element);
89                         return;
90                     }
91                     var parent = new CKEDITOR.dom.element(element.$.parentNode);
92                     if (parent.$.className.indexOf('media_iframe_video') != -1 && is_editable_node(parent)) {
93                         image_dialog(editor, parent);
94                         return;
95                     }
96
97                     element = get_selected_link(editor) || evt.data.element;
98                     if (!(element.is('a') && is_editable_node(element))) {
99                         return;
100                     }
101
102                     editor.getSelection().selectElement(element);
103                     link_dialog(editor);
104                 }, null, null, 500);
105
106                 //noinspection JSValidateTypes
107                 editor.addCommand('link', {
108                     exec: function (editor) {
109                         link_dialog(editor);
110                         return true;
111                     },
112                     canUndo: false,
113                     editorFocus: true,
114                     context: 'a',
115                 });
116                 //noinspection JSValidateTypes
117                 editor.addCommand('cimage', {
118                     exec: function (editor) {
119                         image_dialog(editor);
120                         return true;
121                     },
122                     canUndo: false,
123                     editorFocus: true,
124                     context: 'img',
125                 });
126
127                 editor.ui.addButton('Link', {
128                     label: 'Link',
129                     command: 'link',
130                     toolbar: 'links,10',
131                 });
132                 editor.ui.addButton('Image', {
133                     label: 'Image',
134                     command: 'cimage',
135                     toolbar: 'insert,10',
136                 });
137
138                 editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link');
139             }
140         });
141         CKEDITOR.plugins.add( 'tablebutton', {
142             requires: 'panelbutton,floatpanel',
143             init: function( editor ) {
144                 var label = "Table";
145
146                 editor.ui.add('TableButton', CKEDITOR.UI_PANELBUTTON, {
147                     label: label,
148                     title: label,
149                     // use existing 'table' icon
150                     icon: 'table',
151                     modes: { wysiwyg: true },
152                     editorFocus: true,
153                     // panel opens in iframe, @css is CSS file <link>-ed within
154                     // frame document, @attributes are set on iframe itself.
155                     panel: {
156                         css: '/website/static/src/css/editor.css',
157                         attributes: { 'role': 'listbox', 'aria-label': label, },
158                     },
159
160                     onBlock: function (panel, block) {
161                         block.autoSize = true;
162                         block.element.setHtml(openerp.qweb.render('website.editor.table.panel', {
163                             rows: 5,
164                             cols: 5,
165                         }));
166
167                         var $table = $(block.element.$).on('mouseenter', 'td', function (e) {
168                             var $e = $(e.target);
169                             var y = $e.index() + 1;
170                             var x = $e.closest('tr').index() + 1;
171
172                             $table
173                                 .find('td').removeClass('selected').end()
174                                 .find('tr:lt(' + String(x) + ')')
175                                 .children().filter(function () { return $(this).index() < y; })
176                                 .addClass('selected');
177                         }).on('click', 'td', function (e) {
178                             var $e = $(e.target);
179
180                             //noinspection JSPotentiallyInvalidConstructorUsage
181                             var table = new CKEDITOR.dom.element(
182                                 $(openerp.qweb.render('website.editor.table', {
183                                     rows: $e.closest('tr').index() + 1,
184                                     cols: $e.index() + 1,
185                                 }))[0]);
186
187                             editor.insertElement(table);
188                             setTimeout(function () {
189                                 //noinspection JSPotentiallyInvalidConstructorUsage
190                                 var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]);
191                                 var range = editor.createRange();
192                                 range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START);
193                                 range.select();
194                             }, 0);
195                         });
196
197                         block.element.getDocument().getBody().setStyle('overflow', 'hidden');
198                         CKEDITOR.ui.fire('ready', this);
199                     },
200                 });
201             }
202         });
203
204         CKEDITOR.plugins.add('linkstyle', {
205             requires: 'panelbutton,floatpanel',
206             init: function (editor) {
207                 var label = "Link Style";
208
209                 editor.ui.add('LinkStyle', CKEDITOR.UI_PANELBUTTON, {
210                     label: label,
211                     title: label,
212                     icon: '/website/static/src/img/bglink.png',
213                     modes: { wysiwyg: true },
214                     editorFocus: true,
215                     context: 'a',
216                     panel: {
217                         css: '/web/static/lib/bootstrap/css/bootstrap.css',
218                         attributes: { 'role': 'listbox', 'aria-label': label },
219                     },
220
221                     types: {
222                         'btn-default': _t("Basic"),
223                         'btn-primary': _t("Primary"),
224                         'btn-success': _t("Success"),
225                         'btn-info': _t("Info"),
226                         'btn-warning': _t("Warning"),
227                         'btn-danger': _t("Danger"),
228                     },
229                     sizes: {
230                         'btn-xs': _t("Extra Small"),
231                         'btn-sm': _t("Small"),
232                         '': _t("Default"),
233                         'btn-lg': _t("Large")
234                     },
235
236                     onRender: function () {
237                         var self = this;
238                         editor.on('selectionChange', function (e) {
239                             var path = e.data.path, el;
240
241                             if (!(e = path.contains('a')) || e.isReadOnly()) {
242                                 self.disable();
243                                 return;
244                             }
245
246                             self.enable();
247                         });
248                         // no hook where button is available, so wait
249                         // "some time" after render.
250                         setTimeout(function () {
251                             self.disable();
252                         }, 0)
253                     },
254                     enable: function () {
255                         this.setState(CKEDITOR.TRISTATE_OFF);
256                     },
257                     disable: function () {
258                         this.setState(CKEDITOR.TRISTATE_DISABLED);
259                     },
260
261                     onOpen: function () {
262                         var link = get_selected_link(editor);
263                         var id = this._.id;
264                         var block = this._.panel._.panel._.blocks[id];
265                         var $root = $(block.element.$);
266                         $root.find('button').removeClass('active').removeProp('disabled');
267
268                         // enable buttons matching link state
269                         for (var type in this.types) {
270                             if (!this.types.hasOwnProperty(type)) { continue; }
271                             if (!link.hasClass(type)) { continue; }
272
273                             $root.find('button[data-type=types].' + type)
274                                  .addClass('active');
275                         }
276                         var found;
277                         for (var size in this.sizes) {
278                             if (!this.sizes.hasOwnProperty(size)) { continue; }
279                             if (!size || !link.hasClass(size)) { continue; }
280                             found = true;
281                             $root.find('button[data-type=sizes].' + size)
282                                  .addClass('active');
283                         }
284                         if (!found && link.hasClass('btn')) {
285                             $root.find('button[data-type="sizes"][data-set-class=""]')
286                                  .addClass('active');
287                         }
288                     },
289
290                     onBlock: function (panel, block) {
291                         var self = this;
292                         block.autoSize = true;
293
294                         var html = ['<div style="padding: 5px">'];
295                         html.push('<div style="white-space: nowrap">');
296                         _(this.types).each(function (label, key) {
297                             html.push(_.str.sprintf(
298                                 '<button type="button" class="btn %s" ' +
299                                         'data-type="types" data-set-class="%s">%s</button>',
300                                 key, key, label));
301                         });
302                         html.push('</div>');
303                         html.push('<div style="white-space: nowrap; margin: 5px 0; text-align: center">');
304                         _(this.sizes).each(function (label, key) {
305                             html.push(_.str.sprintf(
306                                 '<button type="button" class="btn btn-default %s" ' +
307                                         'data-type="sizes" data-set-class="%s">%s</button>',
308                                 key, key, label));
309                         });
310                         html.push('</div>');
311                         html.push('<button type="button" class="btn btn-link btn-block" ' +
312                                           'data-type="reset">Reset</button>');
313                         html.push('</div>');
314
315                         block.element.setHtml(html.join(' '));
316                         var $panel = $(block.element.$);
317                         $panel.on('click', 'button', function () {
318                             self.clicked(this);
319                         });
320                     },
321                     clicked: function (button) {
322                         editor.focus();
323                         editor.fire('saveSnapshot');
324
325                         var $button = $(button),
326                               $link = $(get_selected_link(editor).$);
327                         if (!$link.hasClass('btn')) {
328                             $link.addClass('btn btn-default');
329                         }
330                         switch($button.data('type')) {
331                         case 'reset':
332                             $link.removeClass('btn')
333                                  .removeClass(_.keys(this.types).join(' '))
334                                  .removeClass(_.keys(this.sizes).join(' '));
335                             break;
336                         case 'types':
337                             $link.removeClass(_.keys(this.types).join(' '))
338                                  .addClass($button.data('set-class'));
339                             break;
340                         case 'sizes':
341                             $link.removeClass(_.keys(this.sizes).join(' '))
342                                  .addClass($button.data('set-class'));
343                         }
344                         this._.panel.hide();
345
346                         editor.fire('saveSnapshot');
347                     },
348
349                 });
350             }
351         });
352
353         CKEDITOR.plugins.add('oeref', {
354             requires: 'widget',
355
356             init: function (editor) {
357                 var specials = {
358                     // Can't find the correct ACL rule to only allow img tags
359                     image: { content: '*' },
360                     html: { text: '*' },
361                     monetary: {
362                         text: {
363                             selector: 'span.oe_currency_value',
364                             allowedContent: { }
365                         }
366                     }
367                 };
368                 _(specials).each(function (editable, type) {
369                     editor.widgets.add(type, {
370                         draggable: false,
371                         editables: editable,
372                         upcast: function (el) {
373                             return  el.attributes['data-oe-type'] === type;
374
375                         }
376                     });
377                 });
378                 editor.widgets.add('oeref', {
379                     draggable: false,
380                     editables: {
381                         text: {
382                             selector: '*',
383                             allowedContent: { }
384                         },
385                     },
386                     upcast: function (el) {
387                         var type = el.attributes['data-oe-type'];
388                         if (!type || (type in specials)) {
389                             return false;
390                         }
391                         if (el.attributes['data-oe-original']) {
392                             while (el.children.length) {
393                                 el.children[0].remove();
394                             }
395                             el.add(new CKEDITOR.htmlParser.text(
396                                 el.attributes['data-oe-original']
397                             ));
398                         }
399                         return true;
400                     }
401                 });
402
403                 editor.widgets.add('icons', {
404                     draggable: false,
405
406                     init: function () {
407                         this.on('edit', function () {
408                             new website.editor.MediaDialog(editor, this.element)
409                                 .appendTo(document.body);
410                         });
411                     },
412                     upcast: function (el) {
413                         return el.hasClass('fa')
414                             // ignore ir.ui.view (other data-oe-model should
415                             // already have been matched by oeref and
416                             // monetary?
417                             && !el.attributes['data-oe-model'];
418                     }
419                 });
420             }
421         });
422
423         var editor = new website.EditorBar();
424         var $body = $(document.body);
425         editor.prependTo($body).then(function () {
426             if (location.search.indexOf("enable_editor") >= 0) {
427                 editor.edit();
428             }
429         });
430         website.editor_bar = editor;
431     };
432
433     /* ----- TOP EDITOR BAR FOR ADMIN ---- */
434     website.EditorBar = openerp.Widget.extend({
435         template: 'website.editorbar',
436         events: {
437             'click button[data-action=edit]': 'edit',
438             'click button[data-action=save]': 'save',
439             'click a[data-action=cancel]': 'cancel',
440         },
441         container: 'body',
442         customize_setup: function() {
443             var self = this;
444             var view_name = $(document.documentElement).data('view-xmlid');
445             if (!view_name) {
446                 this.$('#customize-menu-button').addClass("hidden");
447             }
448             var menu = $('#customize-menu');
449             this.$('#customize-menu-button').click(function(event) {
450                 menu.empty();
451                 openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
452                     function(result) {
453                         _.each(result, function (item) {
454                             if (item.xml_id === "website.debugger" && !window.location.search.match(/[&?]debug(&|$)/)) return;
455                             if (item.header) {
456                                 menu.append('<li class="dropdown-header">' + item.name + '</li>');
457                             } else {
458                                 menu.append(_.str.sprintf('<li role="presentation"><a href="#" data-view-id="%s" role="menuitem"><strong class="fa fa%s-square-o"></strong> %s</a></li>',
459                                     item.id, item.active ? '-check' : '', item.name));
460                             }
461                         });
462                         // Adding Static Menus
463                         menu.append('<li class="divider"></li>');
464                         menu.append('<li><a data-action="ace" href="#">HTML Editor</a></li>');
465                         menu.append('<li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
466                         menu.append('<li><a href="/web#return_label=Website&action=website.action_module_website">Install Apps</a></li>');
467                         self.trigger('rte:customize_menu_ready');
468                     }
469                 );
470             });
471             menu.on('click', 'a[data-action!=ace]', function (event) {
472                 var view_id = $(event.currentTarget).data('view-id');
473                 return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
474                     model: 'ir.ui.view',
475                     method: 'toggle',
476                     args: [],
477                     kwargs: {
478                         ids: [parseInt(view_id, 10)],
479                         context: website.get_context()
480                     }
481                 }).then( function() {
482                     window.location.reload();
483                 });
484             });
485         },
486         start: function() {
487             // remove placeholder editor bar
488             var fakebar = document.getElementById('website-top-navbar-placeholder');
489             if (fakebar) {
490                 fakebar.parentNode.removeChild(fakebar);
491             }
492
493             var self = this;
494             this.saving_mutex = new openerp.Mutex();
495
496             this.$('#website-top-edit').hide();
497             this.$('#website-top-view').show();
498
499             $('.dropdown-toggle').dropdown();
500             this.customize_setup();
501
502             this.$buttons = {
503                 edit: this.$('button[data-action=edit]'),
504                 save: this.$('button[data-action=save]'),
505                 cancel: this.$('button[data-action=cancel]'),
506             };
507
508             this.rte = new website.RTE(this);
509             this.rte.on('change', this, this.proxy('rte_changed'));
510             this.rte.on('rte:ready', this, function () {
511                 self.setup_hover_buttons();
512                 self.trigger('rte:ready');
513                 self.check_height();
514             });
515
516             $(window).on('resize', _.debounce(this.check_height.bind(this), 50));
517             this.check_height();
518
519             if (website.is_editable_button) {
520                 this.$("button[data-action=edit]").removeClass("hidden");
521             }
522
523             return $.when(
524                 this._super.apply(this, arguments),
525                 this.rte.appendTo(this.$('#website-top-edit .nav.pull-right'))
526             ).then(function () {
527                 self.check_height();
528             });
529         },
530         check_height: function () {
531             var editor_height = this.$el.outerHeight();
532             if (this.get('height') != editor_height) {
533                 $(document.body).css('padding-top', editor_height);
534                 this.set('height', editor_height);
535             }
536         },
537         edit: function () {
538             this.$buttons.edit.prop('disabled', true);
539             this.$('#website-top-view').hide();
540             this.$('#website-top-edit').show();
541             $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden");
542
543             this.rte.start_edition().then(this.check_height.bind(this));
544             this.trigger('rte:called');
545         },
546         rte_changed: function () {
547             this.$buttons.save.prop('disabled', false);
548         },
549         save: function () {
550             var self = this;
551
552             observer.disconnect();
553             var editor = this.rte.editor;
554             var root = editor.element && editor.element.$;
555             editor.destroy();
556             // FIXME: select editables then filter by dirty?
557             var defs = this.rte.fetch_editables(root)
558                 .filter('.oe_dirty')
559                 .removeAttr('contentEditable')
560                 .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger')
561                 .map(function () {
562                     var $el = $(this);
563                     // TODO: Add a queue with concurrency limit in webclient
564                     // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
565                     return self.saving_mutex.exec(function () {
566                         return self.saveElement($el)
567                             .then(undefined, function (thing, response) {
568                                 // because ckeditor regenerates all the dom,
569                                 // we can't just setup the popover here as
570                                 // everything will be destroyed by the DOM
571                                 // regeneration. Add markings instead, and
572                                 // returns a new rejection with all relevant
573                                 // info
574                                 var id = _.uniqueId('carlos_danger_');
575                                 $el.addClass('oe_dirty oe_carlos_danger');
576                                 $el.addClass(id);
577                                 return $.Deferred().reject({
578                                     id: id,
579                                     error: response.data,
580                                 });
581                             });
582                     });
583                 }).get();
584             return $.when.apply(null, defs).then(function () {
585                 website.reload();
586             }, function (failed) {
587                 // If there were errors, re-enable edition
588                 self.rte.start_edition(true).then(function () {
589                     // jquery's deferred being a pain in the ass
590                     if (!_.isArray(failed)) { failed = [failed]; }
591
592                     _(failed).each(function (failure) {
593                         var html = failure.error.exception_type === "except_osv";
594                         if (html) {
595                             var msg = $("<div/>").text(failure.error.message).html();
596                             var data = msg.substring(3,msg.length-2).split(/', u'/);
597                             failure.error.message = '<b>' + data[0] + '</b><br/>' + data[1];
598                         }
599                         $(root).find('.' + failure.id)
600                             .removeClass(failure.id)
601                             .popover({
602                                 html: html,
603                                 trigger: 'hover',
604                                 content: failure.error.message,
605                                 placement: 'auto top',
606                             })
607                             // Force-show popovers so users will notice them.
608                             .popover('show');
609                     });
610                 });
611             });
612         },
613         /**
614          * Saves an RTE content, which always corresponds to a view section (?).
615          */
616         saveElement: function ($el) {
617             var markup = $el.prop('outerHTML');
618             return openerp.jsonRpc('/web/dataset/call', 'call', {
619                 model: 'ir.ui.view',
620                 method: 'save',
621                 args: [$el.data('oe-id'), markup,
622                        $el.data('oe-xpath') || null,
623                        website.get_context()],
624             });
625         },
626         cancel: function () {
627             new $.Deferred(function (d) {
628                 var $dialog = $(openerp.qweb.render('website.editor.discard')).appendTo(document.body);
629                 $dialog.on('click', '.btn-danger', function () {
630                     d.resolve();
631                 }).on('hidden.bs.modal', function () {
632                     d.reject();
633                 });
634                 d.always(function () {
635                     $dialog.remove();
636                 });
637                 $dialog.modal('show');
638             }).then(function () {
639                 website.reload();
640             })
641         },
642
643         /**
644          * Creates a "hover" button for link edition
645          *
646          * @param {Function} editfn edition function, called when clicking the button
647          * @returns {jQuery}
648          */
649         make_hover_button_link: function (editfn) {
650             return $(openerp.qweb.render('website.editor.hoverbutton.link', {}))
651                 .hide()
652                 .click(function (e) {
653                     e.preventDefault();
654                     e.stopPropagation();
655                     editfn.call(this, e);
656                 })
657                 .appendTo(document.body);
658         },
659
660         /**
661          * Creates a "hover" button for image
662          *
663          * @param {Function} editfn edition function, called when clicking the button
664          * @param {Function} stylefn edition style function, called when clicking the button
665          * @returns {jQuery}
666          */
667         make_hover_button_image: function (editfn, stylefn) {
668             var $div = $(openerp.qweb.render('website.editor.hoverbutton.media', {}))
669                 .hide()
670                 .appendTo(document.body);
671
672             $div.find('[data-toggle="dropdown"]').dropdown();
673             $div.find(".hover-edition-button").click(function (e) {
674                 e.preventDefault();
675                 e.stopPropagation();
676                 editfn.call(this, e);
677             });
678             if (stylefn) {
679                 $div.find(".hover-style-button").click(function (e) {
680                     e.preventDefault();
681                     e.stopPropagation();
682                     stylefn.call(this, e);
683                 });
684             }
685             return $div;
686         },
687         /**
688          * For UI clarity, during RTE edition when the user hovers links and
689          * images a small button should appear to make the capability clear,
690          * as not all users think of double-clicking the image or link.
691          */
692         setup_hover_buttons: function () {
693             var editor = this.rte.editor;
694             var $link_button = this.make_hover_button_link(function () {
695                 var sel = new CKEDITOR.dom.element(previous);
696                 editor.getSelection().selectElement(sel);
697                 if(sel.hasClass('fa')) {
698                     new website.editor.MediaDialog(editor, previous)
699                         .appendTo(document.body);
700                 } else if (previous.tagName.toUpperCase() === 'A') {
701                     link_dialog(editor);
702                 }
703                 $link_button.hide();
704                 previous = null;
705             });
706
707             function is_icons_widget(element) {
708                 var w = editor.widgets.getByElement(element);
709                 return w && w.name === 'icons';
710             }
711
712             // previous is the state of the button-trigger: it's the
713             // currently-ish hovered element which can trigger a button showing.
714             // -ish, because when moving to the button itself ``previous`` is
715             // still set to the element having triggered showing the button.
716             var previous;
717             $(editor.element.$).on('mouseover', 'a', function () {
718                 // Back from edit button -> ignore
719                 if (previous && previous === this) { return; }
720
721                 // hover button should appear for "editable" links and images
722                 // (img and a nodes whose *attributes* are editable, they
723                 // can not be "editing hosts") *or* for non-editing-host
724                 // elements bearing an ``fa`` class. These should have been
725                 // made into CKE widgets which are editing hosts by
726                 // definition, so instead check if the element has been
727                 // converted/upcasted to an fa widget
728                 var selected = new CKEDITOR.dom.element(this);
729                 if (!(is_editable_node(selected) || is_icons_widget(selected))) {
730                     return;
731                 }
732
733                 previous = this;
734                 var $selected = $(this);
735                 var position = $selected.offset();
736                 $link_button.show().offset({
737                     top: $selected.outerHeight()
738                             + position.top,
739                     left: $selected.outerWidth() / 2
740                             + position.left
741                             - $link_button.outerWidth() / 2
742                 })
743             }).on('mouseleave', 'a, img, .fa', function (e) {
744                 var current = document.elementFromPoint(e.clientX, e.clientY);
745                 if (current === $link_button[0] || $(current).parent()[0] === $link_button[0]) {
746                     return;
747                 }
748                 $link_button.hide();
749                 previous = null;
750             });
751         }
752     });
753
754     var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(',');
755     /* ----- RICH TEXT EDITOR ---- */
756     website.RTE = openerp.Widget.extend({
757         tagName: 'li',
758         id: 'oe_rte_toolbar',
759         className: 'oe_right oe_rte_toolbar',
760         // editor.ui.items -> possible commands &al
761         // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
762
763         init: function (EditorBar) {
764             this.EditorBar = EditorBar;
765             this._super.apply(this, arguments);
766         },
767
768         /**
769          * In Webkit-based browsers, triple-click will select a paragraph up to
770          * the start of the next "paragraph" including any empty space
771          * inbetween. When said paragraph is removed or altered, it nukes
772          * the empty space and brings part of the content of the next
773          * "paragraph" (which may well be e.g. an image) into the current one,
774          * completely fucking up layouts and breaking snippets.
775          *
776          * Try to fuck around with selections on triple-click to attempt to
777          * fix this garbage behavior.
778          *
779          * Note: for consistent behavior we may actually want to take over
780          * triple-clicks, in all browsers in order to ensure consistent cross-
781          * platform behavior instead of being at the mercy of rendering engines
782          * & platform selection quirks?
783          */
784         webkitSelectionFixer: function (root) {
785             root.addEventListener('click', function (e) {
786                 // only webkit seems to have a fucked up behavior, ignore others
787                 // FIXME: $.browser goes away in jquery 1.9...
788                 if (!$.browser.webkit) { return; }
789                 // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents
790                 // The detail attribute indicates the number of times a mouse button has been pressed
791                 // we just want the triple click
792                 if (e.detail !== 3) { return; }
793                 e.preventDefault();
794
795                 // Get closest block-level element to the triple-clicked
796                 // element (using ckeditor's block list because why not)
797                 var $closest_block = $(e.target).closest(blocks_selector);
798
799                 // manually set selection range to the content of the
800                 // triple-clicked block-level element, to avoid crossing over
801                 // between block-level elements
802                 document.getSelection().selectAllChildren($closest_block[0]);
803             });
804         },
805         tableNavigation: function (root) {
806             var self = this;
807             $(root).on('keydown', function (e) {
808                 // ignore non-TAB
809                 if (e.which !== 9) { return; }
810
811                 if (self.handleTab(e)) {
812                     e.preventDefault();
813                 }
814             });
815         },
816         /**
817          * Performs whatever operation is necessary on a [TAB] hit, returns
818          * ``true`` if the event's default should be cancelled (if the TAB was
819          * handled by the function)
820          */
821         handleTab: function (event) {
822             var forward = !event.shiftKey;
823
824             var root = window.getSelection().getRangeAt(0).commonAncestorContainer;
825             var $cell = $(root).closest('td,th');
826
827             if (!$cell.length) { return false; }
828
829             var cell = $cell[0];
830
831             // find cell in same row
832             var row = cell.parentNode;
833             var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)];
834             if (sibling) {
835                 document.getSelection().selectAllChildren(sibling);
836                 return true;
837             }
838
839             // find cell in previous/next row
840             var table = row.parentNode;
841             var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)];
842             if (sibling_row) {
843                 var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1];
844                 document.getSelection().selectAllChildren(new_cell);
845                 return true;
846             }
847
848             // at edge cells, copy word/openoffice behavior: if going backwards
849             // from first cell do nothing, if going forwards from last cell add
850             // a row
851             if (forward) {
852                 var row_size = row.cells.length;
853                 var new_row = document.createElement('tr');
854                 while(row_size--) {
855                     var newcell = document.createElement('td');
856                     // zero-width space
857                     newcell.textContent = '\u200B';
858                     new_row.appendChild(newcell);
859                 }
860                 table.appendChild(new_row);
861                 document.getSelection().selectAllChildren(new_row.cells[0]);
862             }
863
864             return true;
865         },
866         /**
867          * Makes the page editable
868          *
869          * @param {Boolean} [restart=false] in case the edition was already set
870          *                                  up once and is being re-enabled.
871          * @returns {$.Deferred} deferred indicating when the RTE is ready
872          */
873         start_edition: function (restart) {
874             var self = this;
875             // create a single editor for the whole page
876             var root = document.getElementById('wrapwrap');
877             if (!restart) {
878                 $(root).on('dragstart', 'img', function (e) {
879                     e.preventDefault();
880                 });
881                 this.webkitSelectionFixer(root);
882                 this.tableNavigation(root);
883             }
884             var def = $.Deferred();
885             var editor = this.editor = CKEDITOR.inline(root, self._config());
886             editor.on('instanceReady', function () {
887                 editor.setReadOnly(false);
888                 // ckeditor set root to editable, disable it (only inner
889                 // sections are editable)
890                 // FIXME: are there cases where the whole editor is editable?
891                 editor.editable().setReadOnly(true);
892
893                 self.setup_editables(root);
894
895                 try {
896                     // disable firefox's broken table resizing thing
897                     document.execCommand("enableObjectResizing", false, "false");
898                     document.execCommand("enableInlineTableEditing", false, "false");
899                 } catch (e) {}
900
901                 // detect & setup any CKEDITOR widget within a newly dropped
902                 // snippet. There does not seem to be a simple way to do it for
903                 // HTML not inserted via ckeditor APIs:
904                 // https://dev.ckeditor.com/ticket/11472
905                 $(document.body)
906                     .off('snippet-dropped')
907                     .on('snippet-dropped', function (e, el) {
908                         // CKEDITOR data processor extended by widgets plugin
909                         // to add wrappers around upcasting elements
910                         el.innerHTML = editor.dataProcessor.toHtml(el.innerHTML, {
911                             fixForBody: false,
912                             dontFilter: true,
913                         });
914                         // then repository.initOnAll() handles the conversion
915                         // from wrapper to actual widget instance (or something
916                         // like that).
917                         setTimeout(function () {
918                             editor.widgets.initOnAll();
919                         }, 0);
920                     });
921
922                 self.trigger('rte:ready');
923                 def.resolve();
924             });
925             return def;
926         },
927
928         setup_editables: function (root) {
929             // selection of editable sub-items was previously in
930             // EditorBar#edit, but for some unknown reason the elements were
931             // apparently removed and recreated (?) at editor initalization,
932             // and observer setup was lost.
933             var self = this;
934             // setup dirty-marking for each editable element
935             this.fetch_editables(root)
936                 .addClass('oe_editable')
937                 .each(function () {
938                     var node = this;
939                     var $node = $(node);
940                     // only explicitly set contenteditable on view sections,
941                     // cke widgets system will do the widgets themselves
942                     if ($node.data('oe-model') === 'ir.ui.view') {
943                         node.contentEditable = true;
944                     }
945
946                     observer.observe(node, OBSERVER_CONFIG);
947                     $node.one('content_changed', function () {
948                         $node.addClass('oe_dirty');
949                         self.trigger('change');
950                     });
951                 });
952         },
953
954         fetch_editables: function (root) {
955             return $(root).find('[data-oe-model]')
956                 .not('link, script')
957                 .not('.oe_snippet_editor')
958                 .filter(function () {
959                     var $this = $(this);
960                     // keep view sections and fields which are *not* in
961                     // view sections for top-level editables
962                     return $this.data('oe-model') === 'ir.ui.view'
963                        || !$this.closest('[data-oe-model = "ir.ui.view"]').length;
964                 });
965         },
966
967         _current_editor: function () {
968             return CKEDITOR.currentInstance;
969         },
970         _config: function () {
971             // base plugins minus
972             // - magicline (captures mousein/mouseout -> breaks draggable)
973             // - contextmenu & tabletools (disable contextual menu)
974             // - bunch of unused plugins
975             var plugins = [
976                 'a11yhelp', 'basicstyles', 'blockquote',
977                 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab',
978                 'elementspath', /*'enterkey',*/ 'entities', 'filebrowser',
979                 'find', 'floatingspace','format', 'htmlwriter', 'iframe',
980                 'indentblock', 'indentlist', 'justify',
981                 'list', 'pastefromword', 'pastetext', 'preview',
982                 'removeformat', 'resize', 'save', 'selectall', 'stylescombo',
983                 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea'
984             ];
985             return {
986                 // FIXME
987                 language: 'en',
988                 // Disable auto-generated titles
989                 // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
990                 title: false,
991                 plugins: plugins.join(','),
992                 uiColor: '',
993                 // FIXME: currently breaks RTE?
994                 // Ensure no config file is loaded
995                 customConfig: '',
996                 // Disable ACF
997                 allowedContent: true,
998                 // Don't insert paragraphs around content in e.g. <li>
999                 autoParagraph: false,
1000                 // Don't automatically add &nbsp; or <br> in empty block-level
1001                 // elements when edition starts
1002                 fillEmptyBlocks: false,
1003                 filebrowserImageUploadUrl: "/website/attach",
1004                 // Support for sharedSpaces in 4.x
1005                 extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,linkstyle',
1006                 // Place toolbar in controlled location
1007                 sharedSpaces: { top: 'oe_rte_toolbar' },
1008                 toolbar: [{
1009                         name: 'basicstyles', items: [
1010                         "Bold", "Italic", "Underline", "Strike", "Subscript",
1011                         "Superscript", "TextColor", "BGColor", "RemoveFormat"
1012                     ]},{
1013                     name: 'span', items: [
1014                         "Link", "LinkStyle", "Blockquote", "BulletedList",
1015                         "NumberedList", "Indent", "Outdent"
1016                     ]},{
1017                     name: 'justify', items: [
1018                         "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
1019                     ]},{
1020                     name: 'special', items: [
1021                         "Image", "TableButton"
1022                     ]},{
1023                     name: 'styles', items: [
1024                         "Styles"
1025                     ]}
1026                 ],
1027                 // styles dropdown in toolbar
1028                 stylesSet: [
1029                     {name: "Normal", element: 'p'},
1030                     {name: "Heading 1", element: 'h1'},
1031                     {name: "Heading 2", element: 'h2'},
1032                     {name: "Heading 3", element: 'h3'},
1033                     {name: "Heading 4", element: 'h4'},
1034                     {name: "Heading 5", element: 'h5'},
1035                     {name: "Heading 6", element: 'h6'},
1036                     {name: "Formatted", element: 'pre'},
1037                     {name: "Address", element: 'address'}
1038                 ],
1039             };
1040         },
1041     });
1042
1043     website.editor = { };
1044     website.editor.Dialog = openerp.Widget.extend({
1045         events: {
1046             'hidden.bs.modal': 'destroy',
1047             'click button.save': 'save',
1048             'click button[data-dismiss="modal"]': 'cancel',
1049         },
1050         init: function (editor) {
1051             this._super();
1052             this.editor = editor;
1053         },
1054         start: function () {
1055             var sup = this._super();
1056             this.$el.modal({backdrop: 'static'});
1057             this.$('input:first').focus();
1058             return sup;
1059         },
1060         save: function () {
1061             this.close();
1062             this.trigger("saved");
1063         },
1064         cancel: function () {
1065             this.trigger("cancel");
1066         },
1067         close: function () {
1068             this.$el.modal('hide');
1069         },
1070     });
1071
1072     website.editor.LinkDialog = website.editor.Dialog.extend({
1073         template: 'website.editor.dialog.link',
1074         events: _.extend({}, website.editor.Dialog.prototype.events, {
1075             'change :input.url-source': function (e) { this.changed($(e.target)); },
1076             'mousedown': function (e) {
1077                 var $target = $(e.target).closest('.list-group-item');
1078                 if (!$target.length || $target.hasClass('active')) {
1079                     // clicked outside groups, or clicked in active groups
1080                     return;
1081                 }
1082
1083                 this.changed($target.find('.url-source').filter(':input'));
1084             },
1085             'click button.remove': 'remove_link',
1086             'change input#link-text': function (e) {
1087                 this.text = $(e.target).val()
1088             },
1089         }),
1090         init: function (editor) {
1091             this._super(editor);
1092             this.text = null;
1093             // Store last-performed request to be able to cancel/abort it.
1094             this.page_exists_req = null;
1095             this.search_pages_req = null;
1096         },
1097         start: function () {
1098             var self = this;
1099             var last;
1100             this.$('#link-page').select2({
1101                 minimumInputLength: 1,
1102                 placeholder: _t("New or existing page"),
1103                 query: function (q) {
1104                     if (q.term == last) return;
1105                     last = q.term;
1106                     $.when(
1107                         self.page_exists(q.term),
1108                         self.fetch_pages(q.term)
1109                     ).then(function (exists, results) {
1110                         var rs = _.map(results, function (r) {
1111                             return { id: r.url, text: r.name, };
1112                         });
1113                         if (!exists) {
1114                             rs.push({
1115                                 create: true,
1116                                 id: q.term,
1117                                 text: _.str.sprintf(_t("Create page '%s'"), q.term),
1118                             });
1119                         }
1120                         q.callback({
1121                             more: false,
1122                             results: rs
1123                         });
1124                     }, function () {
1125                         q.callback({more: false, results: []});
1126                     });
1127                 },
1128             });
1129             return this._super().then(this.proxy('bind_data'));
1130         },
1131         save: function () {
1132             var self = this, _super = this._super.bind(this);
1133             var $e = this.$('.list-group-item.active .url-source').filter(':input');
1134             var val = $e.val();
1135             if (!val || !$e[0].checkValidity()) {
1136                 // FIXME: error message
1137                 $e.closest('.form-group').addClass('has-error');
1138                 $e.focus();
1139                 return;
1140             }
1141
1142             var done = $.when();
1143             if ($e.hasClass('email-address')) {
1144                 this.make_link('mailto:' + val, false, val);
1145             } else if ($e.hasClass('page')) {
1146                 var data = $e.select2('data');
1147                 if (!data.create) {
1148                     self.make_link(data.id, false, data.text);
1149                 } else {
1150                     // Create the page, get the URL back
1151                     done = $.get(_.str.sprintf(
1152                             '/website/add/%s?noredirect=1', encodeURI(data.id)))
1153                         .then(function (response) {
1154                             self.make_link(response, false, data.id);
1155                         });
1156                 }
1157             } else {
1158                 this.make_link(val, this.$('input.window-new').prop('checked'));
1159             }
1160             done.then(_super);
1161         },
1162         make_link: function (url, new_window, label) {
1163         },
1164         bind_data: function (text, href, new_window) {
1165             href = href || this.element && (this.element.data( 'cke-saved-href')
1166                                     ||  this.element.getAttribute('href'));
1167
1168             if (new_window === undefined) {
1169                 new_window = this.element
1170                         ? this.element.getAttribute('target') === '_blank'
1171                         : false;
1172             }
1173             if (text === undefined) {
1174                 text = this.element ? this.element.getText() : '';
1175             }
1176
1177             this.$('input#link-text').val(text);
1178             this.$('input.window-new').prop('checked', new_window);
1179
1180             if (!href) { return; }
1181             var match, $control;
1182             if ((match = /mailto:(.+)/.exec(href))) {
1183                 $control = this.$('input.email-address').val(match[1]);
1184             }
1185             if (!$control) {
1186                 $control = this.$('input.url').val(href);
1187             }
1188
1189             this.changed($control);
1190         },
1191         changed: function ($e) {
1192             this.$('.url-source').filter(':input').not($e).val('')
1193                     .filter(function () { return !!$(this).data('select2'); })
1194                     .select2('data', null);
1195             $e.closest('.list-group-item')
1196                 .addClass('active')
1197                 .siblings().removeClass('active')
1198                 .addBack().removeClass('has-error');
1199         },
1200         call: function (method, args, kwargs) {
1201             var self = this;
1202             var req = method + '_req';
1203
1204             if (this[req]) { this[req].abort(); }
1205
1206             return this[req] = openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1207                 model: 'website',
1208                 method: method,
1209                 args: args,
1210                 kwargs: kwargs,
1211             }).always(function () {
1212                 self[req] = null;
1213             });
1214         },
1215         page_exists: function (term) {
1216             return this.call('page_exists', [null, term], {
1217                 context: website.get_context(),
1218             });
1219         },
1220         fetch_pages: function (term) {
1221             return this.call('search_pages', [null, term], {
1222                 limit: 9,
1223                 context: website.get_context(),
1224             });
1225         },
1226     });
1227     website.editor.RTELinkDialog = website.editor.LinkDialog.extend({
1228         start: function () {
1229             var element;
1230             if ((element = this.get_selected_link()) && element.hasAttribute('href')) {
1231                 this.editor.getSelection().selectElement(element);
1232             }
1233             this.element = element;
1234             if (element) {
1235                 this.add_removal_button();
1236             }
1237
1238             return this._super();
1239         },
1240         add_removal_button: function () {
1241             this.$('.modal-footer').prepend(
1242                 openerp.qweb.render(
1243                     'website.editor.dialog.link.footer-button'));
1244         },
1245         remove_link: function () {
1246             var editor = this.editor;
1247             // same issue as in make_link
1248             setTimeout(function () {
1249                 editor.removeStyle(new CKEDITOR.style({
1250                     element: 'a',
1251                     type: CKEDITOR.STYLE_INLINE,
1252                     alwaysRemoveElement: true,
1253                 }));
1254             }, 0);
1255             this.close();
1256         },
1257         /**
1258          * Greatly simplified version of CKEDITOR's
1259          * plugins.link.dialogs.link.onOk.
1260          *
1261          * @param {String} url
1262          * @param {Boolean} [new_window=false]
1263          * @param {String} [label=null]
1264          */
1265         make_link: function (url, new_window, label) {
1266             var attributes = {href: url, 'data-cke-saved-href': url};
1267             var to_remove = [];
1268             if (new_window) {
1269                 attributes['target'] = '_blank';
1270             } else {
1271                 to_remove.push('target');
1272             }
1273
1274             if (this.element) {
1275                 this.element.setAttributes(attributes);
1276                 this.element.removeAttributes(to_remove);
1277                 if (this.text) { this.element.setText(this.text); }
1278             } else {
1279                 var selection = this.editor.getSelection();
1280                 var range = selection.getRanges(true)[0];
1281
1282                 if (range.collapsed) {
1283                     //noinspection JSPotentiallyInvalidConstructorUsage
1284                     var text = new CKEDITOR.dom.text(
1285                         this.text || label || url);
1286                     range.insertNode(text);
1287                     range.selectNodeContents(text);
1288                 }
1289
1290                 //noinspection JSPotentiallyInvalidConstructorUsage
1291                 new CKEDITOR.style({
1292                     type: CKEDITOR.STYLE_INLINE,
1293                     element: 'a',
1294                     attributes: attributes,
1295                 }).applyToRange(range);
1296
1297                 // focus dance between RTE & dialog blow up the stack in Safari
1298                 // and Chrome, so defer select() until dialog has been closed
1299                 setTimeout(function () {
1300                     range.select();
1301                 }, 0);
1302             }
1303         },
1304         /**
1305          * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root,
1306          * if the editor is set directly on a link it will thus not work.
1307          */
1308         get_selected_link: function () {
1309             return get_selected_link(this.editor);
1310         },
1311     });
1312
1313     website.editor.Media = openerp.Widget.extend({
1314         init: function (parent, editor, media) {
1315             this._super();
1316             this.parent = parent;
1317             this.editor = editor;
1318             this.media = media;
1319         },
1320         start: function () {
1321             this.$preview = this.$('.preview-container').detach();
1322             return this._super();
1323         },
1324         search: function (needle) {
1325         },
1326         save: function () {
1327         },
1328         clear: function () {
1329         },
1330         cancel: function () {
1331         },
1332         close: function () {
1333         },
1334     });
1335     website.editor.MediaDialog = website.editor.Dialog.extend({
1336         template: 'website.editor.dialog.media',
1337         events : _.extend({}, website.editor.Dialog.prototype.events, {
1338             'input input#icon-search': 'search',
1339         }),
1340
1341         init: function (editor, media) {
1342             this._super(editor);
1343             this.editor = editor;
1344             this.page = 0;
1345             this.media = media;
1346         },
1347         start: function () {
1348             var self = this;
1349
1350             this.imageDialog = new website.editor.RTEImageDialog(this, this.editor, this.media);
1351             this.imageDialog.appendTo(this.$("#editor-media-image"));
1352             this.iconDialog = new website.editor.FontIconsDialog(this, this.editor, this.media);
1353             this.iconDialog.appendTo(this.$("#editor-media-icon"));
1354             this.videoDialog = new website.editor.VideoDialog(this, this.editor, this.media);
1355             this.videoDialog.appendTo(this.$("#editor-media-video"));
1356
1357             this.active = this.imageDialog;
1358
1359             $('a[data-toggle="tab"]').on('shown.bs.tab', function (event) {
1360                 if ($(event.target).is('[href="#editor-media-image"]')) {
1361                     self.active = self.imageDialog;
1362                     self.$('li.search, li.previous, li.next').removeClass("hidden");
1363                 } else if ($(event.target).is('[href="#editor-media-icon"]')) {
1364                     self.active = self.iconDialog;
1365                     self.$('li.search, li.previous, li.next').removeClass("hidden");
1366                     self.$('.nav-tabs li.previous, .nav-tabs li.next').addClass("hidden");
1367                 } else if ($(event.target).is('[href="#editor-media-video"]')) {
1368                     self.active = self.videoDialog;
1369                     self.$('.nav-tabs li.search').addClass("hidden");
1370                 }
1371             });
1372
1373             if (this.media) {
1374                 if (this.media.$.nodeName === "IMG") {
1375                     this.$('[href="#editor-media-image"]').tab('show');
1376                 } else if (this.media.$.className.match(/(^|\s)media_iframe_video($|\s)/)) {
1377                     this.$('[href="#editor-media-video"]').tab('show');
1378                 } else if (this.media.$.className.match(/(^|\s)fa($|\s)/)) {
1379                     this.$('[href="#editor-media-icon"]').tab('show');
1380                 }
1381
1382                 if ($(this.media.$).parent().data("oe-field") === "image") {
1383                     this.$('[href="#editor-media-video"], [href="#editor-media-icon"]').addClass('hidden');
1384                 }
1385             }
1386
1387             return this._super();
1388         },
1389         save: function () {
1390             var self = this;
1391             if (self.media) {
1392                 this.media.$.innerHTML = "";
1393                 if (this.active !== this.imageDialog) {
1394                     this.imageDialog.clear();
1395                 }
1396                 if (this.active !== this.iconDialog) {
1397                     this.iconDialog.clear();
1398                 }
1399                 if (this.active !== this.videoDialog) {
1400                     this.videoDialog.clear();
1401                 }
1402             } else {
1403                 var selection = this.editor.getSelection();
1404                 var range = selection.getRanges(true)[0];
1405                 this.media = new CKEDITOR.dom.element("img");
1406                 range.insertNode(this.media);
1407                 range.selectNodeContents(this.media);
1408                 this.active.media = this.media;
1409             }
1410
1411             var $el = $(self.active.media.$);
1412
1413             this.active.save();
1414
1415             this.media.$.className = this.media.$.className.replace(/\s+/g, ' ');
1416
1417             setTimeout(function () {
1418                 $el.trigger("saved", self.active.media.$);
1419                 $(document.body).trigger("media-saved", [$el[0], self.active.media.$]);
1420             },0);
1421
1422             this._super();
1423         },
1424         searchTimer: null,
1425         search: function () {
1426             var self = this;
1427             var needle = this.$("input#icon-search").val();
1428             clearTimeout(this.searchTimer);
1429             this.searchTimer = setTimeout(function () {
1430                 self.active.search(needle || "");
1431             },250);
1432         }
1433     });
1434
1435     /**
1436      * ImageDialog widget. Lets users change an image, including uploading a
1437      * new image in OpenERP or selecting the image style (if supported by
1438      * the caller).
1439      *
1440      * Initialized as usual, but the caller can hook into two events:
1441      *
1442      * @event start({url, style}) called during dialog initialization and
1443      *                            opening, the handler can *set* the ``url``
1444      *                            and ``style`` properties on its parameter
1445      *                            to provide these as default values to the
1446      *                            dialog
1447      * @event save({url, style}) called during dialog finalization, the handler
1448      *                           is provided with the image url and style
1449      *                           selected by the users (or possibly the ones
1450      *                           originally passed in)
1451      */
1452     var IMAGES_PER_ROW = 6;
1453     var IMAGES_ROWS = 2;
1454     website.editor.ImageDialog = website.editor.Media.extend({
1455         template: 'website.editor.dialog.image',
1456         events: _.extend({}, website.editor.Dialog.prototype.events, {
1457             'change .url-source': function (e) {
1458                 this.changed($(e.target));
1459             },
1460             'click button.filepicker': function () {
1461                 this.$('input[type=file]').click();
1462             },
1463             'change input[type=file]': 'file_selection',
1464             'submit form': 'form_submit',
1465             'change input.url': "change_input",
1466             'keyup input.url': "change_input",
1467             //'change select.image-style': 'preview_image',
1468             'click .existing-attachments img': 'select_existing',
1469             'click .existing-attachment-remove': 'try_remove',
1470         }),
1471
1472         init: function (parent, editor, media) {
1473             this.page = 0;
1474             this._super(parent, editor, media);
1475         },
1476         start: function () {
1477             var self = this;
1478             var res = this._super();
1479
1480             var o = { url: null };
1481             // avoid typos, prevent addition of new properties to the object
1482             Object.preventExtensions(o);
1483             this.trigger('start', o);
1484
1485             this.parent.$(".pager > li").click(function (e) {
1486                 e.preventDefault();
1487                 var $target = $(e.currentTarget);
1488                 if ($target.hasClass('disabled')) {
1489                     return;
1490                 }
1491                 self.page += $target.hasClass('previous') ? -1 : 1;
1492                 self.display_attachments();
1493             });
1494
1495             this.set_image(o.url);
1496
1497             return res;
1498         },
1499         save: function () {
1500             if (!this.link) {
1501                 this.link = this.$(".existing-attachments img:first").attr('src');
1502             }
1503             this.trigger('save', {
1504                 url: this.link
1505             });
1506             this.media.renameNode("img");
1507             $(this.media).attr('src', this.link);
1508             return this._super();
1509         },
1510         clear: function () {
1511             this.media.$.className = this.media.$.className.replace(/(^|\s)(img(\s|$)|img-[^\s]*)/g, ' ');
1512         },
1513         cancel: function () {
1514             this.trigger('cancel');
1515         },
1516
1517         change_input: function (e) {
1518             var $input = $(e.target);
1519             var $button = $input.parent().find("button");
1520             if ($input.val() === "") {
1521                 $button.addClass("btn-default").removeClass("btn-primary");
1522             } else {
1523                 $button.removeClass("btn-default").addClass("btn-primary");
1524             }
1525         },
1526
1527         search: function (needle) {
1528             var self = this;
1529             this.fetch_existing(needle).then(function () {
1530                 self.selected_existing(self.$('input.url').val());
1531             });
1532         },
1533
1534         set_image: function (url, error) {
1535             var self = this;
1536             if (url) this.link = url;
1537             this.$('input.url').val('');
1538             this.fetch_existing().then(function () {
1539                 self.selected_existing(url);
1540             });
1541         },
1542
1543         form_submit: function (event) {
1544             var self = this;
1545             var $form = this.$('form[action="/website/attach"]');
1546             if (!$form.find('input[name="upload"]').val().length) {
1547                 var url = $form.find('input[name="url"]').val();
1548                 if (this.selected_existing(url).size()) {
1549                     event.preventDefault();
1550                     return false;
1551                 }
1552             }
1553             var callback = _.uniqueId('func_');
1554             this.$('input[name=func]').val(callback);
1555             window[callback] = function (url, error) {
1556                 delete window[callback];
1557                 self.file_selected(url, error);
1558             };
1559         },
1560         file_selection: function () {
1561             this.$el.addClass('nosave');
1562             this.$('form').removeClass('has-error').find('.help-block').empty();
1563             this.$('button.filepicker').removeClass('btn-danger btn-success');
1564             this.$('form').submit();
1565         },
1566         file_selected: function(url, error) {
1567             var $button = this.$('button.filepicker');
1568             if (!error) {
1569                 $button.addClass('btn-success');
1570             } else {
1571                 url = null;
1572                 this.$('form').addClass('has-error')
1573                     .find('.help-block').text(error);
1574                 $button.addClass('btn-danger');
1575             }
1576             this.set_image(url, error);
1577             // auto save and close popup
1578             this.parent.save();
1579         },
1580
1581         fetch_existing: function (needle) {
1582             var domain = [['res_model', '=', 'ir.ui.view'], '|',
1583                         ['mimetype', '=', false], ['mimetype', '=like', 'image/%']];
1584             if (needle && needle.length) {
1585                 domain.push('|', ['datas_fname', 'ilike', needle], ['name', 'ilike', needle]);
1586             }
1587             return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1588                 model: 'ir.attachment',
1589                 method: 'search_read',
1590                 args: [],
1591                 kwargs: {
1592                     fields: ['name', 'website_url'],
1593                     domain: domain,
1594                     order: 'id desc',
1595                     context: website.get_context(),
1596                 }
1597             }).then(this.proxy('fetched_existing'));
1598         },
1599         fetched_existing: function (records) {
1600             this.records = records;
1601             this.display_attachments();
1602         },
1603         display_attachments: function () {
1604             this.$('.help-block').empty();
1605             var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
1606
1607             var from = this.page * per_screen;
1608             var records = this.records;
1609
1610             // Create rows of 3 records
1611             var rows = _(records).chain()
1612                 .slice(from, from + per_screen)
1613                 .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
1614                 .values()
1615                 .value();
1616
1617             this.$('.existing-attachments').replaceWith(
1618                 openerp.qweb.render(
1619                     'website.editor.dialog.image.existing.content', {rows: rows}));
1620             this.parent.$('.pager')
1621                 .find('li.previous').toggleClass('disabled', (from === 0)).end()
1622                 .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
1623         },
1624         select_existing: function (e) {
1625             var link = $(e.currentTarget).attr('src');
1626             this.link = link;
1627             this.selected_existing(link);
1628         },
1629         selected_existing: function (link) {
1630             this.$('.existing-attachment-cell.media_selected').removeClass("media_selected");
1631             var $select = this.$('.existing-attachment-cell img').filter(function () {
1632                 return $(this).attr("src") == link;
1633             }).first();
1634             $select.parent().addClass("media_selected");
1635             return $select;
1636         },
1637
1638         try_remove: function (e) {
1639             var $help_block = this.$('.help-block').empty();
1640             var self = this;
1641             var $a = $(e.target);
1642             var id = parseInt($a.data('id'), 10);
1643             var attachment = _.findWhere(this.records, {id: id});
1644             var $both = $a.parent().children();
1645
1646             $both.css({borderWidth: "5px", borderColor: "#f00"});
1647
1648             return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1649                 model: 'ir.attachment',
1650                 method: 'try_remove',
1651                 args: [],
1652                 kwargs: {
1653                     ids: [id],
1654                     context: website.get_context()
1655                 }
1656             }).then(function (prevented) {
1657                 if (_.isEmpty(prevented)) {
1658                     self.records = _.without(self.records, attachment);
1659                     self.display_attachments();
1660                     return;
1661                 }
1662                 $both.css({borderWidth: "", borderColor: ""});
1663                 $help_block.replaceWith(openerp.qweb.render(
1664                     'website.editor.dialog.image.existing.error', {
1665                         views: prevented[id]
1666                     }
1667                 ));
1668             });
1669         },
1670     });
1671
1672     website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
1673         init: function (parent, editor, media) {
1674             this._super(parent, editor, media);
1675
1676             this.on('start', this, this.proxy('started'));
1677             this.on('save', this, this.proxy('saved'));
1678         },
1679         started: function (holder) {
1680             if (!this.media) {
1681                 var selection = this.editor.getSelection();
1682                 this.media = selection && selection.getSelectedElement();
1683             }
1684
1685             var el = this.media;
1686             if (!el || !el.is('img')) {
1687                 return;
1688             }
1689             holder.url = el.getAttribute('src');
1690         },
1691         saved: function (data) {
1692             var element, editor = this.editor;
1693             if (!(element = this.media)) {
1694                 element = editor.document.createElement('img');
1695                 element.addClass('img');
1696                 element.addClass('img-responsive');
1697                 // focus event handler interactions between bootstrap (modal)
1698                 // and ckeditor (RTE) lead to blowing the stack in Safari and
1699                 // Chrome (but not FF) when this is done synchronously =>
1700                 // defer insertion so modal has been hidden & destroyed before
1701                 // it happens
1702                 setTimeout(function () {
1703                     editor.insertElement(element);
1704                 }, 0);
1705             }
1706
1707             var style = data.style;
1708             element.setAttribute('src', data.url);
1709             element.removeAttribute('data-cke-saved-src');
1710             if (style) { element.addClass(style); }
1711         },
1712     });
1713
1714     function get_selected_link(editor) {
1715         var sel = editor.getSelection(),
1716             el = sel.getSelectedElement();
1717         if (el && el.is('a')) { return el; }
1718
1719         var range = sel.getRanges(true)[0];
1720         if (!range) { return null; }
1721
1722         range.shrink(CKEDITOR.SHRINK_TEXT);
1723         var commonAncestor = range.getCommonAncestor();
1724         var viewRoot = editor.elementPath(commonAncestor).contains(function (element) {
1725             return element.data('oe-model') === 'ir.ui.view';
1726         });
1727         if (!viewRoot) { return null; }
1728         // if viewRoot is the first link, don't edit it.
1729         return new CKEDITOR.dom.elementPath(commonAncestor, viewRoot)
1730                 .contains('a', true);
1731     }
1732
1733     website.editor.FontIconsDialog = website.editor.Media.extend({
1734         template: 'website.editor.dialog.font-icons',
1735         events : _.extend({}, website.editor.Dialog.prototype.events, {
1736             change: 'update_preview',
1737             'click .font-icons-icon': function (e) {
1738                 e.preventDefault();
1739                 e.stopPropagation();
1740
1741                 this.$('#fa-icon').val(e.target.getAttribute('data-id'));
1742                 this.update_preview();
1743             },
1744             'click #fa-preview span': function (e) {
1745                 e.preventDefault();
1746                 e.stopPropagation();
1747
1748                 this.$('#fa-size').val(e.target.getAttribute('data-size'));
1749                 this.update_preview();
1750             },
1751         }),
1752
1753         // List of FontAwesome icons in 4.0.3, extracted from the cheatsheet.
1754         // Each icon provides the unicode codepoint as ``text`` and the class
1755         // name as ``id`` so the whole thing can be fed directly to select2
1756         // without post-processing and do the right thing (except for the part
1757         // where we still need to implement ``initSelection``)
1758         // TODO: add id/name to the text in order to allow FAYT selection of icons?
1759         icons: [{"text": "\uf000", "id": "fa-glass"}, {"text": "\uf001", "id": "fa-music"}, {"text": "\uf002", "id": "fa-search"}, {"text": "\uf003", "id": "fa-envelope-o"}, {"text": "\uf004", "id": "fa-heart"}, {"text": "\uf005", "id": "fa-star"}, {"text": "\uf006", "id": "fa-star-o"}, {"text": "\uf007", "id": "fa-user"}, {"text": "\uf008", "id": "fa-film"}, {"text": "\uf009", "id": "fa-th-large"}, {"text": "\uf00a", "id": "fa-th"}, {"text": "\uf00b", "id": "fa-th-list"}, {"text": "\uf00c", "id": "fa-check"}, {"text": "\uf00d", "id": "fa-times"}, {"text": "\uf00e", "id": "fa-search-plus"}, {"text": "\uf010", "id": "fa-search-minus"}, {"text": "\uf011", "id": "fa-power-off"}, {"text": "\uf012", "id": "fa-signal"}, {"text": "\uf013", "id": "fa-cog"}, {"text": "\uf014", "id": "fa-trash-o"}, {"text": "\uf015", "id": "fa-home"}, {"text": "\uf016", "id": "fa-file-o"}, {"text": "\uf017", "id": "fa-clock-o"}, {"text": "\uf018", "id": "fa-road"}, {"text": "\uf019", "id": "fa-download"}, {"text": "\uf01a", "id": "fa-arrow-circle-o-down"}, {"text": "\uf01b", "id": "fa-arrow-circle-o-up"}, {"text": "\uf01c", "id": "fa-inbox"}, {"text": "\uf01d", "id": "fa-play-circle-o"}, {"text": "\uf01e", "id": "fa-repeat"}, {"text": "\uf021", "id": "fa-refresh"}, {"text": "\uf022", "id": "fa-list-alt"}, {"text": "\uf023", "id": "fa-lock"}, {"text": "\uf024", "id": "fa-flag"}, {"text": "\uf025", "id": "fa-headphones"}, {"text": "\uf026", "id": "fa-volume-off"}, {"text": "\uf027", "id": "fa-volume-down"}, {"text": "\uf028", "id": "fa-volume-up"}, {"text": "\uf029", "id": "fa-qrcode"}, {"text": "\uf02a", "id": "fa-barcode"}, {"text": "\uf02b", "id": "fa-tag"}, {"text": "\uf02c", "id": "fa-tags"}, {"text": "\uf02d", "id": "fa-book"}, {"text": "\uf02e", "id": "fa-bookmark"}, {"text": "\uf02f", "id": "fa-print"}, {"text": "\uf030", "id": "fa-camera"}, {"text": "\uf031", "id": "fa-font"}, {"text": "\uf032", "id": "fa-bold"}, {"text": "\uf033", "id": "fa-italic"}, {"text": "\uf034", "id": "fa-text-height"}, {"text": "\uf035", "id": "fa-text-width"}, {"text": "\uf036", "id": "fa-align-left"}, {"text": "\uf037", "id": "fa-align-center"}, {"text": "\uf038", "id": "fa-align-right"}, {"text": "\uf039", "id": "fa-align-justify"}, {"text": "\uf03a", "id": "fa-list"}, {"text": "\uf03b", "id": "fa-outdent"}, {"text": "\uf03c", "id": "fa-indent"}, {"text": "\uf03d", "id": "fa-video-camera"}, {"text": "\uf03e", "id": "fa-picture-o"}, {"text": "\uf040", "id": "fa-pencil"}, {"text": "\uf041", "id": "fa-map-marker"}, {"text": "\uf042", "id": "fa-adjust"}, {"text": "\uf043", "id": "fa-tint"}, {"text": "\uf044", "id": "fa-pencil-square-o"}, {"text": "\uf045", "id": "fa-share-square-o"}, {"text": "\uf046", "id": "fa-check-square-o"}, {"text": "\uf047", "id": "fa-arrows"}, {"text": "\uf048", "id": "fa-step-backward"}, {"text": "\uf049", "id": "fa-fast-backward"}, {"text": "\uf04a", "id": "fa-backward"}, {"text": "\uf04b", "id": "fa-play"}, {"text": "\uf04c", "id": "fa-pause"}, {"text": "\uf04d", "id": "fa-stop"}, {"text": "\uf04e", "id": "fa-forward"}, {"text": "\uf050", "id": "fa-fast-forward"}, {"text": "\uf051", "id": "fa-step-forward"}, {"text": "\uf052", "id": "fa-eject"}, {"text": "\uf053", "id": "fa-chevron-left"}, {"text": "\uf054", "id": "fa-chevron-right"}, {"text": "\uf055", "id": "fa-plus-circle"}, {"text": "\uf056", "id": "fa-minus-circle"}, {"text": "\uf057", "id": "fa-times-circle"}, {"text": "\uf058", "id": "fa-check-circle"}, {"text": "\uf059", "id": "fa-question-circle"}, {"text": "\uf05a", "id": "fa-info-circle"}, {"text": "\uf05b", "id": "fa-crosshairs"}, {"text": "\uf05c", "id": "fa-times-circle-o"}, {"text": "\uf05d", "id": "fa-check-circle-o"}, {"text": "\uf05e", "id": "fa-ban"}, {"text": "\uf060", "id": "fa-arrow-left"}, {"text": "\uf061", "id": "fa-arrow-right"}, {"text": "\uf062", "id": "fa-arrow-up"}, {"text": "\uf063", "id": "fa-arrow-down"}, {"text": "\uf064", "id": "fa-share"}, {"text": "\uf065", "id": "fa-expand"}, {"text": "\uf066", "id": "fa-compress"}, {"text": "\uf067", "id": "fa-plus"}, {"text": "\uf068", "id": "fa-minus"}, {"text": "\uf069", "id": "fa-asterisk"}, {"text": "\uf06a", "id": "fa-exclamation-circle"}, {"text": "\uf06b", "id": "fa-gift"}, {"text": "\uf06c", "id": "fa-leaf"}, {"text": "\uf06d", "id": "fa-fire"}, {"text": "\uf06e", "id": "fa-eye"}, {"text": "\uf070", "id": "fa-eye-slash"}, {"text": "\uf071", "id": "fa-exclamation-triangle"}, {"text": "\uf072", "id": "fa-plane"}, {"text": "\uf073", "id": "fa-calendar"}, {"text": "\uf074", "id": "fa-random"}, {"text": "\uf075", "id": "fa-comment"}, {"text": "\uf076", "id": "fa-magnet"}, {"text": "\uf077", "id": "fa-chevron-up"}, {"text": "\uf078", "id": "fa-chevron-down"}, {"text": "\uf079", "id": "fa-retweet"}, {"text": "\uf07a", "id": "fa-shopping-cart"}, {"text": "\uf07b", "id": "fa-folder"}, {"text": "\uf07c", "id": "fa-folder-open"}, {"text": "\uf07d", "id": "fa-arrows-v"}, {"text": "\uf07e", "id": "fa-arrows-h"}, {"text": "\uf080", "id": "fa-bar-chart-o"}, {"text": "\uf081", "id": "fa-twitter-square"}, {"text": "\uf082", "id": "fa-facebook-square"}, {"text": "\uf083", "id": "fa-camera-retro"}, {"text": "\uf084", "id": "fa-key"}, {"text": "\uf085", "id": "fa-cogs"}, {"text": "\uf086", "id": "fa-comments"}, {"text": "\uf087", "id": "fa-thumbs-o-up"}, {"text": "\uf088", "id": "fa-thumbs-o-down"}, {"text": "\uf089", "id": "fa-star-half"}, {"text": "\uf08a", "id": "fa-heart-o"}, {"text": "\uf08b", "id": "fa-sign-out"}, {"text": "\uf08c", "id": "fa-linkedin-square"}, {"text": "\uf08d", "id": "fa-thumb-tack"}, {"text": "\uf08e", "id": "fa-external-link"}, {"text": "\uf090", "id": "fa-sign-in"}, {"text": "\uf091", "id": "fa-trophy"}, {"text": "\uf092", "id": "fa-github-square"}, {"text": "\uf093", "id": "fa-upload"}, {"text": "\uf094", "id": "fa-lemon-o"}, {"text": "\uf095", "id": "fa-phone"}, {"text": "\uf096", "id": "fa-square-o"}, {"text": "\uf097", "id": "fa-bookmark-o"}, {"text": "\uf098", "id": "fa-phone-square"}, {"text": "\uf099", "id": "fa-twitter"}, {"text": "\uf09a", "id": "fa-facebook"}, {"text": "\uf09b", "id": "fa-github"}, {"text": "\uf09c", "id": "fa-unlock"}, {"text": "\uf09d", "id": "fa-credit-card"}, {"text": "\uf09e", "id": "fa-rss"}, {"text": "\uf0a0", "id": "fa-hdd-o"}, {"text": "\uf0a1", "id": "fa-bullhorn"}, {"text": "\uf0f3", "id": "fa-bell"}, {"text": "\uf0a3", "id": "fa-certificate"}, {"text": "\uf0a4", "id": "fa-hand-o-right"}, {"text": "\uf0a5", "id": "fa-hand-o-left"}, {"text": "\uf0a6", "id": "fa-hand-o-up"}, {"text": "\uf0a7", "id": "fa-hand-o-down"}, {"text": "\uf0a8", "id": "fa-arrow-circle-left"}, {"text": "\uf0a9", "id": "fa-arrow-circle-right"}, {"text": "\uf0aa", "id": "fa-arrow-circle-up"}, {"text": "\uf0ab", "id": "fa-arrow-circle-down"}, {"text": "\uf0ac", "id": "fa-globe"}, {"text": "\uf0ad", "id": "fa-wrench"}, {"text": "\uf0ae", "id": "fa-tasks"}, {"text": "\uf0b0", "id": "fa-filter"}, {"text": "\uf0b1", "id": "fa-briefcase"}, {"text": "\uf0b2", "id": "fa-arrows-alt"}, {"text": "\uf0c0", "id": "fa-users"}, {"text": "\uf0c1", "id": "fa-link"}, {"text": "\uf0c2", "id": "fa-cloud"}, {"text": "\uf0c3", "id": "fa-flask"}, {"text": "\uf0c4", "id": "fa-scissors"}, {"text": "\uf0c5", "id": "fa-files-o"}, {"text": "\uf0c6", "id": "fa-paperclip"}, {"text": "\uf0c7", "id": "fa-floppy-o"}, {"text": "\uf0c8", "id": "fa-square"}, {"text": "\uf0c9", "id": "fa-bars"}, {"text": "\uf0ca", "id": "fa-list-ul"}, {"text": "\uf0cb", "id": "fa-list-ol"}, {"text": "\uf0cc", "id": "fa-strikethrough"}, {"text": "\uf0cd", "id": "fa-underline"}, {"text": "\uf0ce", "id": "fa-table"}, {"text": "\uf0d0", "id": "fa-magic"}, {"text": "\uf0d1", "id": "fa-truck"}, {"text": "\uf0d2", "id": "fa-pinterest"}, {"text": "\uf0d3", "id": "fa-pinterest-square"}, {"text": "\uf0d4", "id": "fa-google-plus-square"}, {"text": "\uf0d5", "id": "fa-google-plus"}, {"text": "\uf0d6", "id": "fa-money"}, {"text": "\uf0d7", "id": "fa-caret-down"}, {"text": "\uf0d8", "id": "fa-caret-up"}, {"text": "\uf0d9", "id": "fa-caret-left"}, {"text": "\uf0da", "id": "fa-caret-right"}, {"text": "\uf0db", "id": "fa-columns"}, {"text": "\uf0dc", "id": "fa-sort"}, {"text": "\uf0dd", "id": "fa-sort-asc"}, {"text": "\uf0de", "id": "fa-sort-desc"}, {"text": "\uf0e0", "id": "fa-envelope"}, {"text": "\uf0e1", "id": "fa-linkedin"}, {"text": "\uf0e2", "id": "fa-undo"}, {"text": "\uf0e3", "id": "fa-gavel"}, {"text": "\uf0e4", "id": "fa-tachometer"}, {"text": "\uf0e5", "id": "fa-comment-o"}, {"text": "\uf0e6", "id": "fa-comments-o"}, {"text": "\uf0e7", "id": "fa-bolt"}, {"text": "\uf0e8", "id": "fa-sitemap"}, {"text": "\uf0e9", "id": "fa-umbrella"}, {"text": "\uf0ea", "id": "fa-clipboard"}, {"text": "\uf0eb", "id": "fa-lightbulb-o"}, {"text": "\uf0ec", "id": "fa-exchange"}, {"text": "\uf0ed", "id": "fa-cloud-download"}, {"text": "\uf0ee", "id": "fa-cloud-upload"}, {"text": "\uf0f0", "id": "fa-user-md"}, {"text": "\uf0f1", "id": "fa-stethoscope"}, {"text": "\uf0f2", "id": "fa-suitcase"}, {"text": "\uf0a2", "id": "fa-bell-o"}, {"text": "\uf0f4", "id": "fa-coffee"}, {"text": "\uf0f5", "id": "fa-cutlery"}, {"text": "\uf0f6", "id": "fa-file-text-o"}, {"text": "\uf0f7", "id": "fa-building-o"}, {"text": "\uf0f8", "id": "fa-hospital-o"}, {"text": "\uf0f9", "id": "fa-ambulance"}, {"text": "\uf0fa", "id": "fa-medkit"}, {"text": "\uf0fb", "id": "fa-fighter-jet"}, {"text": "\uf0fc", "id": "fa-beer"}, {"text": "\uf0fd", "id": "fa-h-square"}, {"text": "\uf0fe", "id": "fa-plus-square"}, {"text": "\uf100", "id": "fa-angle-double-left"}, {"text": "\uf101", "id": "fa-angle-double-right"}, {"text": "\uf102", "id": "fa-angle-double-up"}, {"text": "\uf103", "id": "fa-angle-double-down"}, {"text": "\uf104", "id": "fa-angle-left"}, {"text": "\uf105", "id": "fa-angle-right"}, {"text": "\uf106", "id": "fa-angle-up"}, {"text": "\uf107", "id": "fa-angle-down"}, {"text": "\uf108", "id": "fa-desktop"}, {"text": "\uf109", "id": "fa-laptop"}, {"text": "\uf10a", "id": "fa-tablet"}, {"text": "\uf10b", "id": "fa-mobile"}, {"text": "\uf10c", "id": "fa-circle-o"}, {"text": "\uf10d", "id": "fa-quote-left"}, {"text": "\uf10e", "id": "fa-quote-right"}, {"text": "\uf110", "id": "fa-spinner"}, {"text": "\uf111", "id": "fa-circle"}, {"text": "\uf112", "id": "fa-reply"}, {"text": "\uf113", "id": "fa-github-alt"}, {"text": "\uf114", "id": "fa-folder-o"}, {"text": "\uf115", "id": "fa-folder-open-o"}, {"text": "\uf118", "id": "fa-smile-o"}, {"text": "\uf119", "id": "fa-frown-o"}, {"text": "\uf11a", "id": "fa-meh-o"}, {"text": "\uf11b", "id": "fa-gamepad"}, {"text": "\uf11c", "id": "fa-keyboard-o"}, {"text": "\uf11d", "id": "fa-flag-o"}, {"text": "\uf11e", "id": "fa-flag-checkered"}, {"text": "\uf120", "id": "fa-terminal"}, {"text": "\uf121", "id": "fa-code"}, {"text": "\uf122", "id": "fa-reply-all"}, {"text": "\uf122", "id": "fa-mail-reply-all"}, {"text": "\uf123", "id": "fa-star-half-o"}, {"text": "\uf124", "id": "fa-location-arrow"}, {"text": "\uf125", "id": "fa-crop"}, {"text": "\uf126", "id": "fa-code-fork"}, {"text": "\uf127", "id": "fa-chain-broken"}, {"text": "\uf128", "id": "fa-question"}, {"text": "\uf129", "id": "fa-info"}, {"text": "\uf12a", "id": "fa-exclamation"}, {"text": "\uf12b", "id": "fa-superscript"}, {"text": "\uf12c", "id": "fa-subscript"}, {"text": "\uf12d", "id": "fa-eraser"}, {"text": "\uf12e", "id": "fa-puzzle-piece"}, {"text": "\uf130", "id": "fa-microphone"}, {"text": "\uf131", "id": "fa-microphone-slash"}, {"text": "\uf132", "id": "fa-shield"}, {"text": "\uf133", "id": "fa-calendar-o"}, {"text": "\uf134", "id": "fa-fire-extinguisher"}, {"text": "\uf135", "id": "fa-rocket"}, {"text": "\uf136", "id": "fa-maxcdn"}, {"text": "\uf137", "id": "fa-chevron-circle-left"}, {"text": "\uf138", "id": "fa-chevron-circle-right"}, {"text": "\uf139", "id": "fa-chevron-circle-up"}, {"text": "\uf13a", "id": "fa-chevron-circle-down"}, {"text": "\uf13b", "id": "fa-html5"}, {"text": "\uf13c", "id": "fa-css3"}, {"text": "\uf13d", "id": "fa-anchor"}, {"text": "\uf13e", "id": "fa-unlock-alt"}, {"text": "\uf140", "id": "fa-bullseye"}, {"text": "\uf141", "id": "fa-ellipsis-h"}, {"text": "\uf142", "id": "fa-ellipsis-v"}, {"text": "\uf143", "id": "fa-rss-square"}, {"text": "\uf144", "id": "fa-play-circle"}, {"text": "\uf145", "id": "fa-ticket"}, {"text": "\uf146", "id": "fa-minus-square"}, {"text": "\uf147", "id": "fa-minus-square-o"}, {"text": "\uf148", "id": "fa-level-up"}, {"text": "\uf149", "id": "fa-level-down"}, {"text": "\uf14a", "id": "fa-check-square"}, {"text": "\uf14b", "id": "fa-pencil-square"}, {"text": "\uf14c", "id": "fa-external-link-square"}, {"text": "\uf14d", "id": "fa-share-square"}, {"text": "\uf14e", "id": "fa-compass"}, {"text": "\uf150", "id": "fa-caret-square-o-down"}, {"text": "\uf151", "id": "fa-caret-square-o-up"}, {"text": "\uf152", "id": "fa-caret-square-o-right"}, {"text": "\uf153", "id": "fa-eur"}, {"text": "\uf154", "id": "fa-gbp"}, {"text": "\uf155", "id": "fa-usd"}, {"text": "\uf156", "id": "fa-inr"}, {"text": "\uf157", "id": "fa-jpy"}, {"text": "\uf158", "id": "fa-rub"}, {"text": "\uf159", "id": "fa-krw"}, {"text": "\uf15a", "id": "fa-btc"}, {"text": "\uf15b", "id": "fa-file"}, {"text": "\uf15c", "id": "fa-file-text"}, {"text": "\uf15d", "id": "fa-sort-alpha-asc"}, {"text": "\uf15e", "id": "fa-sort-alpha-desc"}, {"text": "\uf160", "id": "fa-sort-amount-asc"}, {"text": "\uf161", "id": "fa-sort-amount-desc"}, {"text": "\uf162", "id": "fa-sort-numeric-asc"}, {"text": "\uf163", "id": "fa-sort-numeric-desc"}, {"text": "\uf164", "id": "fa-thumbs-up"}, {"text": "\uf165", "id": "fa-thumbs-down"}, {"text": "\uf166", "id": "fa-youtube-square"}, {"text": "\uf167", "id": "fa-youtube"}, {"text": "\uf168", "id": "fa-xing"}, {"text": "\uf169", "id": "fa-xing-square"}, {"text": "\uf16a", "id": "fa-youtube-play"}, {"text": "\uf16b", "id": "fa-dropbox"}, {"text": "\uf16c", "id": "fa-stack-overflow"}, {"text": "\uf16d", "id": "fa-instagram"}, {"text": "\uf16e", "id": "fa-flickr"}, {"text": "\uf170", "id": "fa-adn"}, {"text": "\uf171", "id": "fa-bitbucket"}, {"text": "\uf172", "id": "fa-bitbucket-square"}, {"text": "\uf173", "id": "fa-tumblr"}, {"text": "\uf174", "id": "fa-tumblr-square"}, {"text": "\uf175", "id": "fa-long-arrow-down"}, {"text": "\uf176", "id": "fa-long-arrow-up"}, {"text": "\uf177", "id": "fa-long-arrow-left"}, {"text": "\uf178", "id": "fa-long-arrow-right"}, {"text": "\uf179", "id": "fa-apple"}, {"text": "\uf17a", "id": "fa-windows"}, {"text": "\uf17b", "id": "fa-android"}, {"text": "\uf17c", "id": "fa-linux"}, {"text": "\uf17d", "id": "fa-dribbble"}, {"text": "\uf17e", "id": "fa-skype"}, {"text": "\uf180", "id": "fa-foursquare"}, {"text": "\uf181", "id": "fa-trello"}, {"text": "\uf182", "id": "fa-female"}, {"text": "\uf183", "id": "fa-male"}, {"text": "\uf184", "id": "fa-gittip"}, {"text": "\uf185", "id": "fa-sun-o"}, {"text": "\uf186", "id": "fa-moon-o"}, {"text": "\uf187", "id": "fa-archive"}, {"text": "\uf188", "id": "fa-bug"}, {"text": "\uf189", "id": "fa-vk"}, {"text": "\uf18a", "id": "fa-weibo"}, {"text": "\uf18b", "id": "fa-renren"}, {"text": "\uf18c", "id": "fa-pagelines"}, {"text": "\uf18d", "id": "fa-stack-exchange"}, {"text": "\uf18e", "id": "fa-arrow-circle-o-right"}, {"text": "\uf190", "id": "fa-arrow-circle-o-left"}, {"text": "\uf191", "id": "fa-caret-square-o-left"}, {"text": "\uf192", "id": "fa-dot-circle-o"}, {"text": "\uf193", "id": "fa-wheelchair"}, {"text": "\uf194", "id": "fa-vimeo-square"}, {"text": "\uf195", "id": "fa-try"}, {"text": "\uf196", "id": "fa-plus-square-o"}],
1760         /**
1761          * Initializes select2: in Chrome and Safari, <select> font apparently
1762          * isn't customizable (?) and the fontawesome glyphs fail to appear.
1763          */
1764         start: function () {
1765             return this._super().then(this.proxy('load_data'));
1766         },
1767         search: function (needle) {
1768             var icons = this.icons;
1769             if (needle) {
1770                 icons = _(icons).filter(function (icon) {
1771                     return icon.id.substring(3).indexOf(needle) !== -1;
1772                 });
1773             }
1774
1775             this.$('div.font-icons-icons').html(
1776                 openerp.qweb.render(
1777                     'website.editor.dialog.font-icons.icons',
1778                     {icons: icons}));
1779         },
1780         /**
1781          * Removes existing FontAwesome classes on the bound element, and sets
1782          * all the new ones if necessary.
1783          */
1784         save: function () {
1785             var style = this.media.$.attributes.style ? this.media.$.attributes.style.textContent : '';
1786             var classes = (this.media.$.className||"").split(/\s+/);
1787             var non_fa_classes = _.reject(classes, function (cls) {
1788                 return cls === 'fa' || /^fa-/.test(cls);
1789             });
1790             var final_classes = non_fa_classes.concat(this.get_fa_classes());
1791             this.media.$.className = final_classes.join(' ');
1792             this.media.renameNode("span");
1793             this.media.$.attributes.style.textContent = style;
1794             this._super();
1795         },
1796         /**
1797          * Looks up the various FontAwesome classes on the bound element and
1798          * sets the corresponding template/form elements to the right state.
1799          * If multiple classes of the same category are present on an element
1800          * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
1801          * which may not match the visual look of the element.
1802          */
1803         load_data: function () {
1804             var classes = (this.media&&this.media.$.className||"").split(/\s+/);
1805             for (var i = 0; i < classes.length; i++) {
1806                 var cls = classes[i];
1807                 switch(cls) {
1808                 case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
1809                     // size classes
1810                     this.$('#fa-size').val(cls);
1811                     continue;
1812                 case 'fa-spin':
1813                 case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
1814                 case 'fa-flip-horizontal':case 'fa-rotate-vertical':
1815                     this.$('#fa-rotation').val(cls);
1816                     continue;
1817                 case 'fa-fw':
1818                     continue;
1819                 case 'fa-border':
1820                     this.$('#fa-border').prop('checked', true);
1821                     continue;
1822                 default:
1823                     if (!/^fa-/.test(cls)) { continue; }
1824                     this.$('#fa-icon').val(cls);
1825                 }
1826             }
1827             this.update_preview();
1828         },
1829         /**
1830          * Serializes the dialog to an array of FontAwesome classes. Includes
1831          * the base ``fa``.
1832          */
1833         get_fa_classes: function () {
1834             return [
1835                 'fa',
1836                 this.$('#fa-icon').val(),
1837                 this.$('#fa-size').val(),
1838                 this.$('#fa-rotation').val(),
1839                 this.$('#fa-border').prop('checked') ? 'fa-border' : ''
1840             ];
1841         },
1842         update_preview: function () {
1843             this.$preview.empty();
1844             var $preview = this.$('#fa-preview').empty();
1845
1846             var sizes = ['', 'fa-2x', 'fa-3x', 'fa-4x', 'fa-5x'];
1847             var classes = this.get_fa_classes();
1848             var no_sizes = _.difference(classes, sizes).join(' ');
1849             var selected = false;
1850             for (var i = sizes.length - 1; i >= 0; i--) {
1851                 var size = sizes[i];
1852
1853                 var $p = $('<span>')
1854                         .attr('data-size', size)
1855                         .addClass(size)
1856                         .addClass(no_sizes);
1857                 if ((size && _.contains(classes, size)) || (classes[2] === "" && !selected)) {
1858                     this.$preview.append($p.clone());
1859                     this.$('#fa-size').val(size);
1860                     $p.addClass('font-icons-selected');
1861                     selected = true;
1862                 }
1863                 $preview.prepend($p);
1864             }
1865         },
1866         clear: function () {
1867             this.media.$.className = this.media.$.className.replace(/(^|\s)(fa(\s|$)|fa-[^\s]*)/g, ' ');
1868         },
1869     });
1870
1871     website.editor.VideoDialog = website.editor.Media.extend({
1872         template: 'website.editor.dialog.video',
1873         events : _.extend({}, website.editor.Dialog.prototype.events, {
1874             'click input#urlvideo ~ button': 'get_video',
1875             'click input#embedvideo ~ button': 'get_embed_video',
1876             'change input#urlvideo': 'change_input',
1877             'keyup input#urlvideo': 'change_input',
1878             'change input#embedvideo': 'change_input',
1879             'keyup input#embedvideo': 'change_input'
1880         }),
1881         start: function () {
1882             this.$iframe = this.$("iframe");
1883             var $media = $(this.media && this.media.$);
1884             if ($media.hasClass("media_iframe_video")) {
1885                 var src = $media.data('src');
1886                 this.$("input#urlvideo").val(src);
1887                 this.$("#autoplay").attr("checked", src.indexOf('autoplay=1') != -1);
1888                 this.get_video();
1889             }
1890             return this._super();
1891         },
1892         change_input: function (e) {
1893             var $input = $(e.target);
1894             var $button = $input.parent().find("button");
1895             if ($input.val() === "") {
1896                 $button.addClass("btn-default").removeClass("btn-primary");
1897             } else {
1898                 $button.removeClass("btn-default").addClass("btn-primary");
1899             }
1900         },
1901         get_url: function () {
1902             var video_id = this.$("#video_id").val();
1903             var video_type = this.$("#video_type").val();
1904             switch (video_type) {
1905                 case "youtube":
1906                     return "//www.youtube.com/embed/" + video_id + "?autoplay=" + (this.$("#autoplay").is(":checked") ? 1 : 0);
1907                 case "vimeo":
1908                     return "//player.vimeo.com/video/" + video_id + "?autoplay=" + (this.$("#autoplay").is(":checked") ? 1 : 0);
1909                 case "dailymotion":
1910                     return "//www.dailymotion.com/embed/video/" + video_id + "?autoplay=" + (this.$("#autoplay").is(":checked") ? 1 : 0);
1911                 default:
1912                     return video_id;
1913             }
1914         },
1915         get_embed_video: function (event) {
1916             event.preventDefault();
1917             var embedvideo = this.$("input#embedvideo").val().match(/src=["']?([^"']+)["' ]?/);
1918             if (embedvideo) {
1919                 this.$("input#urlvideo").val(embedvideo[1]);
1920                 this.get_video(event);
1921             }
1922             return false;
1923         },
1924         get_video: function (event) {
1925             if (event) event.preventDefault();
1926             var needle = this.$("input#urlvideo").val();
1927             var video_id;
1928             var video_type;
1929
1930             if (needle.indexOf(".youtube.") != -1) {
1931                 video_type = "youtube";
1932                 video_id = needle.match(/\.youtube\.[a-z]+\/(embed\/|watch\?v=)?([^\/?&]+)/i)[2];
1933             } else if (needle.indexOf("//youtu.") != -1) {
1934                 video_type = "youtube";
1935                 video_id = needle.match(/youtube\.[a-z]+\/([^\/?&]+)/i)[1];
1936             } else if (needle.indexOf("player.vimeo.") != -1 || needle.indexOf("//vimeo.") != -1) {
1937                 video_type = "vimeo";
1938                 video_id = needle.match(/vimeo\.[a-z]+\/(video\/)?([^?&]+)/i)[2];
1939             } else if (needle.indexOf(".dailymotion.") != -1) {
1940                 video_type = "dailymotion";
1941                 video_id = needle.match(/dailymotion\.[a-z]+\/(embed\/)?(video\/)?([^\/?&]+)/i)[3];
1942             } else {
1943                 video_type = "";
1944                 video_id = needle;
1945             }
1946
1947             this.$("#video_id").val(video_id);
1948             this.$("#video_type").val(video_type);
1949
1950             this.$iframe.attr("src", this.get_url());
1951             return false;
1952         },
1953         save: function () {
1954             var video_id = this.$("#video_id").val();
1955             if (!video_id) {
1956                 this.$("button.btn-primary").click();
1957                 video_id = this.$("#video_id").val();
1958             }
1959             var video_type = this.$("#video_type").val();
1960             var style = this.media.$.attributes.style ? this.media.$.attributes.style.textContent : '';
1961             var $iframe = $(
1962                 '<div class="media_iframe_video" data-src="'+this.get_url()+'" style="'+style+'">'+
1963                     '<div class="css_editable_mode_display">&nbsp;</div>'+
1964                     '<iframe src="'+this.get_url()+'" frameborder="0" allowfullscreen="allowfullscreen"></iframe>'+
1965                 '</div>');
1966             $(this.media.$).replaceWith($iframe);
1967             this.media.$ = $iframe[0];
1968             this._super();
1969         },
1970         clear: function () {
1971             delete this.media.$.dataset.src;
1972             this.media.$.className = this.media.$.className.replace(/(^|\s)media_iframe_video(\s|$)/g, ' ');
1973         },
1974     });
1975
1976     website.Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
1977     var OBSERVER_CONFIG = {
1978         childList: true,
1979         attributes: true,
1980         characterData: true,
1981         subtree: true,
1982         attributeOldValue: true,
1983     };
1984     var observer = new website.Observer(function (mutations) {
1985         // NOTE: Webkit does not fire DOMAttrModified => webkit browsers
1986         //       relying on JsMutationObserver shim (Chrome < 18, Safari < 6)
1987         //       will not mark dirty on attribute changes (@class, img/@src,
1988         //       a/@href, ...)
1989         _(mutations).chain()
1990             .filter(function (m) {
1991                 // ignore any SVG target, these blokes are like weird mon
1992                 if (m.target && m.target instanceof SVGElement) {
1993                     return false;
1994                 }
1995
1996                 // ignore any change related to mundane image-edit-button
1997                 if (m.target && m.target.className
1998                         && m.target.className.indexOf('image-edit-button') !== -1) {
1999                     return false;
2000                 }
2001                 switch(m.type) {
2002                 case 'attributes': // ignore .cke_focus being added or removed
2003                     // ignore id modification
2004                     if (m.attributeName === 'id') { return false; }
2005                     // if attribute is not a class, can't be .cke_focus change
2006                     if (m.attributeName !== 'class') { return true; }
2007
2008                     // find out what classes were added or removed
2009                     var oldClasses = (m.oldValue || '').split(/\s+/);
2010                     var newClasses = m.target.className.split(/\s+/);
2011                     var change = _.union(_.difference(oldClasses, newClasses),
2012                                          _.difference(newClasses, oldClasses));
2013                     // ignore mutation if the *only* change is .cke_focus
2014                     return change.length !== 1 || change[0] === 'cke_focus';
2015                 case 'childList':
2016                     setTimeout(function () {
2017                         fixup_browser_crap(m.addedNodes);
2018                     }, 0);
2019                     // Remove ignorable nodes from addedNodes or removedNodes,
2020                     // if either set remains non-empty it's considered to be an
2021                     // impactful change. Otherwise it's ignored.
2022                     return !!remove_mundane_nodes(m.addedNodes).length ||
2023                            !!remove_mundane_nodes(m.removedNodes).length;
2024                 default:
2025                     return true;
2026                 }
2027             })
2028             .map(function (m) {
2029                 var node = m.target;
2030                 while (node && !$(node).hasClass('oe_editable')) {
2031                     node = node.parentNode;
2032                 }
2033                 return node;
2034             })
2035             .compact()
2036             .uniq()
2037             .each(function (node) { $(node).trigger('content_changed'); })
2038     });
2039     function remove_mundane_nodes(nodes) {
2040         if (!nodes || !nodes.length) { return []; }
2041
2042         var output = [];
2043         for(var i=0; i<nodes.length; ++i) {
2044             var node = nodes[i];
2045             if (node.nodeType === document.ELEMENT_NODE) {
2046                 if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') {
2047                     // <br type="_moz"> appears when focusing RTE in FF, ignore
2048                     continue;
2049                 } else if (node.nodeName === 'DIV' && $(node).hasClass('oe_drop_zone')) {
2050                     // ignore dropzone inserted by snippets
2051                     continue
2052                 }
2053             }
2054
2055             output.push(node);
2056         }
2057         return output;
2058     }
2059
2060     var programmatic_styles = {
2061         float: 1,
2062         display: 1,
2063         position: 1,
2064         top: 1,
2065         left: 1,
2066         right: 1,
2067         bottom: 1,
2068     };
2069     function fixup_browser_crap(nodes) {
2070         if (!nodes || !nodes.length) { return; }
2071         /**
2072          * Checks that the node only has a @style, not e.g. @class or whatever
2073          */
2074         function has_only_style(node) {
2075             for (var i = 0; i < node.attributes.length; i++) {
2076                 var attr = node.attributes[i];
2077                 if (attr.attributeName !== 'style') {
2078                     return false;
2079                 }
2080             }
2081             return true;
2082         }
2083         function has_programmatic_style(node) {
2084             for (var i = 0; i < node.style.length; i++) {
2085               var style = node.style[i];
2086               if (programmatic_styles[style]) {
2087                   return true;
2088               }
2089             }
2090             return false;
2091         }
2092
2093         for (var i=0; i<nodes.length; ++i) {
2094             var node = nodes[i];
2095             if (node.nodeType !== document.ELEMENT_NODE) { continue; }
2096
2097             if (node.nodeName === 'SPAN'
2098                     && has_only_style(node)
2099                     && !has_programmatic_style(node)) {
2100                 // On backspace, webkit browsers create a <span> with a bunch of
2101                 // inline styles "remembering" where they come from. Refs:
2102                 //    http://www.neotericdesign.com/blog/2013/3/working-around-chrome-s-contenteditable-span-bug
2103                 //    https://code.google.com/p/chromium/issues/detail?id=226941
2104                 //    https://bugs.webkit.org/show_bug.cgi?id=114791
2105                 //    http://dev.ckeditor.com/ticket/9998
2106                 var child, parent = node.parentNode;
2107                 while (child = node.firstChild) {
2108                     parent.insertBefore(child, node);
2109                 }
2110                 parent.removeChild(node);
2111                 // chances are we had e.g.
2112                 //  <p>foo</p>
2113                 //  <p>bar</p>
2114                 // merged the lines getting this in webkit
2115                 //  <p>foo<span>bar</span></p>
2116                 // after unwrapping the span, we have 2 text nodes
2117                 //  <p>[foo][bar]</p>
2118                 // where we probably want only one. Normalize will merge
2119                 // adjacent text nodes. However, does not merge text and cdata
2120                 parent.normalize();
2121             }
2122         }
2123     }
2124 })();