[IMP] website editor: better rendering of the popover when failure write raise by...
[odoo/odoo.git] / addons / website / static / src / js / website.editor.js
index 2019b29..30af6fd 100644 (file)
@@ -2,7 +2,7 @@
     'use strict';
 
     var website = openerp.website;
-    // $.fn.data automatically parses value, '0'|'1' -> 0|1
+    var _t = openerp._t;
 
     website.add_template_file('/website/static/src/xml/website.editor.xml');
     website.dom_ready.done(function () {
         if (!is_smartphone) {
             website.ready().then(website.init_editor);
         }
+
+        $(document).on('click', 'a.js_link2post', function (ev) {
+            ev.preventDefault();
+            website.form(this.pathname, 'POST');
+        });
+
+        $(document).on('hide.bs.dropdown', '.dropdown', function (ev) {
+            // Prevent dropdown closing when a contenteditable children is focused
+            if (ev.originalEvent
+                    && $(ev.target).has(ev.originalEvent.target).length
+                    && $(ev.originalEvent.target).is('[contenteditable]')) {
+                ev.preventDefault();
+            }
+        });
     });
 
     function link_dialog(editor) {
                     link_dialog(editor);
                 }, null, null, 500);
 
+                var previousSelection;
+                editor.on('selectionChange', function (evt) {
+                    var selected = evt.data.path.lastElement;
+                    if (previousSelection) {
+                        // cleanup previous selection
+                        $(previousSelection).next().remove();
+                        previousSelection = null;
+                    }
+                    if (!selected.is('img')
+                            || selected.data('cke-realelement')
+                            || selected.isReadOnly()
+                            || selected.data('oe-model') === 'ir.ui.view') {
+                        return;
+                    }
+
+                    // display button
+                    var $el = $(previousSelection = selected.$);
+                    var $btn = $('<button type="button" class="btn btn-primary image-edit-button" contenteditable="false">Edit</button>')
+                        .insertAfter($el)
+                        .click(function (e) {
+                            e.preventDefault();
+                            e.stopPropagation();
+                            image_dialog(editor);
+                        });
+
+                    var position = $el.position();
+                    $btn.css({
+                        position: 'absolute',
+                        top: $el.height() / 2 + position.top - $btn.outerHeight() / 2,
+                        left: $el.width() / 2 + position.left - $btn.outerWidth() / 2,
+                    });
+                });
+                editor.on('destroy', function (evt) {
+                    if (previousSelection) {
+                        $('.image-edit-button').remove();
+                    }
+                });
+
                 //noinspection JSValidateTypes
                 editor.addCommand('link', {
                     exec: function (editor) {
             }
         });
 
