[WIP] website: add media editor template
[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') && is_editable_node(element)) {
88                         image_dialog(editor, element);
89                         return;
90                     }
91
92                     element = get_selected_link(editor) || evt.data.element;
93                     if (!(element.is('a') && is_editable_node(element))) {
94                         return;
95                     }
96
97                     editor.getSelection().selectElement(element);
98                     link_dialog(editor);
99                 }, null, null, 500);
100
101                 //noinspection JSValidateTypes
102                 editor.addCommand('link', {
103                     exec: function (editor) {
104                         link_dialog(editor);
105                         return true;
106                     },
107                     canUndo: false,
108                     editorFocus: true,
109                     context: 'a',
110                 });
111                 //noinspection JSValidateTypes
112                 editor.addCommand('image', {
113                     exec: function (editor) {
114                         image_dialog(editor);
115                         return true;
116                     },
117                     canUndo: false,
118                     editorFocus: true,
119                     context: 'img',
120                 });
121
122                 editor.ui.addButton('Link', {
123                     label: 'Link',
124                     command: 'link',
125                     toolbar: 'links,10',
126                 });
127                 editor.ui.addButton('Image', {
128                     label: 'Image',
129                     command: 'image',
130                     toolbar: 'insert,10',
131                 });
132
133                 editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link');
134             }
135         });
136         CKEDITOR.plugins.add( 'tablebutton', {
137             requires: 'panelbutton,floatpanel',
138             init: function( editor ) {
139                 var label = "Table";
140
141                 editor.ui.add('TableButton', CKEDITOR.UI_PANELBUTTON, {
142                     label: label,
143                     title: label,
144                     // use existing 'table' icon
145                     icon: 'table',
146                     modes: { wysiwyg: true },
147                     editorFocus: true,
148                     // panel opens in iframe, @css is CSS file <link>-ed within
149                     // frame document, @attributes are set on iframe itself.
150                     panel: {
151                         css: '/website/static/src/css/editor.css',
152                         attributes: { 'role': 'listbox', 'aria-label': label, },
153                     },
154
155                     onBlock: function (panel, block) {
156                         block.autoSize = true;
157                         block.element.setHtml(openerp.qweb.render('website.editor.table.panel', {
158                             rows: 5,
159                             cols: 5,
160                         }));
161
162                         var $table = $(block.element.$).on('mouseenter', 'td', function (e) {
163                             var $e = $(e.target);
164                             var y = $e.index() + 1;
165                             var x = $e.closest('tr').index() + 1;
166
167                             $table
168                                 .find('td').removeClass('selected').end()
169                                 .find('tr:lt(' + String(x) + ')')
170                                 .children().filter(function () { return $(this).index() < y; })
171                                 .addClass('selected');
172                         }).on('click', 'td', function (e) {
173                             var $e = $(e.target);
174
175                             //noinspection JSPotentiallyInvalidConstructorUsage
176                             var table = new CKEDITOR.dom.element(
177                                 $(openerp.qweb.render('website.editor.table', {
178                                     rows: $e.closest('tr').index() + 1,
179                                     cols: $e.index() + 1,
180                                 }))[0]);
181
182                             editor.insertElement(table);
183                             setTimeout(function () {
184                                 //noinspection JSPotentiallyInvalidConstructorUsage
185                                 var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]);
186                                 var range = editor.createRange();
187                                 range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START);
188                                 range.select();
189                             }, 0);
190                         });
191
192                         block.element.getDocument().getBody().setStyle('overflow', 'hidden');
193                         CKEDITOR.ui.fire('ready', this);
194                     },
195                 });
196             }
197         });
198
199         CKEDITOR.plugins.add('linkstyle', {
200             requires: 'panelbutton,floatpanel',
201             init: function (editor) {
202                 var label = "Link Style";
203
204                 editor.ui.add('LinkStyle', CKEDITOR.UI_PANELBUTTON, {
205                     label: label,
206                     title: label,
207                     icon: '/website/static/src/img/bglink.png',
208                     modes: { wysiwyg: true },
209                     editorFocus: true,
210                     context: 'a',
211                     panel: {
212                         css: '/web/static/lib/bootstrap/css/bootstrap.css',
213                         attributes: { 'role': 'listbox', 'aria-label': label },
214                     },
215
216                     types: {
217                         'btn-default': _t("Basic"),
218                         'btn-primary': _t("Primary"),
219                         'btn-success': _t("Success"),
220                         'btn-info': _t("Info"),
221                         'btn-warning': _t("Warning"),
222                         'btn-danger': _t("Danger"),
223                     },
224                     sizes: {
225                         'btn-xs': _t("Extra Small"),
226                         'btn-sm': _t("Small"),
227                         '': _t("Default"),
228                         'btn-lg': _t("Large")
229                     },
230
231                     onRender: function () {
232                         var self = this;
233                         editor.on('selectionChange', function (e) {
234                             var path = e.data.path, el;
235
236                             if (!(e = path.contains('a')) || e.isReadOnly()) {
237                                 self.disable();
238                                 return;
239                             }
240
241                             self.enable();
242                         });
243                         // no hook where button is available, so wait
244                         // "some time" after render.
245                         setTimeout(function () {
246                             self.disable();
247                         }, 0)
248                     },
249                     enable: function () {
250                         this.setState(CKEDITOR.TRISTATE_OFF);
251                     },
252                     disable: function () {
253                         this.setState(CKEDITOR.TRISTATE_DISABLED);
254                     },
255
256                     onOpen: function () {
257                         var link = get_selected_link(editor);
258                         var id = this._.id;
259                         var block = this._.panel._.panel._.blocks[id];
260                         var $root = $(block.element.$);
261                         $root.find('button').removeClass('active').removeProp('disabled');
262
263                         // enable buttons matching link state
264                         for (var type in this.types) {
265                             if (!this.types.hasOwnProperty(type)) { continue; }
266                             if (!link.hasClass(type)) { continue; }
267
268                             $root.find('button[data-type=types].' + type)
269                                  .addClass('active');
270                         }
271                         var found;
272                         for (var size in this.sizes) {
273                             if (!this.sizes.hasOwnProperty(size)) { continue; }
274                             if (!size || !link.hasClass(size)) { continue; }
275                             found = true;
276                             $root.find('button[data-type=sizes].' + size)
277                                  .addClass('active');
278                         }
279                         if (!found && link.hasClass('btn')) {
280                             $root.find('button[data-type="sizes"][data-set-class=""]')
281                                  .addClass('active');
282                         }
283                     },
284
285                     onBlock: function (panel, block) {
286                         var self = this;
287                         block.autoSize = true;
288
289                         var html = ['<div style="padding: 5px">'];
290                         html.push('<div style="white-space: nowrap">');
291                         _(this.types).each(function (label, key) {
292                             html.push(_.str.sprintf(
293                                 '<button type="button" class="btn %s" ' +
294                                         'data-type="types" data-set-class="%s">%s</button>',
295                                 key, key, label));
296                         });
297                         html.push('</div>');
298                         html.push('<div style="white-space: nowrap; margin: 5px 0; text-align: center">');
299                         _(this.sizes).each(function (label, key) {
300                             html.push(_.str.sprintf(
301                                 '<button type="button" class="btn btn-default %s" ' +
302                                         'data-type="sizes" data-set-class="%s">%s</button>',
303                                 key, key, label));
304                         });
305                         html.push('</div>');
306                         html.push('<button type="button" class="btn btn-link btn-block" ' +
307                                           'data-type="reset">Reset</button>');
308                         html.push('</div>');
309
310                         block.element.setHtml(html.join(' '));
311                         var $panel = $(block.element.$);
312                         $panel.on('click', 'button', function () {
313                             self.clicked(this);
314                         });
315                     },
316                     clicked: function (button) {
317                         editor.focus();
318                         editor.fire('saveSnapshot');
319
320                         var $button = $(button),
321                               $link = $(get_selected_link(editor).$);
322                         if (!$link.hasClass('btn')) {
323                             $link.addClass('btn btn-default');
324                         }
325                         switch($button.data('type')) {
326                         case 'reset':
327                             $link.removeClass('btn')
328                                  .removeClass(_.keys(this.types).join(' '))
329                                  .removeClass(_.keys(this.sizes).join(' '));
330                             break;
331                         case 'types':
332                             $link.removeClass(_.keys(this.types).join(' '))
333                                  .addClass($button.data('set-class'));
334                             break;
335                         case 'sizes':
336                             $link.removeClass(_.keys(this.sizes).join(' '))
337                                  .addClass($button.data('set-class'));
338                         }
339                         this._.panel.hide();
340
341                         editor.fire('saveSnapshot');
342                     },
343
344                 });
345             }
346         });
347
348         CKEDITOR.plugins.add('oeref', {
349             requires: 'widget',
350
351             init: function (editor) {
352                 var specials = {
353                     // Can't find the correct ACL rule to only allow img tags
354                     image: { content: '*' },
355                     html: { text: '*' },
356                     monetary: {
357                         text: {
358                             selector: 'span.oe_currency_value',
359                             allowedContent: { }
360                         }
361                     }
362                 };
363                 _(specials).each(function (editable, type) {
364                     editor.widgets.add(type, {
365                         draggable: false,
366                         editables: editable,
367                         upcast: function (el) {
368                             return  el.attributes['data-oe-type'] === type;
369
370                         }
371                     });
372                 });
373                 editor.widgets.add('oeref', {
374                     draggable: false,
375                     editables: {
376                         text: {
377                             selector: '*',
378                             allowedContent: { }
379                         },
380                     },
381                     upcast: function (el) {
382                         var type = el.attributes['data-oe-type'];
383                         if (!type || (type in specials)) {
384                             return false;
385                         }
386                         if (el.attributes['data-oe-original']) {
387                             while (el.children.length) {
388                                 el.children[0].remove();
389                             }
390                             el.add(new CKEDITOR.htmlParser.text(
391                                 el.attributes['data-oe-original']
392                             ));
393                         }
394                         return true;
395                     }
396                 });
397
398                 editor.widgets.add('icons', {
399                     draggable: false,
400
401                     init: function () {
402                         this.on('edit', function () {
403                             new website.editor.FontIconsDialog(editor, this.element.$)
404                                 .appendTo(document.body);
405                         });
406                     },
407                     upcast: function (el) {
408                         return el.hasClass('fa')
409                             // ignore ir.ui.view (other data-oe-model should
410                             // already have been matched by oeref and
411                             // monetary?
412                             && !el.attributes['data-oe-model'];
413                     }
414                 });
415             }
416         });
417
418         var editor = new website.EditorBar();
419         var $body = $(document.body);
420         editor.prependTo($body).then(function () {
421             if (location.search.indexOf("enable_editor") >= 0) {
422                 editor.edit();
423             }
424         });
425     };
426
427     /* ----- TOP EDITOR BAR FOR ADMIN ---- */
428     website.EditorBar = openerp.Widget.extend({
429         template: 'website.editorbar',
430         events: {
431             'click button[data-action=edit]': 'edit',
432             'click button[data-action=save]': 'save',
433             'click a[data-action=cancel]': 'cancel',
434         },
435         container: 'body',
436         customize_setup: function() {
437             var self = this;
438             var view_name = $(document.documentElement).data('view-xmlid');
439             if (!view_name) {
440                 this.$('#customize-menu-button').addClass("hidden");
441             }
442             var menu = $('#customize-menu');
443             this.$('#customize-menu-button').click(function(event) {
444                 menu.empty();
445                 openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
446                     function(result) {
447                         _.each(result, function (item) {
448                             if (item.xml_id === "website.debugger" && !window.location.search.match(/[&?]debug(&|$)/)) return;
449                             if (item.header) {
450                                 menu.append('<li class="dropdown-header">' + item.name + '</li>');
451                             } else {
452                                 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>',
453                                     item.id, item.active ? '-check' : '', item.name));
454                             }
455                         });
456                         // Adding Static Menus
457                         menu.append('<li class="divider"></li>');
458                         menu.append('<li><a data-action="ace" href="#">HTML Editor</a></li>');
459                         menu.append('<li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
460                         menu.append('<li><a href="/web#return_label=Website&action=website.action_module_website">Install Apps</a></li>');
461                         self.trigger('rte:customize_menu_ready');
462                     }
463                 );
464             });
465             menu.on('click', 'a[data-action!=ace]', function (event) {
466                 var view_id = $(event.currentTarget).data('view-id');
467                 openerp.jsonRpc('/website/customize_template_toggle', 'call', {
468                     'view_id': view_id
469                 }).then( function() {
470                     window.location.reload();
471                 });
472             });
473         },
474         start: function() {
475             // remove placeholder editor bar
476             var fakebar = document.getElementById('website-top-navbar-placeholder');
477             if (fakebar) {
478                 fakebar.parentNode.removeChild(fakebar);
479             }
480
481             var self = this;
482             this.saving_mutex = new openerp.Mutex();
483
484             this.$('#website-top-edit').hide();
485             this.$('#website-top-view').show();
486
487             $('.dropdown-toggle').dropdown();
488             this.customize_setup();
489
490             this.$buttons = {
491                 edit: this.$('button[data-action=edit]'),
492                 save: this.$('button[data-action=save]'),
493                 cancel: this.$('button[data-action=cancel]'),
494             };
495
496             this.rte = new website.RTE(this);
497             this.rte.on('change', this, this.proxy('rte_changed'));
498             this.rte.on('rte:ready', this, function () {
499                 self.setup_hover_buttons();
500                 self.trigger('rte:ready');
501                 self.check_height();
502             });
503
504             $(window).on('resize', _.debounce(this.check_height.bind(this), 50));
505             this.check_height();
506
507             if (website.is_editable_button) {
508                 this.$("button[data-action=edit]").removeClass("hidden");
509             }
510
511             return $.when(
512                 this._super.apply(this, arguments),
513                 this.rte.appendTo(this.$('#website-top-edit .nav.pull-right'))
514             ).then(function () {
515                 self.check_height();
516             });
517         },
518         check_height: function () {
519             var editor_height = this.$el.outerHeight();
520             if (this.get('height') != editor_height) {
521                 $(document.body).css('padding-top', editor_height);
522                 this.set('height', editor_height);
523             }
524         },
525         edit: function () {
526             this.$buttons.edit.prop('disabled', true);
527             this.$('#website-top-view').hide();
528             this.$('#website-top-edit').show();
529             $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden");
530
531             this.rte.start_edition().then(this.check_height.bind(this));
532             this.trigger('rte:called');
533         },
534         rte_changed: function () {
535             this.$buttons.save.prop('disabled', false);
536         },
537         save: function () {
538             var self = this;
539
540             observer.disconnect();
541             var editor = this.rte.editor;
542             var root = editor.element.$;
543             editor.destroy();
544             // FIXME: select editables then filter by dirty?
545             var defs = this.rte.fetch_editables(root)
546                 .filter('.oe_dirty')
547                 .removeAttr('contentEditable')
548                 .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger')
549                 .map(function () {
550                     var $el = $(this);
551                     // TODO: Add a queue with concurrency limit in webclient
552                     // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
553                     return self.saving_mutex.exec(function () {
554                         return self.saveElement($el)
555                             .then(undefined, function (thing, response) {
556                                 // because ckeditor regenerates all the dom,
557                                 // we can't just setup the popover here as
558                                 // everything will be destroyed by the DOM
559                                 // regeneration. Add markings instead, and
560                                 // returns a new rejection with all relevant
561                                 // info
562                                 var id = _.uniqueId('carlos_danger_');
563                                 $el.addClass('oe_dirty oe_carlos_danger');
564                                 $el.addClass(id);
565                                 return $.Deferred().reject({
566                                     id: id,
567                                     error: response.data,
568                                 });
569                             });
570                     });
571                 }).get();
572             return $.when.apply(null, defs).then(function () {
573                 website.reload();
574             }, function (failed) {
575                 // If there were errors, re-enable edition
576                 self.rte.start_edition(true).then(function () {
577                     // jquery's deferred being a pain in the ass
578                     if (!_.isArray(failed)) { failed = [failed]; }
579
580                     _(failed).each(function (failure) {
581                         var html = failure.error.exception_type === "except_osv";
582                         if (html) {
583                             var msg = $("<div/>").text(failure.error.message).html();
584                             var data = msg.substring(3,msg.length-2).split(/', u'/);
585                             failure.error.message = '<b>' + data[0] + '</b><br/>' + data[1];
586                         }
587                         $(root).find('.' + failure.id)
588                             .removeClass(failure.id)
589                             .popover({
590                                 html: html,
591                                 trigger: 'hover',
592                                 content: failure.error.message,
593                                 placement: 'auto top',
594                             })
595                             // Force-show popovers so users will notice them.
596                             .popover('show');
597                     });
598                 });
599             });
600         },
601         /**
602          * Saves an RTE content, which always corresponds to a view section (?).
603          */
604         saveElement: function ($el) {
605             var markup = $el.prop('outerHTML');
606             return openerp.jsonRpc('/web/dataset/call', 'call', {
607                 model: 'ir.ui.view',
608                 method: 'save',
609                 args: [$el.data('oe-id'), markup,
610                        $el.data('oe-xpath') || null,
611                        website.get_context()],
612             });
613         },
614         cancel: function () {
615             new $.Deferred(function (d) {
616                 var $dialog = $(openerp.qweb.render('website.editor.discard')).appendTo(document.body);
617                 $dialog.on('click', '.btn-danger', function () {
618                     d.resolve();
619                 }).on('hidden.bs.modal', function () {
620                     d.reject();
621                 });
622                 d.always(function () {
623                     $dialog.remove();
624                 });
625                 $dialog.modal('show');
626             }).then(function () {
627                 website.reload();
628             })
629         },
630
631         /**
632          * Creates a "hover" button for image and link edition
633          *
634          * @param {String} label the button's label
635          * @param {Function} editfn edition function, called when clicking the button
636          * @param {String} [classes] additional classes to set on the button
637          * @returns {jQuery}
638          */
639         make_hover_button: function (label, editfn, classes, styleButton) {
640             var $div = $(openerp.qweb.render('website.editor.hoverbutton', {
641                 label: label,
642                 classes: classes,
643                 styleButton: styleButton
644             })).hide().appendTo(document.body);
645
646             $div.find("button.hover-edition-button").click(function (e) {
647                 e.preventDefault();
648                 e.stopPropagation();
649                 editfn.call(this, e);
650             });
651             if (styleButton) {
652                 $div.find("button.hover-style-button").click(function (e) {
653                     e.preventDefault();
654                     e.stopPropagation();
655                     styleButton.call(this, e);
656                 });
657             }
658             return $div;
659         },
660         /**
661          * For UI clarity, during RTE edition when the user hovers links and
662          * images a small button should appear to make the capability clear,
663          * as not all users think of double-clicking the image or link.
664          */
665         setup_hover_buttons: function () {
666             var editor = this.rte.editor;
667             var $link_button = this.make_hover_button(_t("Change"), function () {
668                 var sel = new CKEDITOR.dom.element(previous);
669                 editor.getSelection().selectElement(sel);
670                 if(sel.hasClass('fa')) {
671                     new website.editor.FontIconsDialog(editor, previous)
672                         .appendTo(document.body);
673                 } else if (previous.tagName.toUpperCase() === 'A') {
674                     link_dialog(editor);
675                 }
676                 $link_button.hide();
677                 previous = null;
678             }, 'btn-xs');
679
680             var $image_button = this.make_hover_button(_t("Change"), function () {
681                 image_dialog(editor, new CKEDITOR.dom.element(previous));
682                 $image_button.hide();
683                 previous = null;
684             }, 'btn-sm', function () {
685                 var prev = previous;
686                 var sel = new CKEDITOR.dom.element(prev);
687                 var $sel = $(sel.$);
688                 var $button = $(this);
689
690                 if ($sel.data('transfo')) {
691                     $sel.transfo("destroy");
692                     $button.addClass("btn-primary").removeClass("btn-default");
693                 } else {
694                     $sel.transfo();
695                     $sel.data('transfo').$markup
696                         .on("mouseover", function () {
697                             $sel.trigger("mouseover");
698                             $button.removeClass("btn-primary").addClass("btn-default");
699                             $image_button.show();
700                         });
701                     $sel.data('transfo').$markup.mouseover();
702                 }
703             });
704
705             function is_icons_widget(element) {
706                 var w = editor.widgets.getByElement(element);
707                 return w && w.name === 'icons';
708             }
709
710             // previous is the state of the button-trigger: it's the
711             // currently-ish hovered element which can trigger a button showing.
712             // -ish, because when moving to the button itself ``previous`` is
713             // still set to the element having triggered showing the button.
714             var previous;
715             $(editor.element.$).on('mouseover', 'a, img, .fa', function () {
716                 // Back from edit button -> ignore
717                 if (previous && previous === this) { return; }
718
719                 // hover button should appear for "editable" links and images
720                 // (img and a nodes whose *attributes* are editable, they
721                 // can not be "editing hosts") *or* for non-editing-host
722                 // elements bearing an ``fa`` class. These should have been
723                 // made into CKE widgets which are editing hosts by
724                 // definition, so instead check if the element has been
725                 // converted/upcasted to an fa widget
726                 var selected = new CKEDITOR.dom.element(this);
727                 if (!(is_editable_node(selected) || is_icons_widget(selected))) {
728                     return;
729                 }
730
731                 previous = this;
732                 var $selected = $(this);
733                 var position = $selected.offset();
734                 if ($selected.is('img')) {
735                     $link_button.hide();
736                     // center button on image
737                     $image_button.show().offset({
738                         top: $selected.outerHeight() / 2
739                                 + position.top
740                                 - $image_button.outerHeight() / 2,
741                         left: $selected.outerWidth() / 2
742                                 + position.left
743                                 - $image_button.outerWidth() / 2,
744                     });
745                 } else {
746                     $image_button.hide();
747                     // put button below link, horizontally centered
748                     $link_button.show().offset({
749                         top: $selected.outerHeight()
750                                 + position.top,
751                         left: $selected.outerWidth() / 2
752                                 + position.left
753                                 - $link_button.outerWidth() / 2
754                     })
755                 }
756                 
757                 if ($selected.parents('[data-oe-field="image"]').length) {
758                     $image_button.find("button.hover-style-button").hide();
759                 } else {
760                     $image_button.find("button.hover-style-button").show().addClass("btn-primary").removeClass("btn-default");
761                 }
762             }).on('mouseleave', 'a, img, .fa', function (e) {
763                 var current = document.elementFromPoint(e.clientX, e.clientY);
764                 if (current === $link_button[0] || $(current).parent()[0] === $link_button[0] ||
765                     current === $image_button[0] || $(current).parent()[0] === $image_button[0]) {
766                     return;
767                 }
768                 $image_button.add($link_button).hide();
769                 previous = null;
770             });
771         }
772     });
773
774     var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(',');
775     /* ----- RICH TEXT EDITOR ---- */
776     website.RTE = openerp.Widget.extend({
777         tagName: 'li',
778         id: 'oe_rte_toolbar',
779         className: 'oe_right oe_rte_toolbar',
780         // editor.ui.items -> possible commands &al
781         // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
782
783         init: function (EditorBar) {
784             this.EditorBar = EditorBar;
785             this._super.apply(this, arguments);
786         },
787
788         /**
789          * In Webkit-based browsers, triple-click will select a paragraph up to
790          * the start of the next "paragraph" including any empty space
791          * inbetween. When said paragraph is removed or altered, it nukes
792          * the empty space and brings part of the content of the next
793          * "paragraph" (which may well be e.g. an image) into the current one,
794          * completely fucking up layouts and breaking snippets.
795          *
796          * Try to fuck around with selections on triple-click to attempt to
797          * fix this garbage behavior.
798          *
799          * Note: for consistent behavior we may actually want to take over
800          * triple-clicks, in all browsers in order to ensure consistent cross-
801          * platform behavior instead of being at the mercy of rendering engines
802          * & platform selection quirks?
803          */
804         webkitSelectionFixer: function (root) {
805             root.addEventListener('click', function (e) {
806                 // only webkit seems to have a fucked up behavior, ignore others
807                 // FIXME: $.browser goes away in jquery 1.9...
808                 if (!$.browser.webkit) { return; }
809                 // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents
810                 // The detail attribute indicates the number of times a mouse button has been pressed
811                 // we just want the triple click
812                 if (e.detail !== 3) { return; }
813                 e.preventDefault();
814
815                 // Get closest block-level element to the triple-clicked
816                 // element (using ckeditor's block list because why not)
817                 var $closest_block = $(e.target).closest(blocks_selector);
818
819                 // manually set selection range to the content of the
820                 // triple-clicked block-level element, to avoid crossing over
821                 // between block-level elements
822                 document.getSelection().selectAllChildren($closest_block[0]);
823             });
824         },
825         tableNavigation: function (root) {
826             var self = this;
827             $(root).on('keydown', function (e) {
828                 // ignore non-TAB
829                 if (e.which !== 9) { return; }
830
831                 if (self.handleTab(e)) {
832                     e.preventDefault();
833                 }
834             });
835         },
836         /**
837          * Performs whatever operation is necessary on a [TAB] hit, returns
838          * ``true`` if the event's default should be cancelled (if the TAB was
839          * handled by the function)
840          */
841         handleTab: function (event) {
842             var forward = !event.shiftKey;
843
844             var root = window.getSelection().getRangeAt(0).commonAncestorContainer;
845             var $cell = $(root).closest('td,th');
846
847             if (!$cell.length) { return false; }
848
849             var cell = $cell[0];
850
851             // find cell in same row
852             var row = cell.parentNode;
853             var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)];
854             if (sibling) {
855                 document.getSelection().selectAllChildren(sibling);
856                 return true;
857             }
858
859             // find cell in previous/next row
860             var table = row.parentNode;
861             var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)];
862             if (sibling_row) {
863                 var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1];
864                 document.getSelection().selectAllChildren(new_cell);
865                 return true;
866             }
867
868             // at edge cells, copy word/openoffice behavior: if going backwards
869             // from first cell do nothing, if going forwards from last cell add
870             // a row
871             if (forward) {
872                 var row_size = row.cells.length;
873                 var new_row = document.createElement('tr');
874                 while(row_size--) {
875                     var newcell = document.createElement('td');
876                     // zero-width space
877                     newcell.textContent = '\u200B';
878                     new_row.appendChild(newcell);
879                 }
880                 table.appendChild(new_row);
881                 document.getSelection().selectAllChildren(new_row.cells[0]);
882             }
883
884             return true;
885         },
886         /**
887          * Makes the page editable
888          *
889          * @param {Boolean} [restart=false] in case the edition was already set
890          *                                  up once and is being re-enabled.
891          * @returns {$.Deferred} deferred indicating when the RTE is ready
892          */
893         start_edition: function (restart) {
894             var self = this;
895             // create a single editor for the whole page
896             var root = document.getElementById('wrapwrap');
897             if (!restart) {
898                 $(root).on('dragstart', 'img', function (e) {
899                     e.preventDefault();
900                 });
901                 this.webkitSelectionFixer(root);
902                 this.tableNavigation(root);
903             }
904             var def = $.Deferred();
905             var editor = this.editor = CKEDITOR.inline(root, self._config());
906             editor.on('instanceReady', function () {
907                 editor.setReadOnly(false);
908                 // ckeditor set root to editable, disable it (only inner
909                 // sections are editable)
910                 // FIXME: are there cases where the whole editor is editable?
911                 editor.editable().setReadOnly(true);
912
913                 self.setup_editables(root);
914
915                 try {
916                     // disable firefox's broken table resizing thing
917                     document.execCommand("enableObjectResizing", false, "false");
918                     document.execCommand("enableInlineTableEditing", false, "false");
919                 } catch (e) {}
920
921                 // detect & setup any CKEDITOR widget within a newly dropped
922                 // snippet. There does not seem to be a simple way to do it for
923                 // HTML not inserted via ckeditor APIs:
924                 // https://dev.ckeditor.com/ticket/11472
925                 $(document.body)
926                     .off('snippet-dropped')
927                     .on('snippet-dropped', function (e, el) {
928                         // CKEDITOR data processor extended by widgets plugin
929                         // to add wrappers around upcasting elements
930                         el.innerHTML = editor.dataProcessor.toHtml(el.innerHTML, {
931                             fixForBody: false,
932                             dontFilter: true,
933                         });
934                         // then repository.initOnAll() handles the conversion
935                         // from wrapper to actual widget instance (or something
936                         // like that).
937                         setTimeout(function () {
938                             editor.widgets.initOnAll();
939                         }, 0);
940                     });
941
942                 self.trigger('rte:ready');
943                 def.resolve();
944             });
945             return def;
946         },
947
948         setup_editables: function (root) {
949             // selection of editable sub-items was previously in
950             // EditorBar#edit, but for some unknown reason the elements were
951             // apparently removed and recreated (?) at editor initalization,
952             // and observer setup was lost.
953             var self = this;
954             // setup dirty-marking for each editable element
955             this.fetch_editables(root)
956                 .addClass('oe_editable')
957                 .each(function () {
958                     var node = this;
959                     var $node = $(node);
960                     // only explicitly set contenteditable on view sections,
961                     // cke widgets system will do the widgets themselves
962                     if ($node.data('oe-model') === 'ir.ui.view') {
963                         node.contentEditable = true;
964                     }
965
966                     observer.observe(node, OBSERVER_CONFIG);
967                     $node.one('content_changed', function () {
968                         $node.addClass('oe_dirty');
969                         self.trigger('change');
970                     });
971                 });
972         },
973
974         fetch_editables: function (root) {
975             return $(root).find('[data-oe-model]')
976                 .not('link, script')
977                 .not('.oe_snippet_editor')
978                 .filter(function () {
979                     var $this = $(this);
980                     // keep view sections and fields which are *not* in
981                     // view sections for top-level editables
982                     return $this.data('oe-model') === 'ir.ui.view'
983                        || !$this.closest('[data-oe-model = "ir.ui.view"]').length;
984                 });
985         },
986
987         _current_editor: function () {
988             return CKEDITOR.currentInstance;
989         },
990         _config: function () {
991             // base plugins minus
992             // - magicline (captures mousein/mouseout -> breaks draggable)
993             // - contextmenu & tabletools (disable contextual menu)
994             // - bunch of unused plugins
995             var plugins = [
996                 'a11yhelp', 'basicstyles', 'blockquote',
997                 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab',
998                 'elementspath', /*'enterkey',*/ 'entities', 'filebrowser',
999                 'find', 'floatingspace','format', 'htmlwriter', 'iframe',
1000                 'indentblock', 'indentlist', 'justify',
1001                 'list', 'pastefromword', 'pastetext', 'preview',
1002                 'removeformat', 'resize', 'save', 'selectall', 'stylescombo',
1003                 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea'
1004             ];
1005             return {
1006                 // FIXME
1007                 language: 'en',
1008                 // Disable auto-generated titles
1009                 // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
1010                 title: false,
1011                 plugins: plugins.join(','),
1012                 uiColor: '',
1013                 // FIXME: currently breaks RTE?
1014                 // Ensure no config file is loaded
1015                 customConfig: '',
1016                 // Disable ACF
1017                 allowedContent: true,
1018                 // Don't insert paragraphs around content in e.g. <li>
1019                 autoParagraph: false,
1020                 // Don't automatically add &nbsp; or <br> in empty block-level
1021                 // elements when edition starts
1022                 fillEmptyBlocks: false,
1023                 filebrowserImageUploadUrl: "/website/attach",
1024                 // Support for sharedSpaces in 4.x
1025                 extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,linkstyle',
1026                 // Place toolbar in controlled location
1027                 sharedSpaces: { top: 'oe_rte_toolbar' },
1028                 toolbar: [{
1029                         name: 'basicstyles', items: [
1030                         "Bold", "Italic", "Underline", "Strike", "Subscript",
1031                         "Superscript", "TextColor", "BGColor", "RemoveFormat"
1032                     ]},{
1033                     name: 'span', items: [
1034                         "Link", "LinkStyle", "Blockquote", "BulletedList",
1035                         "NumberedList", "Indent", "Outdent"
1036                     ]},{
1037                     name: 'justify', items: [
1038                         "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
1039                     ]},{
1040                     name: 'special', items: [
1041                         "Image", "TableButton"
1042                     ]},{
1043                     name: 'styles', items: [
1044                         "Styles"
1045                     ]}
1046                 ],
1047                 // styles dropdown in toolbar
1048                 stylesSet: [
1049                     {name: "Normal", element: 'p'},
1050                     {name: "Heading 1", element: 'h1'},
1051                     {name: "Heading 2", element: 'h2'},
1052                     {name: "Heading 3", element: 'h3'},
1053                     {name: "Heading 4", element: 'h4'},
1054                     {name: "Heading 5", element: 'h5'},
1055                     {name: "Heading 6", element: 'h6'},
1056                     {name: "Formatted", element: 'pre'},
1057                     {name: "Address", element: 'address'}
1058                 ],
1059             };
1060         },
1061     });
1062
1063     website.editor = { };
1064     website.editor.Dialog = openerp.Widget.extend({
1065         events: {
1066             'hidden.bs.modal': 'destroy',
1067             'click button.save': 'save',
1068             'click button[data-dismiss="modal"]': 'cancel',
1069         },
1070         init: function (editor) {
1071             this._super();
1072             this.editor = editor;
1073         },
1074         start: function () {
1075             var sup = this._super();
1076             this.$el.modal({backdrop: 'static'});
1077             this.$('input:first').focus();
1078             return sup;
1079         },
1080         save: function () {
1081             this.close();
1082         },
1083         cancel: function () {
1084         },
1085         close: function () {
1086             this.$el.modal('hide');
1087         },
1088     });
1089
1090     website.editor.LinkDialog = website.editor.Dialog.extend({
1091         template: 'website.editor.dialog.link',
1092         events: _.extend({}, website.editor.Dialog.prototype.events, {
1093             'change :input.url-source': function (e) { this.changed($(e.target)); },
1094             'mousedown': function (e) {
1095                 var $target = $(e.target).closest('.list-group-item');
1096                 if (!$target.length || $target.hasClass('active')) {
1097                     // clicked outside groups, or clicked in active groups
1098                     return;
1099                 }
1100
1101                 this.changed($target.find('.url-source').filter(':input'));
1102             },
1103             'click button.remove': 'remove_link',
1104             'change input#link-text': function (e) {
1105                 this.text = $(e.target).val()
1106             },
1107         }),
1108         init: function (editor) {
1109             this._super(editor);
1110             this.text = null;
1111             // Store last-performed request to be able to cancel/abort it.
1112             this.page_exists_req = null;
1113             this.search_pages_req = null;
1114         },
1115         start: function () {
1116             var self = this;
1117             var last;
1118             this.$('#link-page').select2({
1119                 minimumInputLength: 1,
1120                 placeholder: _t("New or existing page"),
1121                 query: function (q) {
1122                     if (q.term == last) return;
1123                     last = q.term;
1124                     $.when(
1125                         self.page_exists(q.term),
1126                         self.fetch_pages(q.term)
1127                     ).then(function (exists, results) {
1128                         var rs = _.map(results, function (r) {
1129                             return { id: r.url, text: r.name, };
1130                         });
1131                         if (!exists) {
1132                             rs.push({
1133                                 create: true,
1134                                 id: q.term,
1135                                 text: _.str.sprintf(_t("Create page '%s'"), q.term),
1136                             });
1137                         }
1138                         q.callback({
1139                             more: false,
1140                             results: rs
1141                         });
1142                     }, function () {
1143                         q.callback({more: false, results: []});
1144                     });
1145                 },
1146             });
1147             return this._super().then(this.proxy('bind_data'));
1148         },
1149         save: function () {
1150             var self = this, _super = this._super.bind(this);
1151             var $e = this.$('.list-group-item.active .url-source').filter(':input');
1152             var val = $e.val();
1153             if (!val || !$e[0].checkValidity()) {
1154                 // FIXME: error message
1155                 $e.closest('.form-group').addClass('has-error');
1156                 $e.focus();
1157                 return;
1158             }
1159
1160             var done = $.when();
1161             if ($e.hasClass('email-address')) {
1162                 this.make_link('mailto:' + val, false, val);
1163             } else if ($e.hasClass('page')) {
1164                 var data = $e.select2('data');
1165                 if (!data.create) {
1166                     self.make_link(data.id, false, data.text);
1167                 } else {
1168                     // Create the page, get the URL back
1169                     done = $.get(_.str.sprintf(
1170                             '/website/add/%s?noredirect=1', encodeURI(data.id)))
1171                         .then(function (response) {
1172                             self.make_link(response, false, data.id);
1173                         });
1174                 }
1175             } else {
1176                 this.make_link(val, this.$('input.window-new').prop('checked'));
1177             }
1178             done.then(_super);
1179         },
1180         make_link: function (url, new_window, label) {
1181         },
1182         bind_data: function (text, href, new_window) {
1183             href = href || this.element && (this.element.data( 'cke-saved-href')
1184                                     ||  this.element.getAttribute('href'));
1185
1186             if (new_window === undefined) {
1187                 new_window = this.element
1188                         ? this.element.getAttribute('target') === '_blank'
1189                         : false;
1190             }
1191             if (text === undefined) {
1192                 text = this.element ? this.element.getText() : '';
1193             }
1194
1195             this.$('input#link-text').val(text);
1196             this.$('input.window-new').prop('checked', new_window);
1197
1198             if (!href) { return; }
1199             var match, $control;
1200             if ((match = /mailto:(.+)/.exec(href))) {
1201                 $control = this.$('input.email-address').val(match[1]);
1202             }
1203             if (!$control) {
1204                 $control = this.$('input.url').val(href);
1205             }
1206
1207             this.changed($control);
1208         },
1209         changed: function ($e) {
1210             this.$('.url-source').filter(':input').not($e).val('')
1211                     .filter(function () { return !!$(this).data('select2'); })
1212                     .select2('data', null);
1213             $e.closest('.list-group-item')
1214                 .addClass('active')
1215                 .siblings().removeClass('active')
1216                 .addBack().removeClass('has-error');
1217         },
1218         call: function (method, args, kwargs) {
1219             var self = this;
1220             var req = method + '_req';
1221
1222             if (this[req]) { this[req].abort(); }
1223
1224             return this[req] = openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1225                 model: 'website',
1226                 method: method,
1227                 args: args,
1228                 kwargs: kwargs,
1229             }).always(function () {
1230                 self[req] = null;
1231             });
1232         },
1233         page_exists: function (term) {
1234             return this.call('page_exists', [null, term], {
1235                 context: website.get_context(),
1236             });
1237         },
1238         fetch_pages: function (term) {
1239             return this.call('search_pages', [null, term], {
1240                 limit: 9,
1241                 context: website.get_context(),
1242             });
1243         },
1244     });
1245     website.editor.RTELinkDialog = website.editor.LinkDialog.extend({
1246         start: function () {
1247             var element;
1248             if ((element = this.get_selected_link()) && element.hasAttribute('href')) {
1249                 this.editor.getSelection().selectElement(element);
1250             }
1251             this.element = element;
1252             if (element) {
1253                 this.add_removal_button();
1254             }
1255
1256             return this._super();
1257         },
1258         add_removal_button: function () {
1259             this.$('.modal-footer').prepend(
1260                 openerp.qweb.render(
1261                     'website.editor.dialog.link.footer-button'));
1262         },
1263         remove_link: function () {
1264             var editor = this.editor;
1265             // same issue as in make_link
1266             setTimeout(function () {
1267                 editor.removeStyle(new CKEDITOR.style({
1268                     element: 'a',
1269                     type: CKEDITOR.STYLE_INLINE,
1270                     alwaysRemoveElement: true,
1271                 }));
1272             }, 0);
1273             this.close();
1274         },
1275         /**
1276          * Greatly simplified version of CKEDITOR's
1277          * plugins.link.dialogs.link.onOk.
1278          *
1279          * @param {String} url
1280          * @param {Boolean} [new_window=false]
1281          * @param {String} [label=null]
1282          */
1283         make_link: function (url, new_window, label) {
1284             var attributes = {href: url, 'data-cke-saved-href': url};
1285             var to_remove = [];
1286             if (new_window) {
1287                 attributes['target'] = '_blank';
1288             } else {
1289                 to_remove.push('target');
1290             }
1291
1292             if (this.element) {
1293                 this.element.setAttributes(attributes);
1294                 this.element.removeAttributes(to_remove);
1295                 if (this.text) { this.element.setText(this.text); }
1296             } else {
1297                 var selection = this.editor.getSelection();
1298                 var range = selection.getRanges(true)[0];
1299
1300                 if (range.collapsed) {
1301                     //noinspection JSPotentiallyInvalidConstructorUsage
1302                     var text = new CKEDITOR.dom.text(
1303                         this.text || label || url);
1304                     range.insertNode(text);
1305                     range.selectNodeContents(text);
1306                 }
1307
1308                 //noinspection JSPotentiallyInvalidConstructorUsage
1309                 new CKEDITOR.style({
1310                     type: CKEDITOR.STYLE_INLINE,
1311                     element: 'a',
1312                     attributes: attributes,
1313                 }).applyToRange(range);
1314
1315                 // focus dance between RTE & dialog blow up the stack in Safari
1316                 // and Chrome, so defer select() until dialog has been closed
1317                 setTimeout(function () {
1318                     range.select();
1319                 }, 0);
1320             }
1321         },
1322         /**
1323          * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root,
1324          * if the editor is set directly on a link it will thus not work.
1325          */
1326         get_selected_link: function () {
1327             return get_selected_link(this.editor);
1328         },
1329     });
1330
1331     website.editor.Media = openerp.Widget.extend({
1332         init: function (editor) {
1333             this._super();
1334             this.editor = editor;
1335         },
1336     });
1337     website.editor.MediaDialog = website.editor.Dialog.extend({
1338         template: 'website.editor.dialog.media',
1339         init: function (editor, media) {
1340             this.media = media;
1341             this.editor = editor;
1342             this._super(editor);
1343         },
1344         start: function (editor, media) {
1345             this.imageDialog = new website.editor.RTEImageDialog(this.editor, this.media);
1346             this.imageDialog.appendTo(this.$("#editor-media-image"));
1347             console.log(this.imageDialog.$el[0]);
1348             this.iconDialog = new website.editor.FontIconsDialog(this.editor, this.media);
1349             this.iconDialog.appendTo(this.$("#editor-media-icon"));
1350             this.videoDialog = null;
1351             //this.videoDialog.appendTo(this.$("#editor-media-video"));
1352             return this._super();
1353         }
1354     });
1355
1356     /**
1357      * ImageDialog widget. Lets users change an image, including uploading a
1358      * new image in OpenERP or selecting the image style (if supported by
1359      * the caller).
1360      *
1361      * Initialized as usual, but the caller can hook into two events:
1362      *
1363      * @event start({url, style}) called during dialog initialization and
1364      *                            opening, the handler can *set* the ``url``
1365      *                            and ``style`` properties on its parameter
1366      *                            to provide these as default values to the
1367      *                            dialog
1368      * @event save({url, style}) called during dialog finalization, the handler
1369      *                           is provided with the image url and style
1370      *                           selected by the users (or possibly the ones
1371      *                           originally passed in)
1372      */
1373     website.editor.ImageDialog = website.editor.Media.extend({
1374         template: 'website.editor.dialog.image',
1375         events: _.extend({}, website.editor.Dialog.prototype.events, {
1376             'change .url-source': function (e) { this.changed($(e.target)); },
1377             'click button.filepicker': function () {
1378                 this.$('input[type=file]').click();
1379             },
1380             'change input[type=file]': 'file_selection',
1381             'change input.url': 'preview_image',
1382             'click a[href=#existing]': 'browse_existing',
1383             'change select.image-style': 'preview_image',
1384         }),
1385
1386         start: function () {
1387             this.$('button.wait').text("Uploading…");
1388             var $options = this.$('.image-style').children();
1389             this.image_styles = $options.map(function () { return this.value; }).get();
1390
1391             var o = { url: null, style: null, };
1392             // avoid typos, prevent addition of new properties to the object
1393             Object.preventExtensions(o);
1394             this.trigger('start', o);
1395
1396             if (o.url) {
1397                 if (o.style) {
1398                     this.$('.image-style').val(o.style);
1399                 }
1400                 this.set_image(o.url);
1401             }
1402
1403             return this._super();
1404         },
1405         save: function () {
1406             this.trigger('save', {
1407                 url: this.$('input.url').val(),
1408                 style: this.$('.image-style').val(),
1409             });
1410             return this._super();
1411         },
1412         cancel: function () {
1413             this.trigger('cancel');
1414         },
1415
1416         /**
1417          * Sets the provided image url as the dialog's value-to-save and
1418          * refreshes the preview element to use it.
1419          */
1420         set_image: function (url, error) {
1421             this.$('input.url').val(
1422                 error ? '' : url);
1423             this.$('input.url').val(url);
1424             this.preview_image();
1425         },
1426
1427         file_selection: function () {
1428             this.$el.addClass('nosave');
1429             this.$('form').removeClass('has-error').find('.help-block').empty();
1430             this.$('button.filepicker').removeClass('btn-danger btn-success');
1431
1432             var self = this;
1433             var callback = _.uniqueId('func_');
1434             this.$('input[name=func]').val(callback);
1435
1436             window[callback] = function (url, error) {
1437                 delete window[callback];
1438                 self.file_selected(url, error);
1439             };
1440             this.$('form').submit();
1441         },
1442         file_selected: function(url, error) {
1443             var $button = this.$('button.filepicker');
1444             if (!error) {
1445                 $button.addClass('btn-success');
1446             } else {
1447                 url = null;
1448                 this.$('form').addClass('has-error')
1449                     .find('.help-block').text(error);
1450                 $button.addClass('btn-danger');
1451             }
1452             this.set_image(url, error);
1453         },
1454         preview_image: function () {
1455             var loaded = function () {
1456                 this.$el.removeClass('nosave');
1457             }.bind(this);
1458             var image = this.$('input.url').val();
1459             if (!image) { loaded(); return; }
1460
1461             var $img = this.$('img.image-preview')
1462                 .attr('src', image)
1463                 .removeClass(this.image_styles.join(' '))
1464                 .addClass(this.$('select.image-style').val());
1465
1466             if ($img.prop('complete')) {
1467                 loaded();
1468             } else {
1469                 $img.load(loaded)
1470             }
1471         },
1472         browse_existing: function (e) {
1473             e.preventDefault();
1474             this.$('form').removeClass('has-error').find('.help-block').empty();
1475             this.$('button.filepicker').removeClass('btn-danger btn-success');
1476             new website.editor.ExistingImageDialog(this).appendTo(document.body);
1477         },
1478     });
1479
1480     website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
1481         init: function (editor, image) {
1482             this._super(editor);
1483
1484             this.element = image;
1485
1486             this.on('start', this, this.proxy('started'));
1487             this.on('save', this, this.proxy('saved'));
1488         },
1489         started: function (holder) {
1490             if (!this.element) {
1491                 var selection = this.editor.getSelection();
1492                 this.element = selection && selection.getSelectedElement();
1493             }
1494
1495             var el = this.element;
1496             if (!el || !el.is('img')) {
1497                 return;
1498             }
1499             _(this.image_styles).each(function (style) {
1500                 if (el.hasClass(style)) {
1501                     holder.style = style;
1502                 }
1503             });
1504             holder.url = el.getAttribute('src');
1505         },
1506         saved: function (data) {
1507             var element, editor = this.editor;
1508             if (!(element = this.element)) {
1509                 element = editor.document.createElement('img');
1510                 element.addClass('img');
1511                 element.addClass('img-responsive');
1512                 // focus event handler interactions between bootstrap (modal)
1513                 // and ckeditor (RTE) lead to blowing the stack in Safari and
1514                 // Chrome (but not FF) when this is done synchronously =>
1515                 // defer insertion so modal has been hidden & destroyed before
1516                 // it happens
1517                 setTimeout(function () {
1518                     editor.insertElement(element);
1519                 }, 0);
1520             }
1521
1522             var style = data.style;
1523             element.setAttribute('src', data.url);
1524             element.removeAttribute('data-cke-saved-src');
1525             $(element.$).removeClass(this.image_styles.join(' '));
1526             if (style) { element.addClass(style); }
1527         },
1528     });
1529
1530     var IMAGES_PER_ROW = 6;
1531     var IMAGES_ROWS = 4;
1532     website.editor.ExistingImageDialog = website.editor.Dialog.extend({
1533         template: 'website.editor.dialog.image.existing',
1534         events: _.extend({}, website.editor.Dialog.prototype.events, {
1535             'click .existing-attachments img': 'select_existing',
1536             'click .pager > li': function (e) {
1537                 e.preventDefault();
1538                 var $target = $(e.currentTarget);
1539                 if ($target.hasClass('disabled')) {
1540                     return;
1541                 }
1542                 this.page += $target.hasClass('previous') ? -1 : 1;
1543                 this.display_attachments();
1544             },
1545             'click .existing-attachment-remove': 'try_remove',
1546         }),
1547         init: function (parent) {
1548             this.image = null;
1549             this.page = 0;
1550             this.parent = parent;
1551             this._super(parent.editor);
1552         },
1553
1554         start: function () {
1555             return $.when(
1556                 this._super(),
1557                 this.fetch_existing().then(this.proxy('fetched_existing')));
1558         },
1559
1560         fetch_existing: function () {
1561             return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1562                 model: 'ir.attachment',
1563                 method: 'search_read',
1564                 args: [],
1565                 kwargs: {
1566                     fields: ['name', 'website_url'],
1567                     domain: [['res_model', '=', 'ir.ui.view']],
1568                     order: 'id desc',
1569                     context: website.get_context(),
1570                 }
1571             });
1572         },
1573         fetched_existing: function (records) {
1574             this.records = records;
1575             this.display_attachments();
1576         },
1577         display_attachments: function () {
1578             this.$('.help-block').empty();
1579             var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
1580
1581             var from = this.page * per_screen;
1582             var records = this.records;
1583
1584             // Create rows of 3 records
1585             var rows = _(records).chain()
1586                 .slice(from, from + per_screen)
1587                 .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
1588                 .values()
1589                 .value();
1590
1591             this.$('.existing-attachments').replaceWith(
1592                 openerp.qweb.render(
1593                     'website.editor.dialog.image.existing.content', {rows: rows}));
1594             this.$('.pager')
1595                 .find('li.previous').toggleClass('disabled', (from === 0)).end()
1596                 .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
1597
1598         },
1599         select_existing: function (e) {
1600             var link = $(e.currentTarget).attr('src');
1601             if (link) {
1602                 this.parent.set_image(link);
1603             }
1604             this.close()
1605         },
1606
1607         try_remove: function (e) {
1608             var $help_block = this.$('.help-block').empty();
1609             var self = this;
1610             var id = parseInt($(e.target).data('id'), 10);
1611             var attachment = _.findWhere(this.records, {id: id});
1612
1613             return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1614                 model: 'ir.attachment',
1615                 method: 'try_remove',
1616                 args: [],
1617                 kwargs: {
1618                     ids: [id],
1619                     context: website.get_context()
1620                 }
1621             }).then(function (prevented) {
1622                 if (_.isEmpty(prevented)) {
1623                     self.records = _.without(self.records, attachment);
1624                     self.display_attachments();
1625                     return;
1626                 }
1627                 $help_block.replaceWith(openerp.qweb.render(
1628                     'website.editor.dialog.image.existing.error', {
1629                         views: prevented[id]
1630                     }
1631                 ));
1632             });
1633         },
1634     });
1635
1636     function get_selected_link(editor) {
1637         var sel = editor.getSelection(),
1638             el = sel.getSelectedElement();
1639         if (el && el.is('a')) { return el; }
1640
1641         var range = sel.getRanges(true)[0];
1642         if (!range) { return null; }
1643
1644         range.shrink(CKEDITOR.SHRINK_TEXT);
1645         var commonAncestor = range.getCommonAncestor();
1646         var viewRoot = editor.elementPath(commonAncestor).contains(function (element) {
1647             return element.data('oe-model') === 'ir.ui.view';
1648         });
1649         if (!viewRoot) { return null; }
1650         // if viewRoot is the first link, don't edit it.
1651         return new CKEDITOR.dom.elementPath(commonAncestor, viewRoot)
1652                 .contains('a', true);
1653     }
1654
1655     website.editor.FontIconsDialog = website.editor.Media.extend({
1656         template: 'website.editor.dialog.font-icons',
1657         events : _.extend({}, website.editor.Dialog.prototype.events, {
1658             change: 'update_preview',
1659             'click .font-icons-icon': function (e) {
1660                 e.preventDefault();
1661                 e.stopPropagation();
1662
1663                 this.$('#fa-icon').val(e.target.getAttribute('data-id'));
1664                 this.update_preview();
1665             },
1666             'click #fa-preview span': function (e) {
1667                 e.preventDefault();
1668                 e.stopPropagation();
1669
1670                 this.$('#fa-size').val(e.target.getAttribute('data-size'));
1671                 this.update_preview();
1672             },
1673             'input input#icon-search': function () {
1674                 var needle = this.$('#icon-search').val();
1675                 var icons = this.icons;
1676                 if (needle) {
1677                     icons = _(icons).filter(function (icon) {
1678                         return icon.id.substring(3).indexOf(needle) !== -1;
1679                     });
1680                 }
1681
1682                 this.$('div.font-icons-icons').html(
1683                     openerp.qweb.render(
1684                         'website.editor.dialog.font-icons.icons',
1685                         {icons: icons}));
1686             },
1687         }),
1688
1689         // List of FontAwesome icons in 4.0.3, extracted from the cheatsheet.
1690         // Each icon provides the unicode codepoint as ``text`` and the class
1691         // name as ``id`` so the whole thing can be fed directly to select2
1692         // without post-processing and do the right thing (except for the part
1693         // where we still need to implement ``initSelection``)
1694         // TODO: add id/name to the text in order to allow FAYT selection of icons?
1695         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"}],
1696         init: function (editor, element) {
1697             this._super(editor);
1698             this.element = element;
1699         },
1700         /**
1701          * Initializes select2: in Chrome and Safari, <select> font apparently
1702          * isn't customizable (?) and the fontawesome glyphs fail to appear.
1703          */
1704         start: function () {
1705             return this._super().then(this.proxy('load_data'));
1706         },
1707         /**
1708          * Removes existing FontAwesome classes on the bound element, and sets
1709          * all the new ones if necessary.
1710          */
1711         save: function () {
1712             var classes = (this.element.className||"").split(/\s+/);
1713             var non_fa_classes = _.reject(classes, function (cls) {
1714                 return cls === 'fa' || /^fa-/.test(cls);
1715             });
1716             var final_classes = non_fa_classes.concat(this.get_fa_classes());
1717             this.element.className = final_classes.join(' ');
1718             this._super();
1719         },
1720         /**
1721          * Looks up the various FontAwesome classes on the bound element and
1722          * sets the corresponding template/form elements to the right state.
1723          * If multiple classes of the same category are present on an element
1724          * (e.g. fa-lg and fa-3x) the last one occurring will be selected,
1725          * which may not match the visual look of the element.
1726          */
1727         load_data: function () {
1728             var classes = (this.element.className||"").split(/\s+/);
1729             for (var i = 0; i < classes.length; i++) {
1730                 var cls = classes[i];
1731                 switch(cls) {
1732                 case 'fa-2x':case 'fa-3x':case 'fa-4x':case 'fa-5x':
1733                     // size classes
1734                     this.$('#fa-size').val(cls);
1735                     continue;
1736                 case 'fa-spin':
1737                 case 'fa-rotate-90':case 'fa-rotate-180':case 'fa-rotate-270':
1738                 case 'fa-flip-horizontal':case 'fa-rotate-vertical':
1739                     this.$('#fa-rotation').val(cls);
1740                     continue;
1741                 case 'fa-fw':
1742                     continue;
1743                 case 'fa-border':
1744                     this.$('#fa-border').prop('checked', true);
1745                     continue;
1746                 default:
1747                     if (!/^fa-/.test(cls)) { continue; }
1748                     this.$('#fa-icon').val(cls);
1749                 }
1750             }
1751             this.update_preview();
1752         },
1753         /**
1754          * Serializes the dialog to an array of FontAwesome classes. Includes
1755          * the base ``fa``.
1756          */
1757         get_fa_classes: function () {
1758             return [
1759                 'fa',
1760                 this.$('#fa-icon').val(),
1761                 this.$('#fa-size').val(),
1762                 this.$('#fa-rotation').val(),
1763                 this.$('#fa-border').prop('checked') ? 'fa-border' : ''
1764             ];
1765         },
1766         update_preview: function () {
1767             var $preview = this.$('#fa-preview').empty();
1768             var sizes = ['', 'fa-2x', 'fa-3x', 'fa-4x', 'fa-5x'];
1769             var classes = this.get_fa_classes();
1770             var no_sizes = _.difference(classes, sizes).join(' ');
1771             var selected = false;
1772             for (var i = sizes.length - 1; i >= 0; i--) {
1773                 var size = sizes[i];
1774
1775                 var $p = $('<span>')
1776                         .attr('data-size', size)
1777                         .addClass(size)
1778                         .addClass(no_sizes);
1779                 if ((size && _.contains(classes, size)) || (!size && !selected)) {
1780                     $p.addClass('font-icons-selected');
1781                     selected = true;
1782                 }
1783                 $preview.prepend($p);
1784             }
1785         }
1786     });
1787
1788     website.Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
1789     var OBSERVER_CONFIG = {
1790         childList: true,
1791         attributes: true,
1792         characterData: true,
1793         subtree: true,
1794         attributeOldValue: true,
1795     };
1796     var observer = new website.Observer(function (mutations) {
1797         // NOTE: Webkit does not fire DOMAttrModified => webkit browsers
1798         //       relying on JsMutationObserver shim (Chrome < 18, Safari < 6)
1799         //       will not mark dirty on attribute changes (@class, img/@src,
1800         //       a/@href, ...)
1801         _(mutations).chain()
1802             .filter(function (m) {
1803                 // ignore any change related to mundane image-edit-button
1804                 if (m.target && m.target.className
1805                         && m.target.className.indexOf('image-edit-button') !== -1) {
1806                     return false;
1807                 }
1808                 switch(m.type) {
1809                 case 'attributes': // ignore .cke_focus being added or removed
1810                     // ignore id modification
1811                     if (m.attributeName === 'id') { return false; }
1812                     // if attribute is not a class, can't be .cke_focus change
1813                     if (m.attributeName !== 'class') { return true; }
1814
1815                     // find out what classes were added or removed
1816                     var oldClasses = (m.oldValue || '').split(/\s+/);
1817                     var newClasses = m.target.className.split(/\s+/);
1818                     var change = _.union(_.difference(oldClasses, newClasses),
1819                                          _.difference(newClasses, oldClasses));
1820                     // ignore mutation if the *only* change is .cke_focus
1821                     return change.length !== 1 || change[0] === 'cke_focus';
1822                 case 'childList':
1823                     setTimeout(function () {
1824                         fixup_browser_crap(m.addedNodes);
1825                     }, 0);
1826                     // Remove ignorable nodes from addedNodes or removedNodes,
1827                     // if either set remains non-empty it's considered to be an
1828                     // impactful change. Otherwise it's ignored.
1829                     return !!remove_mundane_nodes(m.addedNodes).length ||
1830                            !!remove_mundane_nodes(m.removedNodes).length;
1831                 default:
1832                     return true;
1833                 }
1834             })
1835             .map(function (m) {
1836                 var node = m.target;
1837                 while (node && !$(node).hasClass('oe_editable')) {
1838                     node = node.parentNode;
1839                 }
1840                 return node;
1841             })
1842             .compact()
1843             .uniq()
1844             .each(function (node) { $(node).trigger('content_changed'); })
1845     });
1846     function remove_mundane_nodes(nodes) {
1847         if (!nodes || !nodes.length) { return []; }
1848
1849         var output = [];
1850         for(var i=0; i<nodes.length; ++i) {
1851             var node = nodes[i];
1852             if (node.nodeType === document.ELEMENT_NODE) {
1853                 if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') {
1854                     // <br type="_moz"> appears when focusing RTE in FF, ignore
1855                     continue;
1856                 } else if (node.nodeName === 'DIV' && $(node).hasClass('oe_drop_zone')) {
1857                     // ignore dropzone inserted by snippets
1858                     continue
1859                 }
1860             }
1861
1862             output.push(node);
1863         }
1864         return output;
1865     }
1866
1867     var programmatic_styles = {
1868         float: 1,
1869         display: 1,
1870         position: 1,
1871         top: 1,
1872         left: 1,
1873         right: 1,
1874         bottom: 1,
1875     };
1876     function fixup_browser_crap(nodes) {
1877         if (!nodes || !nodes.length) { return; }
1878         /**
1879          * Checks that the node only has a @style, not e.g. @class or whatever
1880          */
1881         function has_only_style(node) {
1882             for (var i = 0; i < node.attributes.length; i++) {
1883                 var attr = node.attributes[i];
1884                 if (attr.attributeName !== 'style') {
1885                     return false;
1886                 }
1887             }
1888             return true;
1889         }
1890         function has_programmatic_style(node) {
1891             for (var i = 0; i < node.style.length; i++) {
1892               var style = node.style[i];
1893               if (programmatic_styles[style]) {
1894                   return true;
1895               }
1896             }
1897             return false;
1898         }
1899
1900         for (var i=0; i<nodes.length; ++i) {
1901             var node = nodes[i];
1902             if (node.nodeType !== document.ELEMENT_NODE) { continue; }
1903
1904             if (node.nodeName === 'SPAN'
1905                     && has_only_style(node)
1906                     && !has_programmatic_style(node)) {
1907                 // On backspace, webkit browsers create a <span> with a bunch of
1908                 // inline styles "remembering" where they come from. Refs:
1909                 //    http://www.neotericdesign.com/blog/2013/3/working-around-chrome-s-contenteditable-span-bug
1910                 //    https://code.google.com/p/chromium/issues/detail?id=226941
1911                 //    https://bugs.webkit.org/show_bug.cgi?id=114791
1912                 //    http://dev.ckeditor.com/ticket/9998
1913                 var child, parent = node.parentNode;
1914                 while (child = node.firstChild) {
1915                     parent.insertBefore(child, node);
1916                 }
1917                 parent.removeChild(node);
1918                 // chances are we had e.g.
1919                 //  <p>foo</p>
1920                 //  <p>bar</p>
1921                 // merged the lines getting this in webkit
1922                 //  <p>foo<span>bar</span></p>
1923                 // after unwrapping the span, we have 2 text nodes
1924                 //  <p>[foo][bar]</p>
1925                 // where we probably want only one. Normalize will merge
1926                 // adjacent text nodes. However, does not merge text and cdata
1927                 parent.normalize();
1928             }
1929         }
1930     }
1931 })();