diff options
Diffstat (limited to 'addons/website/static/src/js/website.editor.js')
| -rw-r--r-- | addons/website/static/src/js/website.editor.js | 701 |
1 files changed, 438 insertions, 263 deletions
diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 74cf6ab81c0..51f14a0f6d5 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -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 () { @@ -14,12 +14,12 @@ $(document).on('click', 'a.js_link2post', function (ev) { ev.preventDefault(); - var form = document.createElement('form'); - form.action = this.pathname; // restrict to same origin - form.method = 'POST'; - // TODO: support this.search as form input fields - $(this).append(form); - form.submit(); + website.form(this.pathname, 'POST'); + }); + + $(document).on('submit', '.cke_editable form', function (ev) { + // Disable form submition in editable mode + ev.preventDefault(); }); $(document).on('hide.bs.dropdown', '.dropdown', function (ev) { @@ -32,11 +32,32 @@ }); }); + /** + * An editing host is an HTML element with @contenteditable=true, or the + * child of a document in designMode=on (but that one's not supported) + * + * https://dvcs.w3.org/hg/editing/raw-file/tip/editing.html#editing-host + */ + function is_editing_host(element) { + return element.getAttribute('contentEditable') === 'true'; + } + /** + * Checks that both the element's content *and the element itself* are + * editable: an editing host is considered non-editable because its content + * is editable but its attributes should not be considered editable + */ + function is_editable_node(element) { + return !(element.data('oe-model') === 'ir.ui.view' + || element.data('cke-realelement') + || is_editing_host(element) + || element.isReadOnly()); + } + function link_dialog(editor) { return new website.editor.RTELinkDialog(editor).appendTo(document.body); } - function image_dialog(editor) { - return new website.editor.RTEImageDialog(editor).appendTo(document.body); + function image_dialog(editor, image) { + return new website.editor.RTEImageDialog(editor, image).appendTo(document.body); } // only enable editors manually @@ -54,18 +75,13 @@ init: function (editor) { editor.on('doubleclick', function (evt) { var element = evt.data.element; - if (element.is('img') - && !element.data('cke-realelement') - && !element.isReadOnly() - && (element.data('oe-model') !== 'ir.ui.view')) { - image_dialog(editor); + if (element.is('img') && is_editable_node(element)) { + image_dialog(editor, element); return; } element = get_selected_link(editor) || evt.data.element; - if (element.isReadOnly() - || !element.is('a') - || element.data('oe-model')) { + if (!(element.is('a') && is_editable_node(element))) { return; } @@ -73,39 +89,6 @@ 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" 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, - }); - }); - //noinspection JSValidateTypes editor.addCommand('link', { exec: function (editor) { @@ -129,13 +112,11 @@ label: 'Link', command: 'link', toolbar: 'links,10', - icon: '/website/static/lib/ckeditor/plugins/link/icons/link.png', }); editor.ui.addButton('Image', { label: 'Image', command: 'image', toolbar: 'insert,10', - icon: '/website/static/lib/ckeditor/plugins/image/icons/image.png', }); editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link'); @@ -204,177 +185,186 @@ } }); - CKEDITOR.plugins.add('oeref', { - requires: 'widget', - - init: function (editor) { - editor.widgets.add('oeref', { - editables: { text: '*' }, - - upcast: function (el) { - return el.attributes['data-oe-type'] - && el.attributes['data-oe-type'] !== 'monetary'; - }, - }); - editor.widgets.add('monetary', { - editables: { text: 'span.oe_currency_value' }, - - upcast: function (el) { - return el.attributes['data-oe-type'] === 'monetary'; - } - }); - } - }); - - CKEDITOR.plugins.add('bootstrapcombo', { - requires: 'richcombo', - + CKEDITOR.plugins.add('linkstyle', { + requires: 'panelbutton,floatpanel', init: function (editor) { - var config = editor.config; - - editor.ui.addRichCombo('BootstrapLinkCombo', { - // default title - label: "Links", - // hover - title: "Link styling", - toolbar: 'styles,10', - allowedContent: ['a'], + var label = "Link Style"; + editor.ui.add('LinkStyle', CKEDITOR.UI_PANELBUTTON, { + label: label, + title: label, + icon: '/website/static/src/img/bglink.png', + modes: { wysiwyg: true }, + editorFocus: true, panel: { - css: [ - '/website/static/lib/bootstrap/css/bootstrap.css', - CKEDITOR.skin.getPath( 'editor' ) - ].concat( config.contentsCss ), - multiSelect: true, + css: '/website/static/lib/bootstrap/css/bootstrap.css', + attributes: { 'role': 'listbox', 'aria-label': label }, }, types: { - 'basic': 'btn-default', - 'primary': 'btn-primary', - 'success': 'btn-success', - 'info': 'btn-info', - 'warning': 'btn-warning', - 'danger': 'btn-danger', + 'btn-default': _t("Basic"), + 'btn-primary': _t("Primary"), + 'btn-success': _t("Success"), + 'btn-info': _t("Info"), + 'btn-warning': _t("Warning"), + 'btn-danger': _t("Danger"), }, - sizes: { - 'large': 'btn-lg', - 'default': '', - 'small': 'btn-sm', - 'extra small': 'btn-xs', + 'btn-xs': _t("Extra Small"), + 'btn-sm': _t("Small"), + '': _t("Default"), + 'btn-lg': _t("Large") }, - 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; + if (!(e = path.contains('a')) || e.isReadOnly()) { 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; }); + // no hook where button is available, so wait + // "some time" after render. setTimeout(function () { - // Because I can't find any normal hook where the - // bloody button's bloody element is available self.disable(); - }, 0); + }, 0) + }, + enable: function () { + this.setState(CKEDITOR.TRISTATE_OFF); + }, + disable: function () { + this.setState(CKEDITOR.TRISTATE_DISABLED); }, - 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; } + onOpen: function () { + var link = get_selected_link(editor); + var id = this._.id; + var block = this._.panel._.panel._.blocks[id]; + var $root = $(block.element.$); + $root.find('button').removeClass('active').removeProp('disabled'); + + // enable buttons matching link state + for (var type in this.types) { + if (!this.types.hasOwnProperty(type)) { continue; } + if (!link.hasClass(type)) { continue; } - this.mark(val); - break; + $root.find('button[data-type=types].' + type) + .addClass('active'); } - var found; - for(val in this.sizes) { - if (!this.sizes.hasOwnProperty(val)) { continue; } - cls = this.sizes[val]; - if (!cls || !this.element.hasClass(cls)) { continue; } - + for (var size in this.sizes) { + if (!this.sizes.hasOwnProperty(size)) { continue; } + if (!size || !link.hasClass(size)) { continue; } found = true; - this.mark(val); - break; + $root.find('button[data-type=sizes].' + size) + .addClass('active'); } - if (!found && this.element.hasClass('btn')) { - this.mark('default'); + if (!found && link.hasClass('btn')) { + $root.find('button[data-type="sizes"][data-set-class=""]') + .addClass('active'); } }, - onClick: function (value) { + + onBlock: function (panel, block) { + var self = this; + block.autoSize = true; + + var html = ['<div style="padding: 5px">']; + html.push('<div style="white-space: nowrap">'); + _(this.types).each(function (label, key) { + html.push(_.str.sprintf( + '<button type="button" class="btn %s" ' + + 'data-type="types" data-set-class="%s">%s</button>', + key, key, label)); + }); + html.push('</div>'); + html.push('<div style="white-space: nowrap; margin: 5px 0; text-align: center">'); + _(this.sizes).each(function (label, key) { + html.push(_.str.sprintf( + '<button type="button" class="btn btn-default %s" ' + + 'data-type="sizes" data-set-class="%s">%s</button>', + key, key, label)); + }); + html.push('</div>'); + html.push('<button type="button" class="btn btn-link btn-block" ' + + 'data-type="reset">Reset</button>'); + html.push('</div>'); + + block.element.setHtml(html.join(' ')); + var $panel = $(block.element.$); + $panel.on('click', 'button', function () { + self.clicked(this); + }); + }, + clicked: function (button) { editor.focus(); - editor.fire('saveShapshot'); + editor.fire('saveSnapshot'); - // basic btn setup - var el = this.element; - if (!el.hasClass('btn')) { - el.addClass('btn'); - el.addClass('btn-default'); + var $button = $(button), + $link = $(get_selected_link(editor).$); + if (!$link.hasClass('btn')) { + $link.addClass('btn 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); + switch($button.data('type')) { + case 'reset': + $link.removeClass('btn') + .removeClass(_.keys(this.types).join(' ')) + .removeClass(_.keys(this.sizes).join(' ')); + break; + case 'types': + $link.removeClass(_.keys(this.types).join(' ')) + .addClass($button.data('set-class')); + break; + case 'sizes': + $link.removeClass(_.keys(this.sizes).join(' ')) + .addClass($button.data('set-class')); } + this._.panel.hide(); - editor.fire('saveShapshot'); + editor.fire('saveSnapshot'); }, - 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); + + }); + } + }); + + CKEDITOR.plugins.add('oeref', { + requires: 'widget', + + init: function (editor) { + editor.widgets.add('oeref', { + editables: { text: '*' }, + draggable: false, + + upcast: function (el) { + var matches = el.attributes['data-oe-type'] && el.attributes['data-oe-type'] !== 'monetary'; + if (!matches) { return false; } + + if (el.attributes['data-oe-original']) { + while (el.children.length) { + el.children[0].remove(); + } + el.add(new CKEDITOR.htmlParser.text( + el.attributes['data-oe-original'] + )); } + return true; + }, + }); + editor.widgets.add('monetary', { + editables: { text: 'span.oe_currency_value' }, + draggable: false, + + upcast: function (el) { + return el.attributes['data-oe-type'] === 'monetary'; } }); - }, + } }); var editor = new website.EditorBar(); @@ -384,15 +374,15 @@ editor.edit(); } }); - $body.css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight()); }; - /* ----- TOP EDITOR BAR FOR ADMIN ---- */ + + /* ----- TOP EDITOR BAR FOR ADMIN ---- */ website.EditorBar = openerp.Widget.extend({ template: 'website.editorbar', events: { 'click button[data-action=edit]': 'edit', 'click button[data-action=save]': 'save', - 'click button[data-action=cancel]': 'cancel', + 'click a[data-action=cancel]': 'cancel', 'click a[data-action=new_page]': 'new_page', }, container: 'body', @@ -408,15 +398,15 @@ if (item.header) { menu.append('<li class="dropdown-header">' + item.name + '</li>'); } else { - menu.append(_.str.sprintf('<li role="presentation"><a href="#" data-view-id="%s" role="menuitem"><strong class="icon-check%s"></strong> %s</a></li>', - item.id, item.active ? '' : '-empty', item.name)); + 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>', + item.id, item.active ? '-check' : '', item.name)); } }); // Adding Static Menus 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#action=website.action_module_website">Install Apps</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'); } ); @@ -432,7 +422,6 @@ }, start: function() { var self = this; - this.saving_mutex = new openerp.Mutex(); this.$('#website-top-edit').hide(); @@ -450,13 +439,27 @@ this.rte = new website.RTE(this); this.rte.on('change', this, this.proxy('rte_changed')); this.rte.on('rte:ready', this, function () { + self.setup_hover_buttons(); self.trigger('rte:ready'); + self.check_height(); }); + $(window).on('resize', _.debounce(this.check_height.bind(this), 50)); + this.check_height(); + return $.when( this._super.apply(this, arguments), this.rte.appendTo(this.$('#website-top-edit .nav.pull-right')) - ); + ).then(function () { + self.check_height(); + }); + }, + check_height: function () { + var editor_height = this.$el.outerHeight(); + if (this.get('height') != editor_height) { + $(document.body).css('padding-top', editor_height); + this.set('height', editor_height); + } }, edit: function () { this.$buttons.edit.prop('disabled', true); @@ -464,7 +467,7 @@ this.$('#website-top-edit').show(); $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden"); - this.rte.start_edition(); + this.rte.start_edition().then(this.check_height.bind(this)); }, rte_changed: function () { this.$buttons.save.prop('disabled', false); @@ -513,16 +516,23 @@ 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'); - }) + }); }); }); }, @@ -540,7 +550,20 @@ }); }, cancel: function () { - website.reload(); + new $.Deferred(function (d) { + var $dialog = $(openerp.qweb.render('website.editor.discard')).appendTo(document.body); + $dialog.on('click', '.btn-danger', function () { + d.resolve(); + }).on('hidden.bs.modal', function () { + d.reject(); + }); + d.always(function () { + $dialog.remove(); + }); + $dialog.modal('show'); + }).then(function () { + website.reload(); + }) }, new_page: function (ev) { ev.preventDefault(); @@ -548,9 +571,100 @@ window_title: "New Page", input: "Page Title", }).then(function (val) { - document.location = '/pagenew/' + encodeURI(val); + if (val) { + document.location = '/pagenew/' + encodeURI(val); + } + }); + }, + + /** + * Creates a "hover" button for image and link edition + * + * @param {String} label the button's label + * @param {Function} editfn edition function, called when clicking the button + * @param {String} [classes] additional classes to set on the button + * @returns {jQuery} + */ + make_hover_button: function (label, editfn, classes) { + return $(openerp.qweb.render('website.editor.hoverbutton', { + label: label, + classes: classes, + })).hide().appendTo(document.body).click(function (e) { + e.preventDefault(); + e.stopPropagation(); + editfn.call(this, e); }); }, + /** + * For UI clarity, during RTE edition when the user hovers links and + * images a small button should appear to make the capability clear, + * as not all users think of double-clicking the image or link. + */ + setup_hover_buttons: function () { + var editor = this.rte.editor; + var $link_button = this.make_hover_button(_t("Edit Link"), function () { + var sel = new CKEDITOR.dom.element(previous); + editor.getSelection().selectElement(sel); + link_dialog(editor); + $link_button.hide(); + previous = null; + }, 'btn-xs'); + var $image_button = this.make_hover_button(_t("Edit Image"), function () { + image_dialog(editor, new CKEDITOR.dom.element(previous)); + $image_button.hide(); + previous = null; + }); + + // previous is the state of the button-trigger: it's the + // currently-ish hovered element which can trigger a button showing. + // -ish, because when moving to the button itself ``previous`` is + // still set to the element having triggered showing the button. + var previous; + $(editor.element.$).on('mouseover', 'a, img', function (e) { + // Back from edit button -> ignore + if (previous && previous === this) { return; } + + var selected = new CKEDITOR.dom.element(this); + if (!is_editable_node(selected)) { + return; + } + + previous = this; + var $selected = $(this); + var position = $selected.offset(); + if ($selected.is('img')) { + var image_top = position.top + parseInt($selected.css('marginTop'), 10); + var image_left = position.left + parseInt($selected.css('marginLeft'), 10); + $link_button.hide(); + // center button on image + $image_button.show().offset({ + top: $selected.outerHeight() / 2 + + image_top + - $image_button.outerHeight() / 2, + left: $selected.outerWidth() / 2 + + image_left + - $image_button.outerWidth() / 2, + }); + } else { + $image_button.hide(); + // put button below link, horizontally centered + $link_button.show().offset({ + top: $selected.outerHeight() + + position.top, + left: $selected.outerWidth() / 2 + + position.left + - $link_button.outerWidth() / 2 + }) + } + }).on('mouseleave', 'a, img', function (e) { + var current = document.elementFromPoint(e.clientX, e.clientY); + if (current === $link_button[0] || current === $image_button[0]) { + return; + } + $image_button.add($link_button).hide(); + previous = null; + }); + } }); var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(','); @@ -694,9 +808,11 @@ 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(); @@ -754,7 +870,7 @@ var plugins = [ 'a11yhelp', 'basicstyles', 'blockquote', 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab', - 'elementspath', 'enterkey', 'entities', 'filebrowser', + 'elementspath', /*'enterkey',*/ 'entities', 'filebrowser', 'find', 'floatingspace','format', 'htmlwriter', 'iframe', 'indentblock', 'indentlist', 'justify', 'list', 'pastefromword', 'pastetext', 'preview', @@ -781,19 +897,16 @@ fillEmptyBlocks: false, filebrowserImageUploadUrl: "/website/attach", // Support for sharedSpaces in 4.x - extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,bootstrapcombo', + extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref,linkstyle', // Place toolbar in controlled location sharedSpaces: { top: 'oe_rte_toolbar' }, toolbar: [{ - name: 'clipboard', items: [ - "Undo" - ]},{ name: 'basicstyles', items: [ "Bold", "Italic", "Underline", "Strike", "Subscript", "Superscript", "TextColor", "BGColor", "RemoveFormat" ]},{ name: 'span', items: [ - "Link", "Blockquote", "BulletedList", + "Link", "LinkStyle", "Blockquote", "BulletedList", "NumberedList", "Indent", "Outdent" ]},{ name: 'justify', items: [ @@ -803,7 +916,7 @@ "Image", "TableButton" ]},{ name: 'styles', items: [ - "Styles", "BootstrapLinkCombo" + "Styles" ]} ], // styles dropdown in toolbar @@ -827,6 +940,7 @@ events: { 'hidden.bs.modal': 'destroy', 'click button.save': 'save', + 'click button[data-dismiss="modal"]': 'cancel', }, init: function (editor) { this._super(); @@ -840,6 +954,8 @@ save: function () { this.close(); }, + cancel: function () { + }, close: function () { this.$el.modal('hide'); }, @@ -848,7 +964,7 @@ 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')) { @@ -856,7 +972,7 @@ 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) { @@ -865,22 +981,45 @@ }), 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.page_exists_req = null; + this.search_pages_req = null; }, start: function () { var self = this; - return $.when( - this.fetch_pages().done(this.proxy('fill_pages')), - this._super() - ).done(function () { - self.bind_data(); + this.$('#link-page').select2({ + minimumInputLength: 1, + placeholder: _t("New or existing page"), + query: function (q) { + $.when( + self.page_exists(q.term), + self.fetch_pages(q.term) + ).then(function (exists, results) { + var rs = _.map(results, function (r) { + return { id: r.url, text: r.name, }; + }); + if (!exists) { + rs.push({ + create: true, + id: q.term, + text: _.str.sprintf(_t("Create page '%s'"), q.term), + }); + } + q.callback({ + more: false, + results: rs + }); + }, function () { + q.callback({more: false, results: []}); + }); + }, }); + 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 @@ -892,15 +1031,18 @@ 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); - }); + } 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')); } @@ -923,13 +1065,6 @@ var match, $control; 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); @@ -941,30 +1076,38 @@ 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', { + call: function (method, args, kwargs) { + var self = this; + var req = method + '_req'; + + if (this[req]) { this[req].abort(); } + + return this[req] = openerp.jsonRpc('/web/dataset/call_kw', 'call', { model: 'website', - method: 'list_pages', - args: [null], - kwargs: { - context: website.get_context() - }, + method: method, + args: args, + kwargs: kwargs, + }).always(function () { + 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); + page_exists: function (term) { + return this.call('page_exists', [null, term], { + context: website.get_context(), + }); + }, + fetch_pages: function (term) { + return this.call('search_pages', [null, term], { + limit: 9, + context: website.get_context(), }); }, }); @@ -1110,6 +1253,9 @@ }); return this._super(); }, + cancel: function () { + this.trigger('cancel'); + }, /** * Sets the provided image url as the dialog's value-to-save and @@ -1159,32 +1305,37 @@ }, }); website.editor.RTEImageDialog = website.editor.ImageDialog.extend({ - init: function () { - this._super.apply(this, arguments); + init: function (editor, image) { + this._super(editor); + + this.element = image; this.on('start', this, this.proxy('started')); this.on('save', this, this.proxy('saved')); }, started: function (holder) { - var selection = this.editor.getSelection(); - var el = selection && selection.getSelectedElement(); - this.element = null; - - if (el && el.is('img')) { - this.element = el; - _(this.image_styles).each(function (style) { - if (el.hasClass(style)) { - holder.style = style; - } - }); - holder.url = el.getAttribute('src'); + if (!this.element) { + var selection = this.editor.getSelection(); + this.element = selection && selection.getSelectedElement(); } + + var el = this.element; + if (!el || !el.is('img')) { + return; + } + _(this.image_styles).each(function (style) { + if (el.hasClass(style)) { + holder.style = style; + } + }); + holder.url = el.getAttribute('src'); }, saved: function (data) { var element, editor = this.editor; 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 => @@ -1314,6 +1465,11 @@ // a/@href, ...) _(mutations).chain() .filter(function (m) { + // ignore any change related to mundane image-edit-button + if (m.target && m.target.className + && m.target.className.indexOf('image-edit-button') !== -1) { + return false; + } switch(m.type) { case 'attributes': // ignore .cke_focus being added or removed // if attribute is not a class, can't be .cke_focus change @@ -1327,8 +1483,11 @@ // ignore mutation if the *only* change is .cke_focus return change.length !== 1 || change[0] === 'cke_focus'; case 'childList': - // <br type="_moz"> appears when focusing RTE in FF, ignore - return m.addedNodes.length !== 1 || m.addedNodes[0].nodeName !== 'BR'; + // Remove ignorable nodes from addedNodes or removedNodes, + // if either set remains non-empty it's considered to be an + // impactful change. Otherwise it's ignored. + return !!remove_mundane_nodes(m.addedNodes).length || + !!remove_mundane_nodes(m.removedNodes).length; default: return true; } @@ -1338,11 +1497,27 @@ while (node && !$(node).hasClass('oe_editable')) { node = node.parentNode; } - $(m.target).trigger('node_changed'); return node; }) .compact() .uniq() .each(function (node) { $(node).trigger('content_changed'); }) }); + function remove_mundane_nodes(nodes) { + if (!nodes || !nodes.length) { return []; } + + var output = []; + for(var i=0; i<nodes.length; ++i) { + var node = nodes[i]; + if (node.nodeType === document.ELEMENT_NODE) { + if (node.nodeName === 'BR' && node.getAttribute('type') === '_moz') { + // <br type="_moz"> appears when focusing RTE in FF, ignore + continue; + } + } + + output.push(node); + } + return output; + } })(); |
