'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 =>