summaryrefslogtreecommitdiff
path: root/addons/website/static/src/js/website.editor.js
diff options
context:
space:
mode:
Diffstat (limited to 'addons/website/static/src/js/website.editor.js')
-rw-r--r--addons/website/static/src/js/website.editor.js701
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;
+ }
})();