From 8790ba3c4f058c915401e18637ba2eea29472c3b Mon Sep 17 00:00:00 2001 From: Ingo Schommer Date: Wed, 22 Feb 2012 17:53:37 +0100 Subject: [PATCH] BUGFIX Retain editor focus when insert images, update attributes on existing image to avoid IE bugs through TinyMCE --- javascript/HtmlEditorField.js | 125 +++++++++++++++++++++++++++------- 1 file changed, 100 insertions(+), 25 deletions(-) diff --git a/javascript/HtmlEditorField.js b/javascript/HtmlEditorField.js index 10efac9a9..59ca2621d 100644 --- a/javascript/HtmlEditorField.js +++ b/javascript/HtmlEditorField.js @@ -14,8 +14,6 @@ */ ss.editorWrappers = {}; ss.editorWrappers.tinyMCE = (function() { - var bookmark; - return { /** * @return Mixed Implementation specific object @@ -27,13 +25,11 @@ * Invoked when a content-modifying UI is opened. */ onopen: function() { - bookmark = this.getInstance().selection.getBookmark(); }, /** * Invoked when a content-modifying UI is closed. */ onclose: function() { - bookmark = null; }, /** * Write the HTML back to the original text area field. @@ -104,22 +100,28 @@ this.getInstance().selection.select(node); }, /** + * Insert content at the current caret position + * * @param String HTML */ - insertContent: function(html) { - // Workaround for IE losing focus - this.getInstance().selection.moveToBookmark(bookmark); - this.getInstance().execCommand('mceInsertContent', false, html); + insertContent: function(html, opts) { + this.getInstance().execCommand('mceInsertContent', false, html, opts); + }, + /** + * Replace currently selected content + * + * @param {String} html + */ + replaceContent: function(html, opts) { + this.getInstance().execCommand('mceReplaceContent', false, html, opts); }, /** * Insert or update a link in the content area (based on current editor selection) * * Parameters: {Object} attrs */ - insertLink: function(attrs) { - // Workaround for IE losing focus - this.getInstance().selection.moveToBookmark(bookmark); - this.getInstance().execCommand("mceInsertLink", false, attrs); + insertLink: function(attrs, opts) { + this.getInstance().execCommand("mceInsertLink", false, attrs, opts); }, /** * Remove the link from the currently selected node (if any). @@ -147,6 +149,27 @@ if(href.match(/^javascript:\s*mctmp/)) href = ''; return href; + }, + /** + * Creates a bookmark for the currently selected range, + * which can be used to reselect this range at a later point. + * @return {mixed} + */ + createBookmark: function() { + return this.getInstance().selection.getBookmark(); + }, + /** + * Selects a bookmarked range previously saved through createBookmark(). + * @param {mixed} bookmark + */ + moveToBookmark: function(bookmark) { + this.getInstance().selection.moveToBookmark(bookmark); + }, + /** + * Add new undo point with the current DOM content. + */ + addUndo: function() { + this.getInstance().undoManager.add(); } }; }); @@ -220,7 +243,7 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; isChanged: function() { var ed = this.getEditor(); - return (ed && ed.isDirty()); + return (ed && ed.getInstance() && ed.isDirty()); }, resetChanged: function() { @@ -250,6 +273,9 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; // Wrapper for various HTML editors Editor: null, + // TODO Figure out how to keep bookmark reference in entwine, and still be allowed to delete the JS object + // Bookmark: null, + onmatch: function() { // Move title from headline to (jQuery compatible) title attribute var titleEl = this.find(':header:first'); @@ -270,12 +296,14 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; close: function() { this.dialog('close'); this.getEditor().onclose(); + if(typeof window._ss_htmleditorfield_bookmark != 'undefined') window._ss_htmleditorfield_bookmark = null; }, open: function() { this.updateFromEditor(); this.dialog('open'); this.redraw(); this.getEditor().onopen(); + window._ss_htmleditorfield_bookmark = this.getEditor().createBookmark(); }, /** * Update the view state based on the current editor selection. @@ -374,6 +402,10 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; title : this.find(':input[name=Description]').val() }; + // Workaround for browsers losing focus, similar to tinyMCEPopup.restoreSelection + ed.moveToBookmark(window._ss_htmleditorfield_bookmark); + window._ss_htmleditorfield_bookmark = null; + // Add the new link ed.insertLink(attributes); this.trigger('onafterinsert', attributes); @@ -567,11 +599,11 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; var self = this, ed = this.getEditor(); // HACK: See ondialogopen() - // if($.browser.msie) jQuery(ed.getContainer()).show(); + // jQuery(ed.getContainer()).show(); this.find('.ss-htmleditorfield-file').each(function(el) { - ed.insertContent($(this).getHTML()); + $(this).insertHTML(); }); ed.repaint(); this.close(); @@ -594,13 +626,13 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; // HACK: Hide selected node in IE because its drag handles on potentially selected elements // don't respect the z-index of the dialog overlay. - // if($.browser.msie) jQuery(ed.getContainer()).hide(); + // jQuery(ed.getContainer()).hide(); }, ondialogclose: function() { var ed = this.getEditor(), node = $(ed.getSelectedNode()); // HACK: See ondialogopen() - // if($.browser.msie) jQuery(ed.getContainer()).show(); + // jQuery(ed.getContainer()).show(); this.find('.ss-htmleditorfield-file').remove(); // Remove any existing views this.find('.ss-gridfield-items .ui-selected').removeClass('ui-selected'); // Unselect all items @@ -685,6 +717,19 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; */ getHTML: function() { }, + /** + * Insert updated HTML content into the rich text editor + */ + insertHTML: function() { + var form = this.closest('form'), ed = form.getEditor(); + + // Workaround for browsers losing focus, similar to tinyMCEPopup.restoreSelection + ed.moveToBookmark(window._ss_htmleditorfield_bookmark); + window._ss_htmleditorfield_bookmark = null; + + // Insert content + ed.replaceContent(this.getHTML()); + }, /** * Updates the form values from an existing node in the editor. * @@ -737,8 +782,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; return { 'src' : this.find(':input[name=URL]').val(), 'alt' : this.find(':input[name=AltText]').val(), - 'width' : width ? parseInt(width, 10) : null, - 'height' : height ? parseInt(height, 10) : null, + 'width' : width ? parseInt(width, 10) + "px" : null, + 'height' : height ? parseInt(height, 10) + "px" : null, 'title' : this.find(':input[name=Title]').val(), 'class' : this.find(':input[name=CSSClass]').val() }; @@ -752,7 +797,8 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; var el, attrs = this.getAttributes(), extraData = this.getExtraData(), - imgEl = $('').attr(attrs); + // imgEl = $(''); + imgEl = $('').attr(attrs); if(extraData.CaptionText) { el = $('

' + extraData.CaptionText + '

').prepend(imgEl); @@ -761,6 +807,40 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; } return $('
').append(el).html(); // Little hack to get outerHTML string }, + /** + * Logic similar to TinyMCE 'advimage' plugin, insertAndClose() method. + */ + insertHTML: function() { + var form = this.closest('form'), ed = form.getEditor(), + node = $(ed.getSelectedNode()), captionNode = node.closest('.captionImage'); + + // Workaround for browsers losing focus, similar to tinyMCEPopup.restoreSelection. + // TODO In TinyMCE core this is restricted to IE, but leaving it our also + // breaks Firefox: It doesn't save the selection because it inserts into a temporary TinyMCE + // marker element rather than the content DOM nodes + ed.moveToBookmark(window._ss_htmleditorfield_bookmark); + window._ss_htmleditorfield_bookmark = null; + + if(node && node.is('img')) { + // If the image exists, update it to avoid complications with inserting TinyMCE HTML content + var attrs = this.getAttributes(), extraData = this.getExtraData(); + node.attr(attrs); + // TODO Doesn't allow adding a caption to image after it was first added + if(captionNode.length) { + captionNode.find('.caption').text(extraData.CaptionText); + captionNode.css({width: attrs.width, height: attrs.height}).attr('class', attrs['class']); + } + // Undo needs to be added manually as we're doing direct DOM changes + ed.addUndo(); + } else { + // Otherwise insert the whole HTML content + ed.repaint(); + ed.insertContent(this.getHTML(), {skip_undo : 1}); + ed.addUndo(); // Not sure why undo is separate here, replicating TinyMCE logic + } + + ed.repaint(); + }, updateFromNode: function(node) { this.find(':input[name=AltText]').val(node.attr('alt')); this.find(':input[name=Title]').val(node.attr('title')); @@ -786,11 +866,6 @@ ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; 'height' : height ? parseInt(height, 10) : null }; }, - getExtraData: function() { - return { - 'CaptionText': this.find(':input[name=CaptionText]').val() - }; - }, getHTML: function() { var attrs = this.getAttributes();