X-Git-Url: http://git.inspyration.org/?a=blobdiff_plain;f=addons%2Fwebsite%2Fstatic%2Fsrc%2Fjs%2Fwebsite.editor.js;h=5915ee5b3591a5322052b5f01f8cd1c514b1ca3a;hb=854916ce54817dc23ed887e249586237882a94ae;hp=217d7fd24ee10eac6e409e06e248888e6345b6a4;hpb=9cdbe94ce5006e5a2184941d8d31c1e808a02bcf;p=odoo%2Fodoo.git diff --git a/addons/website/static/src/js/website.editor.js b/addons/website/static/src/js/website.editor.js index 217d7fd..5915ee5 100644 --- a/addons/website/static/src/js/website.editor.js +++ b/addons/website/static/src/js/website.editor.js @@ -3,34 +3,41 @@ var website = openerp.website; // $.fn.data automatically parses value, '0'|'1' -> 0|1 - website.is_editable = $(document.documentElement).data('editable'); - website.templates.push('/website/static/src/xml/website.editor.xml'); + website.add_template_file('/website/static/src/xml/website.editor.xml'); website.dom_ready.done(function () { var is_smartphone = $(document.body)[0].clientWidth < 767; - if (website.is_editable && !is_smartphone) { + if (!is_smartphone) { website.ready().then(website.init_editor); } + + $(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) { - return new website.editor.LinkDialog(editor).appendTo(document.body); + return new website.editor.RTELinkDialog(editor).appendTo(document.body); } function image_dialog(editor) { return new website.editor.RTEImageDialog(editor).appendTo(document.body); } - if (website.is_editable) { - // only enable editors manually - CKEDITOR.disableAutoInline = true; - // EDIT ALL THE THINGS - CKEDITOR.dtd.$editable = $.extend( - {}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline); - // Disable removal of empty elements on CKEDITOR activation. Empty - // elements are used for e.g. support of FontAwesome icons - CKEDITOR.dtd.$removeEmpty = {}; - } + // only enable editors manually + CKEDITOR.disableAutoInline = true; + // EDIT ALL THE THINGS + CKEDITOR.dtd.$editable = $.extend( + {}, CKEDITOR.dtd.$block, CKEDITOR.dtd.$inline); + // Disable removal of empty elements on CKEDITOR activation. Empty + // elements are used for e.g. support of FontAwesome icons + CKEDITOR.dtd.$removeEmpty = {}; + website.init_editor = function () { CKEDITOR.plugins.add('customdialogs', { // requires: 'link,image', @@ -56,9 +63,42 @@ 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 = $('') + .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, data) { + exec: function (editor) { link_dialog(editor); return true; }, @@ -67,7 +107,7 @@ }); //noinspection JSValidateTypes editor.addCommand('image', { - exec: function (editor, data) { + exec: function (editor) { image_dialog(editor); return true; }, @@ -162,16 +202,175 @@ editables: { text: '*' }, upcast: function (el) { - return el.attributes['data-oe-type']; + 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', + + 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( + '%s', + 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( + '%s', + 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 () { - if (location.search.indexOf("unable_editor") >= 0) { + if (location.search.indexOf("enable_editor") >= 0) { editor.edit(); } }); @@ -203,8 +402,11 @@ } }); // Adding Static Menus - menu.append('
  • Change Theme
  • '); - menu.append('
  • Advanced view editor
  • '); + menu.append('
  • '); + menu.append('
  • HTML Editor
  • '); + menu.append('
  • Change Theme
  • '); + menu.append('
  • Install Apps
  • '); + self.trigger('rte:customize_menu_ready'); } ); }); @@ -212,7 +414,7 @@ var view_id = $(event.currentTarget).data('view-id'); openerp.jsonRpc('/website/customize_template_toggle', 'call', { 'view_id': view_id - }).then( function(result) { + }).then( function() { window.location.reload(); }); }); @@ -246,7 +448,6 @@ ); }, edit: function () { - var self = this; this.$buttons.edit.prop('disabled', true); this.$('#website-top-view').hide(); this.$('#website-top-edit').show(); @@ -266,30 +467,58 @@ editor.destroy(); // FIXME: select editables then filter by dirty? var defs = this.rte.fetch_editables(root) - .removeClass('oe_editable cke_focus') - .removeAttr('contentEditable') .filter('.oe_dirty') + .removeAttr('contentEditable') + .removeClass('oe_dirty oe_editable cke_focus oe_carlos_danger') .map(function () { var $el = $(this); // TODO: Add a queue with concurrency limit in webclient // https://github.com/medikoo/deferred/blob/master/lib/ext/function/gate.js return self.saving_mutex.exec(function () { return self.saveElement($el) - .fail(function () { - var data = $el.data(); - console.error(_.str.sprintf('Could not save %s(%d).%s', data.oeModel, data.oeId, data.oeField)); + .then(undefined, function (thing, response) { + // because ckeditor regenerates all the dom, + // we can't just setup the popover here as + // everything will be destroyed by the DOM + // regeneration. Add markings instead, and + // returns a new rejection with all relevant + // info + var id = _.uniqueId('carlos_danger_'); + $el.addClass('oe_dirty oe_carlos_danger'); + $el.addClass(id); + return $.Deferred().reject({ + id: id, + error: response.data, + }); }); }); }).get(); return $.when.apply(null, defs).then(function () { website.reload(); + }, function (failed) { + // If there were errors, re-enable edition + self.rte.start_edition(true).then(function () { + // jquery's deferred being a pain in the ass + if (!_.isArray(failed)) { failed = [failed]; } + + _(failed).each(function (failure) { + $(root).find('.' + failure.id) + .removeClass(failure.id) + .popover({ + trigger: 'hover', + content: failure.error.message, + placement: 'auto top', + }) + // Force-show popovers so users will notice them. + .popover('show'); + }) + }); }); }, /** * Saves an RTE content, which always corresponds to a view section (?). */ saveElement: function ($el) { - $el.removeClass('oe_dirty'); var markup = $el.prop('outerHTML'); return openerp.jsonRpc('/web/dataset/call', 'call', { model: 'ir.ui.view', @@ -300,10 +529,11 @@ }); }, cancel: function () { - window.location.href = window.location.href.replace(/unable_editor(=[^&]*)?|#.*/g, ''); + website.reload(); }, }); + var blocks_selector = _.keys(CKEDITOR.dtd.$block).join(','); /* ----- RICH TEXT EDITOR ---- */ website.RTE = openerp.Widget.extend({ tagName: 'li', @@ -317,13 +547,123 @@ this._super.apply(this, arguments); }, - start_edition: function ($elements) { + /** + * In Webkit-based browsers, triple-click will select a paragraph up to + * the start of the next "paragraph" including any empty space + * inbetween. When said paragraph is removed or altered, it nukes + * the empty space and brings part of the content of the next + * "paragraph" (which may well be e.g. an image) into the current one, + * completely fucking up layouts and breaking snippets. + * + * Try to fuck around with selections on triple-click to attempt to + * fix this garbage behavior. + * + * Note: for consistent behavior we may actually want to take over + * triple-clicks, in all browsers in order to ensure consistent cross- + * platform behavior instead of being at the mercy of rendering engines + * & platform selection quirks? + */ + webkitSelectionFixer: function (root) { + root.addEventListener('click', function (e) { + // only webkit seems to have a fucked up behavior, ignore others + // FIXME: $.browser goes away in jquery 1.9... + if (!$.browser.webkit) { return; } + // http://www.w3.org/TR/DOM-Level-2-Events/events.html#Events-eventgroupings-mouseevents + // The detail attribute indicates the number of times a mouse button has been pressed + // we just want the triple click + if (e.detail !== 3) { return; } + e.preventDefault(); + + // Get closest block-level element to the triple-clicked + // element (using ckeditor's block list because why not) + var $closest_block = $(e.target).closest(blocks_selector); + + // manually set selection range to the content of the + // triple-clicked block-level element, to avoid crossing over + // between block-level elements + document.getSelection().selectAllChildren($closest_block[0]); + }); + }, + tableNavigation: function (root) { + var self = this; + $(root).on('keydown', function (e) { + // ignore non-TAB + if (e.which !== 9) { return; } + + if (self.handleTab(e)) { + e.preventDefault(); + } + }); + }, + /** + * Performs whatever operation is necessary on a [TAB] hit, returns + * ``true`` if the event's default should be cancelled (if the TAB was + * handled by the function) + */ + handleTab: function (event) { + var forward = !event.shiftKey; + + var root = window.getSelection().getRangeAt(0).commonAncestorContainer; + var $cell = $(root).closest('td,th'); + + if (!$cell.length) { return false; } + + var cell = $cell[0]; + + // find cell in same row + var row = cell.parentNode; + var sibling = row.cells[cell.cellIndex + (forward ? 1 : -1)]; + if (sibling) { + document.getSelection().selectAllChildren(sibling); + return true; + } + + // find cell in previous/next row + var table = row.parentNode; + var sibling_row = table.rows[row.rowIndex + (forward ? 1 : -1)]; + if (sibling_row) { + var new_cell = sibling_row.cells[forward ? 0 : sibling_row.cells.length - 1]; + document.getSelection().selectAllChildren(new_cell); + return true; + } + + // at edge cells, copy word/openoffice behavior: if going backwards + // from first cell do nothing, if going forwards from last cell add + // a row + if (forward) { + var row_size = row.cells.length; + var new_row = document.createElement('tr'); + while(row_size--) { + var newcell = document.createElement('td'); + // zero-width space + newcell.textContent = '\u200B'; + new_row.appendChild(newcell); + } + table.appendChild(new_row); + document.getSelection().selectAllChildren(new_row.cells[0]); + } + + return true; + }, + /** + * Makes the page editable + * + * @param {Boolean} [restart=false] in case the edition was already set + * up once and is being re-enabled. + * @returns {$.Deferred} deferred indicating when the RTE is ready + */ + start_edition: function (restart) { var self = this; // create a single editor for the whole page var root = document.getElementById('wrapwrap'); - $(root).on('dragstart', 'img', function (e) { - e.preventDefault(); - }); + if (!restart) { + $(root).on('dragstart', 'img', function (e) { + e.preventDefault(); + }); + this.webkitSelectionFixer(root); + this.tableNavigation(root); + } + var def = $.Deferred(); var editor = this.editor = CKEDITOR.inline(root, self._config()); editor.on('instanceReady', function () { editor.setReadOnly(false); @@ -334,8 +674,14 @@ self.setup_editables(root); + // disable firefox's broken table resizing thing + document.execCommand("enableObjectResizing", false, "false"); + document.execCommand("enableInlineTableEditing", false, "false"); + self.trigger('rte:ready'); + def.resolve(); }); + return def; }, setup_editables: function (root) { @@ -346,12 +692,17 @@ var self = this; // setup dirty-marking for each editable element this.fetch_editables(root) - .prop('contentEditable', true) .addClass('oe_editable') .each(function () { var node = this; - observer.observe(node, OBSERVER_CONFIG); var $node = $(node); + // only explicitly set contenteditable on view sections, + // cke widgets system will do the widgets themselves + if ($node.data('oe-model') === 'ir.ui.view') { + node.contentEditable = true; + } + + observer.observe(node, OBSERVER_CONFIG); $node.one('content_changed', function () { $node.addClass('oe_dirty'); self.trigger('change'); @@ -361,13 +712,12 @@ fetch_editables: function (root) { return $(root).find('[data-oe-model]') - // FIXME: propagation should make "meta" blocks non-editable in the first place... .not('link, script') .not('.oe_snippet_editor') .filter(function () { var $this = $(this); // keep view sections and fields which are *not* in - // view sections for toplevel editables + // view sections for top-level editables return $this.data('oe-model') === 'ir.ui.view' || !$this.closest('[data-oe-model = "ir.ui.view"]').length; }); @@ -382,14 +732,14 @@ // - contextmenu & tabletools (disable contextual menu) // - bunch of unused plugins var plugins = [ - 'a11yhelp', 'basicstyles', 'bidi', 'blockquote', + 'a11yhelp', 'basicstyles', 'blockquote', 'clipboard', 'colorbutton', 'colordialog', 'dialogadvtab', 'elementspath', 'enterkey', 'entities', 'filebrowser', 'find', 'floatingspace','format', 'htmlwriter', 'iframe', 'indentblock', 'indentlist', 'justify', 'list', 'pastefromword', 'pastetext', 'preview', 'removeformat', 'resize', 'save', 'selectall', 'stylescombo', - 'tab', 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea' + 'table', 'templates', 'toolbar', 'undo', 'wysiwygarea' ]; return { // FIXME @@ -411,7 +761,7 @@ 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: [{ @@ -433,7 +783,7 @@ "Image", "TableButton" ]},{ name: 'styles', items: [ - "Styles" + "Styles", "BootstrapLinkCombo" ]} ], // styles dropdown in toolbar @@ -446,14 +796,7 @@ {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'} ], }; }, @@ -471,7 +814,7 @@ }, start: function () { var sup = this._super(); - this.$el.modal(); + this.$el.modal({backdrop: 'static'}); return sup; }, save: function () { @@ -507,6 +850,106 @@ this.text = null; }, start: function () { + var self = this; + return $.when( + this.fetch_pages().done(this.proxy('fill_pages')), + this._super() + ).done(function () { + self.bind_data(); + }); + }, + save: function () { + var self = this, _super = this._super.bind(this); + var $e = this.$('.list-group-item.active .url-source'); + 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); + }); + } else { + this.make_link(val, this.$('input.window-new').prop('checked')); + } + done.then(_super); + }, + make_link: function (url, new_window, label) { + }, + 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))) { + $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(text); + this.$('input.window-new').prop('checked', new_window); + }, + changed: function ($e) { + this.$('.url-source').not($e).val(''); + $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', { + model: 'website', + method: 'list_pages', + args: [null], + kwargs: { + context: website.get_context() + }, + }); + }, + 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); + }); + }, + }); + website.editor.RTELinkDialog = website.editor.LinkDialog.extend({ + start: function () { var element; if ((element = this.get_selected_link()) && element.hasAttribute('href')) { this.editor.getSelection().selectElement(element); @@ -516,10 +959,7 @@ this.add_removal_button(); } - return $.when( - this.fetch_pages().done(this.proxy('fill_pages')), - this._super() - ).done(this.proxy('bind_data')); + return this._super(); }, add_removal_button: function () { this.$('.modal-footer').prepend( @@ -585,66 +1025,6 @@ }, 0); } }, - save: function () { - var self = this, _super = this._super.bind(this); - var $e = this.$('.list-group-item.active .url-source'); - var val = $e.val(); - if (!val || !$e[0].checkValidity()) { - // FIXME: error message - $e.closest('.form-group').addClass('has-error'); - 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', encodeURIComponent(val))) - .then(function (response) { - self.make_link(response, false, val); - }); - } else { - this.make_link(val, this.$('input.window-new').prop('checked')); - } - done.then(_super); - }, - bind_data: function () { - var href = this.element && (this.element.data( 'cke-saved-href') - || this.element.getAttribute('href')); - if (!href) { return; } - - 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); - } - - this.changed($control); - - this.$('input#link-text').val(this.element.getText()); - this.$('input.window-new').prop( - 'checked', this.element.getAttribute('target') === '_blank'); - }, - changed: function ($e) { - this.$('.url-source').not($e).val(''); - $e.closest('.list-group-item') - .addClass('active') - .siblings().removeClass('active') - .addBack().removeClass('has-error'); - }, /** * CKEDITOR.plugins.link.getSelectedLink ignores the editor's root, * if the editor is set directly on a link it will thus not work. @@ -652,27 +1032,8 @@ get_selected_link: function () { return get_selected_link(this.editor); }, - fetch_pages: function () { - return openerp.jsonRpc('/web/dataset/call_kw', 'call', { - model: 'website', - method: 'list_pages', - args: [null], - kwargs: { - context: website.get_context() - }, - }); - }, - 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); - }); - }, }); + /** * ImageDialog widget. Lets users change an image, including uploading a * new image in OpenERP or selecting the image style (if supported by @@ -704,6 +1065,7 @@ }), start: function () { + this.$('.modal-footer [disabled]').text("Uploading…"); var $options = this.$('.image-style').children(); this.image_styles = $options.map(function () { return this.value; }).get(); @@ -738,7 +1100,8 @@ this.preview_image(); }, - file_selection: function (e) { + file_selection: function () { + this.$el.addClass('nosave'); this.$('button.filepicker').removeClass('btn-danger btn-success'); var self = this; @@ -761,6 +1124,7 @@ this.set_image(url); }, preview_image: function () { + this.$el.removeClass('nosave'); var image = this.$('input.url').val(); if (!image) { return; } @@ -800,6 +1164,7 @@ var element, editor = this.editor; if (!(element = this.element)) { element = editor.document.createElement('img'); + element.addClass('img'); // 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 => @@ -914,7 +1279,7 @@ } - var Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver; + website.Observer = window.MutationObserver || window.WebkitMutationObserver || window.JsMutationObserver; var OBSERVER_CONFIG = { childList: true, attributes: true, @@ -922,7 +1287,7 @@ subtree: true, attributeOldValue: true, }; - var observer = new Observer(function (mutations) { + var observer = new website.Observer(function (mutations) { // NOTE: Webkit does not fire DOMAttrModified => webkit browsers // relying on JsMutationObserver shim (Chrome < 18, Safari < 6) // will not mark dirty on attribute changes (@class, img/@src,