+        CKEDITOR.plugins.add('bootstrapcombo', {
+            requires: 'richcombo',
+
+            init: function (editor) {
+                var config = editor.config;
+
+                editor.ui.addRichCombo('BootstrapLinkCombo', {
+                    // default title
+                    label: "Links",
+                    // hover
+                    title: "Link styling",
+                    toolbar: 'styles,10',
+                    allowedContent: ['a'],
+
+                    panel: {
+                                           css: [
+                            '/website/static/lib/bootstrap/css/bootstrap.css',
+                            CKEDITOR.skin.getPath( 'editor' )
+                        ].concat( config.contentsCss ),
+                        multiSelect: true,
+                    },
+
+                    types: {
+                        'basic': 'btn-default',
+                        'primary': 'btn-primary',
+                        'success': 'btn-success',
+                        'info': 'btn-info',
+                        'warning': 'btn-warning',
+                        'danger': 'btn-danger',
+                    },
+
+                    sizes: {
+                        'large': 'btn-lg',
+                        'default': '',
+                        'small': 'btn-sm',
+                        'extra small': 'btn-xs',
+                    },
+
+                    init: function () {
+                        this.add('', 'Reset');
+                        this.startGroup("Types");
+                        for(var type in this.types) {
+                            if (!this.types.hasOwnProperty(type)) { continue; }
+                            var cls = this.types[type];
+                            var el = _.str.sprintf(
+                                '<span class="btn %s">%s</span>',
+                                cls, type);
+                            this.add(type, el);
+                        }
+                        this.startGroup("Sizes");
+                        for (var size in this.sizes) {
+                            if (!this.sizes.hasOwnProperty(size)) { continue; }
+                            cls = this.sizes[size];
+
+                            el = _.str.sprintf(
+                                '<span class="btn btn-default %s">%s</span>',
+                                cls, size);
+                            this.add(size, el);
+                        }
+                        this.commit();
+                    },
+                    onRender: function () {
+                        var self = this;
+                        editor.on('selectionChange', function (e) {
+                            var path = e.data.path, el;
+
+                            if (!(el = path.contains('a'))) {
+                                self.element = null;
+                                self.disable();
+                                return;
+                            }
+
+                            self.enable();
+                            // This is crap, but getting the currently selected
+                            // element from within onOpen absolutely does not
+                            // work, so store the "current" element in the
+                            // widget instead
+                            self.element = el;
+                        });
+                        setTimeout(function () {
+                            // Because I can't find any normal hook where the
+                            // bloody button's bloody element is available
+                            self.disable();
+                        }, 0);
+                    },
+                    onOpen: function () {
+                        this.showAll();
+                        this.unmarkAll();
+
+                        for(var val in this.types) {
+                            if (!this.types.hasOwnProperty(val)) { continue; }
+                            var cls = this.types[val];
+                            if (!this.element.hasClass(cls)) { continue; }
+
+                            this.mark(val);
+                            break;
+                        }
+
+                        var found;
+                        for(val in this.sizes) {
+                            if (!this.sizes.hasOwnProperty(val)) { continue; }
+                            cls = this.sizes[val];
+                            if (!cls || !this.element.hasClass(cls)) { continue; }
+
+                            found = true;
+                            this.mark(val);
+                            break;
+                        }
+                        if (!found && this.element.hasClass('btn')) {
+                            this.mark('default');
+                        }
+                    },
+                    onClick: function (value) {
+                        editor.focus();
+                        editor.fire('saveShapshot');
+
+                        // basic btn setup
+                        var el = this.element;
+                        if (!el.hasClass('btn')) {
+                            el.addClass('btn');
+                            el.addClass('btn-default');
+                        }
+
+                        if (!value) {
+                            this.setClass(this.types);
+                            this.setClass(this.sizes);
+                            el.removeClass('btn');
+                        } else if (value in this.types) {
+                            this.setClass(this.types, value);
+                        } else if (value in this.sizes) {
+                            this.setClass(this.sizes, value);
+                        }
+
+                        editor.fire('saveShapshot');
+                    },
+                    setClass: function (classMap, value) {
+                        var element = this.element;
+                        _(classMap).each(function (cls) {
+                            if (!cls) { return; }
+                            element.removeClass(cls);
+                        }.bind(this));
+
+                        var cls = classMap[value];
+                        if (cls) {
+                            element.addClass(cls);
+                        }
+                    }
+                });
+            },
+        });
+
         var editor = new website.EditorBar();
         var $body = $(document.body);
         editor.prependTo($body).then(function () {
             'click button[data-action=edit]': 'edit',
             'click button[data-action=save]': 'save',
             'click button[data-action=cancel]': 'cancel',
+            'click a[data-action=new_page]': 'new_page',
         },
         container: 'body',
         customize_setup: function() {
                             }
                         });
                         // Adding Static Menus
-                        menu.append('<li class="divider"></li><li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
-                        menu.append('<li class="divider"></li><li><a data-action="ace" href="#">Advanced view editor</a></li>');
+                        menu.append('<li class="divider"></li>');
+                       menu.append('<li><a data-action="ace" href="#">HTML Editor</a></li>');
+                        menu.append('<li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
+                        menu.append('<li><a href="/web#return_label=Website&action=website.action_module_website">Install Apps</a></li>');
                         self.trigger('rte:customize_menu_ready');
                     }
                 );
                     if (!_.isArray(failed)) { failed = [failed]; }
 
                     _(failed).each(function (failure) {
+                        var html = failure.error.exception_type === "except_osv";
+                        if (html) {
+                            var msg = $("<div/>").text(failure.error.message).html();
+                            var data = msg.substring(3,msg.length-2).split(/', u'/);
+                            failure.error.message = '<b>' + data[0] + '</b><br/>' + data[1];
+                        }
                         $(root).find('.' + failure.id)
                             .removeClass(failure.id)
                             .popover({
+                                html: html,
                                 trigger: 'hover',
                                 content: failure.error.message,
                                 placement: 'auto top',
                             })
                             // Force-show popovers so users will notice them.
                             .popover('show');
-                    })
+                    });
                 });
             });
         },
         cancel: function () {
             website.reload();
         },
+        new_page: function (ev) {
+            ev.preventDefault();
+            website.prompt({
+                window_title: "New Page",
+                input: "Page Title",
+            }).then(function (val) {
+                document.location = '/pagenew/' + encodeURI(val);
+            });
+        },
     });
 
     var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(',');
 
                 self.setup_editables(root);
 
