4 var website = openerp.website;
5 // $.fn.data automatically parses value, '0'|'1' -> 0|1
7 website.templates.push('/website/static/src/xml/website.editor.xml');
8 website.dom_ready.done(function () {
9 var is_smartphone = $(document.body)[0].clientWidth < 767;
12 website.ready().then(website.init_editor);
16 function link_dialog(editor) {
17 return new website.editor.LinkDialog(editor).appendTo(document.body);
19 function image_dialog(editor) {
20 return new website.editor.RTEImageDialog(editor).appendTo(document.body);
23 // only enable editors manually
24 CKEDITOR.disableAutoInline = true;
25 // EDIT ALL THE THINGS
26 CKEDITOR.dtd.$editable = $.extend(
27 {}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline);
28 // Disable removal of empty elements on CKEDITOR activation. Empty
29 // elements are used for e.g. support of FontAwesome icons
30 CKEDITOR.dtd.$removeEmpty = {};
32 website.init_editor = function () {
33 CKEDITOR.plugins.add('customdialogs', {
34 // requires: 'link,image',
35 init: function (editor) {
36 editor.on('doubleclick', function (evt) {
37 var element = evt.data.element;
39 && !element.data('cke-realelement')
40 && !element.isReadOnly()
41 && (element.data('oe-model') !== 'ir.ui.view')) {
46 element = get_selected_link(editor) || evt.data.element;
47 if (element.isReadOnly()
49 || element.data('oe-model')) {
53 editor.getSelection().selectElement(element);
57 //noinspection JSValidateTypes
58 editor.addCommand('link', {
59 exec: function (editor) {
66 //noinspection JSValidateTypes
67 editor.addCommand('image', {
68 exec: function (editor) {
76 editor.ui.addButton('Link', {
80 icon: '/website/static/lib/ckeditor/plugins/link/icons/link.png',
82 editor.ui.addButton('Image', {
86 icon: '/website/static/lib/ckeditor/plugins/image/icons/image.png',
89 editor.setKeystroke(CKEDITOR.CTRL + 76 /*L*/, 'link');
92 CKEDITOR.plugins.add( 'tablebutton', {
93 requires: 'panelbutton,floatpanel',
94 init: function( editor ) {
97 editor.ui.add('TableButton', CKEDITOR.UI_PANELBUTTON, {
100 // use existing 'table' icon
102 modes: { wysiwyg: true },
104 // panel opens in iframe, @css is CSS file <link>-ed within
105 // frame document, @attributes are set on iframe itself.
107 css: '/website/static/src/css/editor.css',
108 attributes: { 'role': 'listbox', 'aria-label': label, },
111 onBlock: function (panel, block) {
112 block.autoSize = true;
113 block.element.setHtml(openerp.qweb.render('website.editor.table.panel', {
118 var $table = $(block.element.$).on('mouseenter', 'td', function (e) {
119 var $e = $(e.target);
120 var y = $e.index() + 1;
121 var x = $e.closest('tr').index() + 1;
124 .find('td').removeClass('selected').end()
125 .find('tr:lt(' + String(x) + ')')
126 .children().filter(function () { return $(this).index() < y; })
127 .addClass('selected');
128 }).on('click', 'td', function (e) {
129 var $e = $(e.target);
131 //noinspection JSPotentiallyInvalidConstructorUsage
132 var table = new CKEDITOR.dom.element(
133 $(openerp.qweb.render('website.editor.table', {
134 rows: $e.closest('tr').index() + 1,
135 cols: $e.index() + 1,
138 editor.insertElement(table);
139 setTimeout(function () {
140 //noinspection JSPotentiallyInvalidConstructorUsage
141 var firstCell = new CKEDITOR.dom.element(table.$.rows[0].cells[0]);
142 var range = editor.createRange();
143 range.moveToPosition(firstCell, CKEDITOR.POSITION_AFTER_START);
148 block.element.getDocument().getBody().setStyle('overflow', 'hidden');
149 CKEDITOR.ui.fire('ready', this);
155 CKEDITOR.plugins.add('oeref', {
158 init: function (editor) {
159 editor.widgets.add('oeref', {
160 editables: { text: '*' },
162 upcast: function (el) {
163 return el.attributes['data-oe-type']
164 && el.attributes['data-oe-type'] !== 'monetary';
167 editor.widgets.add('monetary', {
168 editables: { text: 'span.oe_currency_value' },
170 upcast: function (el) {
171 return el.attributes['data-oe-type'] === 'monetary';
177 var editor = new website.EditorBar();
178 var $body = $(document.body);
179 editor.prependTo($body).then(function () {
180 if (location.search.indexOf("enable_editor") >= 0) {
184 $body.css('padding-top', '50px'); // Not working properly: editor.$el.outerHeight());
186 /* ----- TOP EDITOR BAR FOR ADMIN ---- */
187 website.EditorBar = openerp.Widget.extend({
188 template: 'website.editorbar',
190 'click button[data-action=edit]': 'edit',
191 'click button[data-action=save]': 'save',
192 'click button[data-action=cancel]': 'cancel',
195 customize_setup: function() {
197 var view_name = $(document.documentElement).data('view-xmlid');
198 var menu = $('#customize-menu');
199 this.$('#customize-menu-button').click(function(event) {
201 openerp.jsonRpc('/website/customize_template_get', 'call', { 'xml_id': view_name }).then(
203 _.each(result, function (item) {
205 menu.append('<li class="dropdown-header">' + item.name + '</li>');
207 menu.append(_.str.sprintf('<li role="presentation"><a href="#" data-view-id="%s" role="menuitem"><strong class="icon-check%s"></strong> %s</a></li>',
208 item.id, item.active ? '' : '-empty', item.name));
211 // Adding Static Menus
212 menu.append('<li class="divider"></li><li class="js_change_theme"><a href="/page/website.themes">Change Theme</a></li>');
213 menu.append('<li class="divider"></li><li><a data-action="ace" href="#">Advanced view editor</a></li>');
214 self.trigger('rte:customize_menu_ready');
218 menu.on('click', 'a[data-action!=ace]', function (event) {
219 var view_id = $(event.currentTarget).data('view-id');
220 openerp.jsonRpc('/website/customize_template_toggle', 'call', {
222 }).then( function() {
223 window.location.reload();
230 this.saving_mutex = new openerp.Mutex();
232 this.$('#website-top-edit').hide();
233 this.$('#website-top-view').show();
235 $('.dropdown-toggle').dropdown();
236 this.customize_setup();
239 edit: this.$('button[data-action=edit]'),
240 save: this.$('button[data-action=save]'),
241 cancel: this.$('button[data-action=cancel]'),
244 this.rte = new website.RTE(this);
245 this.rte.on('change', this, this.proxy('rte_changed'));
246 this.rte.on('rte:ready', this, function () {
247 self.trigger('rte:ready');
251 this._super.apply(this, arguments),
252 this.rte.appendTo(this.$('#website-top-edit .nav.pull-right'))
256 this.$buttons.edit.prop('disabled', true);
257 this.$('#website-top-view').hide();
258 this.$('#website-top-edit').show();
259 $('.css_non_editable_mode_hidden').removeClass("css_non_editable_mode_hidden");
261 this.rte.start_edition();
263 rte_changed: function () {
264 this.$buttons.save.prop('disabled', false);
269 observer.disconnect();
270 var editor = this.rte.editor;
271 var root = editor.element.$;
273 // FIXME: select editables then filter by dirty?
274 var defs = this.rte.fetch_editables(root)
276 .removeAttr('contentEditable')
277 .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger')
280 // TODO: Add a queue with concurrency limit in webclient
281 // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js
282 return self.saving_mutex.exec(function () {
283 return self.saveElement($el)
284 .then(undefined, function (thing, response) {
285 // because ckeditor regenerates all the dom,
286 // we can't just setup the popover here as
287 // everything will be destroyed by the DOM
288 // regeneration. Add markings instead, and
289 // returns a new rejection with all relevant
291 var id = _.uniqueId('carlos_danger_');
292 $el.addClass('oe_dirty oe_carlos_danger');
294 return $.Deferred().reject({
296 error: response.data,
301 return $.when.apply(null, defs).then(function () {
303 }, function (failed) {
304 // If there were errors, re-enable edition
305 self.rte.start_edition(true).then(function () {
306 // jquery's deferred being a pain in the ass
307 if (!_.isArray(failed)) { failed = [failed]; }
309 _(failed).each(function (failure) {
310 $(root).find('.' + failure.id)
311 .removeClass(failure.id)
314 content: failure.error.message,
315 placement: 'auto top',
317 // Force-show popovers so users will notice them.
324 * Saves an RTE content, which always corresponds to a view section (?).
326 saveElement: function ($el) {
327 var markup = $el.prop('outerHTML');
328 return openerp.jsonRpc('/web/dataset/call', 'call', {
331 args: [$el.data('oe-id'), markup,
332 $el.data('oe-xpath') || null,
333 website.get_context()],
336 cancel: function () {
341 var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(',');
342 /* ----- RICH TEXT EDITOR ---- */
343 website.RTE = openerp.Widget.extend({
345 id: 'oe_rte_toolbar',
346 className: 'oe_right oe_rte_toolbar',
347 // editor.ui.items -> possible commands &al
348 // editor.applyStyle(new CKEDITOR.style({element: "span",styles: {color: "#(color)"},overrides: [{element: "font",attributes: {color: null}}]}, {color: '#ff0000'}));
350 init: function (EditorBar) {
351 this.EditorBar = EditorBar;
352 this._super.apply(this, arguments);
356 * In Webkit-based browsers, triple-click will select a paragraph up to
357 * the start of the next "paragraph" including any empty space
358 * inbetween. When said paragraph is removed or altered, it nukes
359 * the empty space and brings part of the content of the next
360 * "paragraph" (which may well be e.g. an image) into the current one,
361 * completely fucking up layouts and breaking snippets.
363 * Try to fuck around with selections on triple-click to attempt to
364 * fix this garbage behavior.
366 * Note: for consistent behavior we may actually want to take over
367 * triple-clicks, in all browsers in order to ensure consistent cross-
368 * platform behavior instead of being at the mercy of rendering engines
369 * & platform selection quirks?
371 webkitSelectionFixer: function (root) {
372 root.addEventListener('click', function (e) {
373 // only webkit seems to have a fucked up behavior, ignore others
374 // FIXME: $.browser goes away in jquery 1.9...
375 if (!$.browser.webkit) { return; }
376 // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents
377 // The detail attribute indicates the number of times a mouse button has been pressed
378 // we just want the triple click
379 if (e.detail !== 3) { return; }
382 // Get closest block-level element to the triple-clicked
383 // element (using ckeditor's block list because why not)
384 var $closest_block = $(e.target).closest(blocks_selector);
386 // manually set selection range to the content of the
387 // triple-clicked block-level element, to avoid crossing over
388 // between block-level elements
389 document.getSelection().selectAllChildren($closest_block[0]);
392 tableNavigation: function (root) {
394 $(root).on('keydown', function (e) {
396 if (e.which !== 9) { return; }
398 if (self.handleTab(e)) {
404 * Performs whatever operation is necessary on a [TAB] hit, returns
405 * ``true`` if the event's default should be cancelled (if the TAB was
406 * handled by the function)
408 handleTab: function (event) {
409 var forward = !event.shiftKey;
411 var root = window.getSelection().getRangeAt(0).commonAncestorContainer;
412 var $cell = $(root).closest('td,th');
414 if (!$cell.length) { return false; }
418 // find cell in same row
419 var row = cell.parentNode;
420 var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)];
422 document.getSelection().selectAllChildren(sibling);
426 // find cell in previous/next row
427 var table = row.parentNode;
428 var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)];
430 var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1];
431 document.getSelection().selectAllChildren(new_cell);
435 // at edge cells, copy word/openoffice behavior: if going backwards
436 // from first cell do nothing, if going forwards from last cell add
439 var row_size = row.cells.length;
440 var new_row = document.createElement('tr');
442 var newcell = document.createElement('td');
444 newcell.textContent = '\u200B';
445 new_row.appendChild(newcell);
447 table.appendChild(new_row);
448 document.getSelection().selectAllChildren(new_row.cells[0]);
454 * Makes the page editable
456 * @param {Boolean} [restart=false] in case the edition was already set
457 * up once and is being re-enabled.
458 * @returns {$.Deferred} deferred indicating when the RTE is ready
460 start_edition: function (restart) {
462 // create a single editor for the whole page
463 var root = document.getElementById('wrapwrap');
465 $(root).on('dragstart', 'img', function (e) {
468 this.webkitSelectionFixer(root);
469 this.tableNavigation(root);
471 var def = $.Deferred();
472 var editor = this.editor = CKEDITOR.inline(root, self._config());
473 editor.on('instanceReady', function () {
474 editor.setReadOnly(false);
475 // ckeditor set root to editable, disable it (only inner
476 // sections are editable)
477 // FIXME: are there cases where the whole editor is editable?
478 editor.editable().setReadOnly(true);
480 self.setup_editables(root);
482 // disable firefox's broken table resizing thing
483 document.execCommand("enableObjectResizing", false, "false");
484 document.execCommand("enableInlineTableEditing", false, "false");
486 self.trigger('rte:ready');
492 setup_editables: function (root) {
493 // selection of editable sub-items was previously in
494 // EditorBar#edit, but for some unknown reason the elements were
495 // apparently removed and recreated (?) at editor initalization,
496 // and observer setup was lost.
498 // setup dirty-marking for each editable element
499 this.fetch_editables(root)
500 .addClass('oe_editable')
504 // only explicitly set contenteditable on view sections,
505 // cke widgets system will do the widgets themselves
506 if ($node.data('oe-model') === 'ir.ui.view') {
507 node.contentEditable = true;
510 observer.observe(node, OBSERVER_CONFIG);
511 $node.one('content_changed', function () {
512 $node.addClass('oe_dirty');
513 self.trigger('change');
518 fetch_editables: function (root) {
519 return $(root).find('[data-oe-model]')
520 // FIXME: propagation should make "meta" blocks non-editable in the first place...
522 .not('.oe_snippet_editor')
523 .filter(function () {
525 // keep view sections and fields which are *not* in
526 // view sections for top-level editables
527 return $this.data('oe-model') === 'ir.ui.view'
528 || !$this.closest('[data-oe-model = "ir.ui.view"]').length;
532 _current_editor: function () {
533 return CKEDITOR.currentInstance;
535 _config: function () {
536 // base plugins minus
537 // - magicline (captures mousein/mouseout -> breaks draggable)
538 // - contextmenu & tabletools (disable contextual menu)
539 // - bunch of unused plugins
541 'a11yhelp', 'basicstyles', 'blockquote',
542 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab',
543 'elementspath', 'enterkey', 'entities', 'filebrowser',
544 'find', 'floatingspace','format', 'htmlwriter', 'iframe',
545 'indentblock', 'indentlist', 'justify',
546 'list', 'pastefromword', 'pastetext', 'preview',
547 'removeformat', 'resize', 'save', 'selectall', 'stylescombo',
548 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea'
553 // Disable auto-generated titles
554 // FIXME: accessibility, need to generate user-sensible title, used for @title and @aria-label
556 plugins: plugins.join(','),
558 // FIXME: currently breaks RTE?
559 // Ensure no config file is loaded
562 allowedContent: true,
563 // Don't insert paragraphs around content in e.g. <li>
564 autoParagraph: false,
565 // Don't automatically add or <br> in empty block-level
566 // elements when edition starts
567 fillEmptyBlocks: false,
568 filebrowserImageUploadUrl: "/website/attach",
569 // Support for sharedSpaces in 4.x
570 extraPlugins: 'sharedspace,customdialogs,tablebutton,oeref',
571 // Place toolbar in controlled location
572 sharedSpaces: { top: 'oe_rte_toolbar' },
574 name: 'clipboard', items: [
577 name: 'basicstyles', items: [
578 "Bold", "Italic", "Underline", "Strike", "Subscript",
579 "Superscript", "TextColor", "BGColor", "RemoveFormat"
581 name: 'span', items: [
582 "Link", "Blockquote", "BulletedList",
583 "NumberedList", "Indent", "Outdent"
585 name: 'justify', items: [
586 "JustifyLeft", "JustifyCenter", "JustifyRight", "JustifyBlock"
588 name: 'special', items: [
589 "Image", "TableButton"
591 name: 'styles', items: [
595 // styles dropdown in toolbar
597 {name: "Normal", element: 'p'},
598 {name: "Heading 1", element: 'h1'},
599 {name: "Heading 2", element: 'h2'},
600 {name: "Heading 3", element: 'h3'},
601 {name: "Heading 4", element: 'h4'},
602 {name: "Heading 5", element: 'h5'},
603 {name: "Heading 6", element: 'h6'},
604 {name: "Formatted", element: 'pre'},
605 {name: "Address", element: 'address'},
607 {name: "Muted", element: 'span', attributes: {'class': 'text-muted'}},
608 {name: "Primary", element: 'span', attributes: {'class': 'text-primary'}},
609 {name: "Warning", element: 'span', attributes: {'class': 'text-warning'}},
610 {name: "Danger", element: 'span', attributes: {'class': 'text-danger'}},
611 {name: "Success", element: 'span', attributes: {'class': 'text-success'}},
612 {name: "Info", element: 'span', attributes: {'class': 'text-info'}}
618 website.editor = { };
619 website.editor.Dialog = openerp.Widget.extend({
621 'hidden.bs.modal': 'destroy',
622 'click button.save': 'save',
624 init: function (editor) {
626 this.editor = editor;
629 var sup = this._super();
630 this.$el.modal({backdrop: 'static'});
637 this.$el.modal('hide');
641 website.editor.LinkDialog = website.editor.Dialog.extend({
642 template: 'website.editor.dialog.link',
643 events: _.extend({}, website.editor.Dialog.prototype.events, {
644 'change .url-source': function (e) { this.changed($(e.target)); },
645 'mousedown': function (e) {
646 var $target = $(e.target).closest('.list-group-item');
647 if (!$target.length || $target.hasClass('active')) {
648 // clicked outside groups, or clicked in active groups
652 this.changed($target.find('.url-source'));
654 'click button.remove': 'remove_link',
655 'change input#link-text': function (e) {
656 this.text = $(e.target).val()
659 init: function (editor) {
661 // url -> name mapping for existing pages
662 this.pages = Object.create(null);
667 if ((element = this.get_selected_link()) && element.hasAttribute('href')) {
668 this.editor.getSelection().selectElement(element);
670 this.element = element;
672 this.add_removal_button();
676 this.fetch_pages().done(this.proxy('fill_pages')),
678 ).done(this.proxy('bind_data'));
680 add_removal_button: function () {
681 this.$('.modal-footer').prepend(
683 'website.editor.dialog.link.footer-button'));
685 remove_link: function () {
686 var editor = this.editor;
687 // same issue as in make_link
688 setTimeout(function () {
689 editor.removeStyle(new CKEDITOR.style({
691 type: CKEDITOR.STYLE_INLINE,
692 alwaysRemoveElement: true,
698 * Greatly simplified version of CKEDITOR's
699 * plugins.link.dialogs.link.onOk.
701 * @param {String} url
702 * @param {Boolean} [new_window=false]
703 * @param {String} [label=null]
705 make_link: function (url, new_window, label) {
706 var attributes = {href: url, 'data-cke-saved-href': url};
709 attributes['target'] = '_blank';
711 to_remove.push('target');
715 this.element.setAttributes(attributes);
716 this.element.removeAttributes(to_remove);
717 if (this.text) { this.element.setText(this.text); }
719 var selection = this.editor.getSelection();
720 var range = selection.getRanges(true)[0];
722 if (range.collapsed) {
723 //noinspection JSPotentiallyInvalidConstructorUsage
724 var text = new CKEDITOR.dom.text(
725 this.text || label || url);
726 range.insertNode(text);
727 range.selectNodeContents(text);
730 //noinspection JSPotentiallyInvalidConstructorUsage
732 type: CKEDITOR.STYLE_INLINE,
734 attributes: attributes,
735 }).applyToRange(range);
737 // focus dance between RTE & dialog blow up the stack in Safari
738 // and Chrome, so defer select() until dialog has been closed
739 setTimeout(function () {
745 var self = this, _super = this._super.bind(this);
746 var $e = this.$('.list-group-item.active .url-source');
748 if (!val || !$e[0].checkValidity()) {
749 // FIXME: error message
750 $e.closest('.form-group').addClass('has-error');
755 if ($e.hasClass('email-address')) {
756 this.make_link('mailto:' + val, false, val);
757 } else if ($e.hasClass('existing')) {
758 self.make_link(val, false, this.pages[val]);
759 } else if ($e.hasClass('pages')) {
760 // Create the page, get the URL back
761 done = $.get(_.str.sprintf(
762 '/pagenew/%s?noredirect', encodeURI(val)))
763 .then(function (response) {
764 self.make_link(response, false, val);
767 this.make_link(val, this.$('input.window-new').prop('checked'));
771 bind_data: function () {
772 var href = this.element && (this.element.data( 'cke-saved-href')
773 || this.element.getAttribute('href'));
774 if (!href) { return; }
777 if (match = /mailto:(.+)/.exec(href)) {
778 $control = this.$('input.email-address').val(match[1]);
779 } else if (href in this.pages) {
780 $control = this.$('select.existing').val(href);
781 } else if (match = /\/page\/(.+)/.exec(href)) {
782 var actual_href = '/page/website.' + match[1];
783 if (actual_href in this.pages) {
784 $control = this.$('select.existing').val(actual_href);
788 $control = this.$('input.url').val(href);
791 this.changed($control);
793 this.$('input#link-text').val(this.element.getText());
794 this.$('input.window-new').prop(
795 'checked', this.element.getAttribute('target') === '_blank');
797 changed: function ($e) {
798 this.$('.url-source').not($e).val('');
799 $e.closest('.list-group-item')
801 .siblings().removeClass('active')
802 .addBack().removeClass('has-error');
805 * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root,
806 * if the editor is set directly on a link it will thus not work.
808 get_selected_link: function () {
809 return get_selected_link(this.editor);
811 fetch_pages: function () {
812 return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
814 method: 'list_pages',
817 context: website.get_context()
821 fill_pages: function (results) {
823 var pages = this.$('select.existing')[0];
824 _(results).each(function (result) {
825 self.pages[result.url] = result.name;
827 pages.options[pages.options.length] =
828 new Option(result.name, result.url);
833 * ImageDialog widget. Lets users change an image, including uploading a
834 * new image in OpenERP or selecting the image style (if supported by
837 * Initialized as usual, but the caller can hook into two events:
839 * @event start({url, style}) called during dialog initialization and
840 * opening, the handler can *set* the ``url``
841 * and ``style`` properties on its parameter
842 * to provide these as default values to the
844 * @event save({url, style}) called during dialog finalization, the handler
845 * is provided with the image url and style
846 * selected by the users (or possibly the ones
847 * originally passed in)
849 website.editor.ImageDialog = website.editor.Dialog.extend({
850 template: 'website.editor.dialog.image',
851 events: _.extend({}, website.editor.Dialog.prototype.events, {
852 'change .url-source': function (e) { this.changed($(e.target)); },
853 'click button.filepicker': function () {
854 this.$('input[type=file]').click();
856 'change input[type=file]': 'file_selection',
857 'change input.url': 'preview_image',
858 'click a[href=#existing]': 'browse_existing',
859 'change select.image-style': 'preview_image',
863 var $options = this.$('.image-style').children();
864 this.image_styles = $options.map(function () { return this.value; }).get();
866 var o = { url: null, style: null, };
867 // avoid typos, prevent addition of new properties to the object
868 Object.preventExtensions(o);
869 this.trigger('start', o);
873 this.$('.image-style').val(o.style);
875 this.set_image(o.url);
878 return this._super();
881 this.trigger('save', {
882 url: this.$('input.url').val(),
883 style: this.$('.image-style').val(),
885 return this._super();
889 * Sets the provided image url as the dialog's value-to-save and
890 * refreshes the preview element to use it.
892 set_image: function (url) {
893 this.$('input.url').val(url);
894 this.preview_image();
897 file_selection: function () {
898 this.$('button.filepicker').removeClass('btn-danger btn-success');
901 var callback = _.uniqueId('func_');
902 this.$('input[name=func]').val(callback);
904 window[callback] = function (url, error) {
905 delete window[callback];
906 self.file_selected(url, error);
908 this.$('form').submit();
910 file_selected: function(url, error) {
911 var $button = this.$('button.filepicker');
913 $button.addClass('btn-danger');
916 $button.addClass('btn-success');
919 preview_image: function () {
920 var image = this.$('input.url').val();
921 if (!image) { return; }
923 this.$('img.image-preview')
925 .removeClass(this.image_styles.join(' '))
926 .addClass(this.$('select.image-style').val());
928 browse_existing: function (e) {
930 new website.editor.ExistingImageDialog(this).appendTo(document.body);
933 website.editor.RTEImageDialog = website.editor.ImageDialog.extend({
935 this._super.apply(this, arguments);
937 this.on('start', this, this.proxy('started'));
938 this.on('save', this, this.proxy('saved'));
940 started: function (holder) {
941 var selection = this.editor.getSelection();
942 var el = selection && selection.getSelectedElement();
945 if (el && el.is('img')) {
947 _(this.image_styles).each(function (style) {
948 if (el.hasClass(style)) {
949 holder.style = style;
952 holder.url = el.getAttribute('src');
955 saved: function (data) {
956 var element, editor = this.editor;
957 if (!(element = this.element)) {
958 element = editor.document.createElement('img');
959 element.addClass('img');
960 // focus event handler interactions between bootstrap (modal)
961 // and ckeditor (RTE) lead to blowing the stack in Safari and
962 // Chrome (but not FF) when this is done synchronously =>
963 // defer insertion so modal has been hidden & destroyed before
965 setTimeout(function () {
966 editor.insertElement(element);
970 var style = data.style;
971 element.setAttribute('src', data.url);
972 element.removeAttribute('data-cke-saved-src');
973 $(element.$).removeClass(this.image_styles.join(' '));
974 if (style) { element.addClass(style); }
978 var IMAGES_PER_ROW = 6;
980 website.editor.ExistingImageDialog = website.editor.Dialog.extend({
981 template: 'website.editor.dialog.image.existing',
982 events: _.extend({}, website.editor.Dialog.prototype.events, {
983 'click .existing-attachments img': 'select_existing',
984 'click .pager > li': function (e) {
986 var $target = $(e.currentTarget);
987 if ($target.hasClass('disabled')) {
990 this.page += $target.hasClass('previous') ? -1 : 1;
991 this.display_attachments();
994 init: function (parent) {
997 this.parent = parent;
998 this._super(parent.editor);
1001 start: function () {
1004 this.fetch_existing().then(this.proxy('fetched_existing')));
1007 fetch_existing: function () {
1008 return openerp.jsonRpc('/web/dataset/call_kw', 'call', {
1009 model: 'ir.attachment',
1010 method: 'search_read',
1013 fields: ['name', 'website_url'],
1014 domain: [['res_model', '=', 'ir.ui.view']],
1016 context: website.get_context(),
1020 fetched_existing: function (records) {
1021 this.records = records;
1022 this.display_attachments();
1024 display_attachments: function () {
1025 var per_screen = IMAGES_PER_ROW * IMAGES_ROWS;
1027 var from = this.page * per_screen;
1028 var records = this.records;
1030 // Create rows of 3 records
1031 var rows = _(records).chain()
1032 .slice(from, from + per_screen)
1033 .groupBy(function (_, index) { return Math.floor(index / IMAGES_PER_ROW); })
1037 this.$('.existing-attachments').replaceWith(
1038 openerp.qweb.render(
1039 'website.editor.dialog.image.existing.content', {rows: rows}));
1041 .find('li.previous').toggleClass('disabled', (from === 0)).end()
1042 .find('li.next').toggleClass('disabled', (from + per_screen >= records.length));
1045 select_existing: function (e) {
1046 var link = $(e.currentTarget).attr('src');
1048 this.parent.set_image(link);
1054 function get_selected_link(editor) {
1055 var sel = editor.getSelection(),
1056 el = sel.getSelectedElement();
1057 if (el && el.is('a')) { return el; }
1059 var range = sel.getRanges(true)[0];
1060 if (!range) { return null; }
1062 range.shrink(CKEDITOR.SHRINK_TEXT);
1063 var commonAncestor = range.getCommonAncestor();
1064 var viewRoot = editor.elementPath(commonAncestor).contains(function (element) {
1065 return element.data('oe-model') === 'ir.ui.view'
1067 if (!viewRoot) { return null; }
1068 // if viewRoot is the first link, don't edit it.
1069 return new CKEDITOR.dom.elementPath(commonAncestor, viewRoot)
1070 .contains('a', true);
1074 website.Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver;
1075 var OBSERVER_CONFIG = {
1078 characterData: true,
1080 attributeOldValue: true,
1082 var observer = new website.Observer(function (mutations) {
1083 // NOTE: Webkit does not fire DOMAttrModified => webkit browsers
1084 // relying on JsMutationObserver shim (Chrome < 18, Safari < 6)
1085 // will not mark dirty on attribute changes (@class, img/@src,
1087 _(mutations).chain()
1088 .filter(function (m) {
1090 case 'attributes': // ignore .cke_focus being added or removed
1091 // if attribute is not a class, can't be .cke_focus change
1092 if (m.attributeName !== 'class') { return true; }
1094 // find out what classes were added or removed
1095 var oldClasses = (m.oldValue || '').split(/\s+/);
1096 var newClasses = m.target.className.split(/\s+/);
1097 var change = _.union(_.difference(oldClasses, newClasses),
1098 _.difference(newClasses, oldClasses));
1099 // ignore mutation if the *only* change is .cke_focus
1100 return change.length !== 1 || change[0] === 'cke_focus';
1102 // <br type="_moz"> appears when focusing RTE in FF, ignore
1103 return m.addedNodes.length !== 1 || m.addedNodes[0].nodeName !== 'BR';
1109 var node = m.target;
1110 while (node && !$(node).hasClass('oe_editable')) {
1111 node = node.parentNode;
1113 $(m.target).trigger('node_changed');
1118 .each(function (node) { $(node).trigger('content_changed'); })