-                // disable firefox's broken table resizing thing
-                document.execCommand("enableObjectResizing", false, "false");
-                document.execCommand("enableInlineTableEditing", false, "false");
+                try {
+                    // disable firefox's broken table resizing thing
+                    document.execCommand("enableObjectResizing", false, "false");
+                    document.execCommand("enableInlineTableEditing", false, "false");
+                } catch (e) {}
 
                 self.trigger('rte:ready');
                 def.resolve();
                 fillEmptyBlocks: false,
                 filebrowserImageUploadUrl: "/website/attach",
                 // Support for sharedSpaces in 4.x
-                extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref',
+                extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,bootstrapcombo',
                 // Place toolbar in controlled location
                 sharedSpaces: { top: 'oe_rte_toolbar' },
                 toolbar: [{
                         "Image", "TableButton"
                     ]},{
                     name: 'styles', items: [
-                        "Styles"
+                        "Styles", "BootstrapLinkCombo"
                     ]}
                 ],
                 // styles dropdown in toolbar
                     {name: "Heading 5", element: 'h5'},
                     {name: "Heading 6", element: 'h6'},
                     {name: "Formatted", element: 'pre'},
-                    {name: "Address", element: 'address'},
-                    // emphasis
-                    {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
-                    {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
-                    {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
-                    {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
-                    {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
-                    {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
+                    {name: "Address", element: 'address'}
                 ],
             };
         },
     website.editor.LinkDialog = website.editor.Dialog.extend({
         template: 'website.editor.dialog.link',
         events: _.extend({}, website.editor.Dialog.prototype.events, {
-            'change .url-source': function (e) { this.changed($(e.target)); },
+            'change :input.url-source': function (e) { this.changed($(e.target)); },
             'mousedown': function (e) {
                 var $target = $(e.target).closest('.list-group-item');
                 if (!$target.length || $target.hasClass('active')) {
                     return;
                 }
 
-                this.changed($target.find('.url-source'));
+                this.changed($target.find('.url-source').filter(':input'));
             },
             'click button.remove': 'remove_link',
             'change input#link-text': function (e) {
         }),
         init: function (editor) {
             this._super(editor);
-            // url -> name mapping for existing pages
-            this.pages = Object.create(null);
             this.text = null;
+            // Store last-performed request to be able to cancel/abort it.
+            this.req = null;
         },
         start: function () {
-            return $.when(
-                this.fetch_pages().done(this.proxy('fill_pages')),
-                this.fetch_menus().done(this.proxy('fill_menus')),
-                this._super()
-            ).done(this.proxy('bind_data'));
+            var self = this;
+            this.$('#link-page').select2({
+                minimumInputLength: 3,
+                placeholder: _t("New or existing page"),
+                query: function (q) {
+                    // FIXME: out-of-order, abort
+                    self.fetch_pages(q.term).then(function (results) {
+                        var rs = _.map(results, function (r) {
+                            return { id: r.url, text: r.name, };
+                        });
+                        rs.push({
+                            create: true,
+                            id: q.term,
+                            text: _.str.sprintf(_t("Create page '%s'"), q.term),
+                        });
+                        q.callback({
+                            more: false,
+                            results: rs
+                        });
+                    });
+                },
+            });
+            return this._super().then(this.proxy('bind_data'));
         },
         save: function () {
             var self = this, _super = this._super.bind(this);
-            var $e = this.$('.list-group-item.active .url-source');
+            var $e = this.$('.list-group-item.active .url-source').filter(':input');
             var val = $e.val();
             if (!val || !$e[0].checkValidity()) {
                 // FIXME: error message
                 $e.closest('.form-group').addClass('has-error');
+                $e.focus();
                 return;
             }
 
             var done = $.when();
             if ($e.hasClass('email-address')) {
                 this.make_link('mailto:' + val, false, val);
-            } else if ($e.hasClass('existing')) {
-                self.make_link(val, false, this.pages[val]);
-            } else if ($e.hasClass('pages')) {
-                // Create the page, get the URL back
-                done = $.get(_.str.sprintf(
-                        '/pagenew/%s?noredirect', encodeURI(val)))
-                    .then(function (response) {
-                        self.make_link(response, false, val);
-                        var parent_id = self.$('.add-to-menu').val();
-                        if (parent_id) {
-                            return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
-                                model: 'website.menu',
-                                method: 'create',
-                                args: [{
-                                    'name': val,
-                                    'url': response,
-                                    'sequence': 0, // TODO: better tree widget
-                                    'website_id': website.id,
-                                    'parent_id': parent_id|0,
-                                }],
-                                kwargs: {
-                                    context: website.get_context()
-                                },
-                            });
-                        }
-                    });
+            } else if ($e.hasClass('page')) {
+                var data = $e.select2('data');
+                if (!data.create) {
+                    self.make_link(data.id, false, data.text);
+                } else {
+                    // Create the page, get the URL back
+                    done = $.get(_.str.sprintf(
+                            '/pagenew/%s?noredirect', encodeURI(data.id)))
+                        .then(function (response) {
+                            self.make_link(response, false, data.id);
+                        });
+                }
             } else {
                 this.make_link(val, this.$('input.window-new').prop('checked'));
             }
         },
         make_link: function (url, new_window, label) {
         },
-        bind_data: function () {
-            var href = this.element && (this.element.data( 'cke-saved-href')
+        bind_data: function (text, href, new_window) {
+            href = href || this.element && (this.element.data( 'cke-saved-href')
                                     ||  this.element.getAttribute('href'));
             if (!href) { return; }
 
+            if (new_window === undefined) {
+                new_window = this.element.getAttribute('target') === '_blank';
+            }
+            if (text === undefined) {
+                text = this.element.getText();
+            }
+
             var match, $control;
-            if (match = /mailto:(.+)/.exec(href)) {
+            if ((match = /mailto:(.+)/.exec(href))) {
                 $control = this.$('input.email-address').val(match[1]);
-            } else if (href in this.pages) {
-                $control = this.$('select.existing').val(href);
-            } else if (match = /\/page\/(.+)/.exec(href)) {
-                var actual_href = '/page/website.' + match[1];
-                if (actual_href in this.pages) {
-                    $control = this.$('select.existing').val(actual_href);
-                }
             }
             if (!$control) {
                 $control = this.$('input.url').val(href);
 
             this.changed($control);
 
-            this.$('input#link-text').val(this.element.getText());
-            this.$('input.window-new').prop(
-                'checked', this.element.getAttribute('target') === '_blank');
+            this.$('input#link-text').val(text);
+            this.$('input.window-new').prop('checked', new_window);
         },
         changed: function ($e) {
-            this.$('.url-source').not($e).val('');
+            this.$('.url-source').filter(':input').not($e).val('')
+                    .filter(function () { return !!$(this).data('select2'); })
+                    .select2('data', null);
             $e.closest('.list-group-item')
                 .addClass('active')
                 .siblings().removeClass('active')
                 .addBack().removeClass('has-error');
         },
-        fetch_pages: function () {
-            return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
+        fetch_pages: function (term) {
+            var self = this;
+            if (this.req) { this.req.abort(); }
+            return this.req = openerp.jsonRpc('/web/dataset/call_kw', 'call', {
                 model: 'website',
-                method: 'list_pages',
-                args: [null],
+                method: 'search_pages',
+                args: [null, term],
                 kwargs: {
+                    limit: 9,
                     context: website.get_context()
                 },
+            }).done(function () {
+                // request completed successfully -> unstore it
+                self.req = null;
             });
         },
-        fill_pages: function (results) {
-            var self = this;
-            var pages = this.$('select.existing')[0];
-            _(results).each(function (result) {
-                self.pages[result.url] = result.name;
-
-                pages.options[pages.options.length] =
-                        new Option(result.name, result.url);
-            });
-        },
-        fetch_menus: function () {
-            var context = website.get_context();
-            return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
-                model: 'website.menu',
-                method: 'get_tree',
-                args: [[context.website_id]],
-                kwargs: {
-                    context: context
-                },
-            });
-        },
-        fill_menus: function (tree) {
-            var self = this;
-            var menus = this.$('select.add-to-menu')[0];
-            var process_tree = function(node) {
-                var name = (new Array(node.level + 1).join('|-')) + ' ' + node.name;
-                menus.options[menus.options.length] = new Option(name, node.id);
-                node.children.forEach(function (child) {
-                    process_tree(child);
-                });
-            };
-            process_tree(tree);
-        },
     });
     website.editor.RTELinkDialog = website.editor.LinkDialog.extend({
         start: function () {
         }),
 
         start: function () {
+            this.$('.modal-footer [disabled]').text("Uploading…");
             var $options = this.$('.image-style').children();
             this.image_styles = $options.map(function () { return this.value; }).get();
 
         },
 
         file_selection: function () {
+            this.$el.addClass('nosave');
             this.$('button.filepicker').removeClass('btn-danger btn-success');
 
             var self = this;
             this.set_image(url);
         },
         preview_image: function () {
+            this.$el.removeClass('nosave');
             var image = this.$('input.url').val();
             if (!image) { return; }
 
             if (!(element = this.element)) {
                 element = editor.document.createElement('img');
                 element.addClass('img');
+                element.addClass('img-responsive');
                 // focus event handler interactions between bootstrap (modal)
                 // and ckeditor (RTE) lead to blowing the stack in Safari and
                 // Chrome (but not FF) when this is done synchronously =>