/** * Functions for HtmlEditorFields in the back end. * Includes the JS for the ImageUpload forms. * * Relies on the jquery.form.js plugin to power the * ajax / iframe submissions */ var ss = ss || {}; /** * Wrapper for HTML WYSIWYG libraries, which abstracts library internals * from interface concerns like inserting and editing links. * Caution: Incomplete and unstable API. */ ss.editorWrappers = {}; ss.editorWrappers.tinyMCE = (function() { var instance; return { init: function(config) { if(!ss.editorWrappers.tinyMCE.initialized) { tinyMCE.init(config); ss.editorWrappers.tinyMCE.initialized = true; } }, /** * @return Mixed Implementation specific object */ getInstance: function() { return this.instance; }, /** * Invoked when a content-modifying UI is opened. */ onopen: function() { }, /** * Invoked when a content-modifying UI is closed. */ onclose: function() { }, /** * Write the HTML back to the original text area field. */ save: function() { tinyMCE.triggerSave(); }, /** * Create a new instance based on a textarea field. * * Please proxy the events from your editor implementation into JS events * on the textarea field. For events that do not map directly, use the * following naming scheme: editor. * * @param String * @param Object Implementation specific configuration * @param Function */ create: function(domID, config) { this.instance = new tinymce.Editor(domID, config); // Patch TinyMCE events into underlying textarea field. this.instance.onInit.add(function(ed) { if(!ss.editorWrappers.tinyMCE.patched) { // Not ideal, but there's a memory leak we need to patch var originalDestroy = tinymce.themes.AdvancedTheme.prototype.destroy; tinymce.themes.AdvancedTheme.prototype.destroy = function() { originalDestroy.apply(this, arguments); if (this.statusKeyboardNavigation) { this.statusKeyboardNavigation.destroy(); this.statusKeyboardNavigation = null; } }; ss.editorWrappers.tinyMCE.patched = true; } jQuery(ed.getElement()).trigger('editorinit'); // Periodically check for inline changes when focused, // since TinyMCE's onChange only fires on certain actions // like inserting a new paragraph, as opposed to any user input. // This also works around an issue where the "save" button // wouldn't trigger if the click is the cause of a "blur" event // after an (undetected) inline change. This "blur" causes onChange // to trigger, which will change the button markup to show "alternative" styles, // effectively cancelling the original click event. if(ed.settings.update_interval) { var interval; jQuery(ed.getBody()).on('focus', function() { interval = setInterval(function() { // Update underlying element as necessary var element = jQuery(ed.getElement()); if(ed.isDirty()) { // Set content without triggering editor content cleanup element.val(ed.getContent({format : 'raw', no_events : 1})); } }, ed.settings.update_interval); }); jQuery(ed.getBody()).on('blur', function() { clearInterval(interval); }); } }); this.instance.onChange.add(function(ed, l) { // Update underlying textarea on every change, so external handlers // such as changetracker have a chance to trigger properly. ed.save(); jQuery(ed.getElement()).trigger('change'); }); // Add more events here as needed. this.instance.render(); }, /** * Redraw the editor contents */ repaint: function() { tinyMCE.execCommand("mceRepaint"); }, /** * @return boolean */ isDirty: function() { return this.getInstance().isDirty(); }, /** * HTML representation of the edited content. * * Returns: {String} */ getContent: function() { return this.getInstance().getContent(); }, /** * DOM tree of the edited content * * Returns: DOMElement */ getDOM: function() { return this.getInstance().dom; }, /** * Returns: DOMElement */ getContainer: function() { return this.getInstance().getContainer(); }, /** * Get the closest node matching the current selection. * * Returns: {jQuery} DOMElement */ getSelectedNode: function() { return this.getInstance().selection.getNode(); }, /** * Select the given node within the editor DOM * * Parameters: {DOMElement} */ selectNode: function(node) { this.getInstance().selection.select(node); }, /** * Replace entire content * * @param String HTML * @param Object opts */ setContent: function(html, opts) { this.getInstance().execCommand('mceSetContent', false, html, opts); }, /** * Insert content at the current caret position * * @param String 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, opts) { this.getInstance().execCommand("mceInsertLink", false, attrs, opts); }, /** * Remove the link from the currently selected node (if any). */ removeLink: function() { this.getInstance().execCommand('unlink', false); }, /** * Strip any editor-specific notation from link in order to make it presentable in the UI. * * Parameters: * {Object} * {DOMElement} */ cleanLink: function(href, node) { var cb = tinyMCE.settings['urlconverter_callback']; if(cb) href = eval(cb + "(href, node, true);"); // Turn into relative if(href.match(new RegExp('^' + tinyMCE.settings['document_base_url'] + '(.*)$'))) { href = RegExp.$1; } // Get rid of TinyMCE's temporary URLs 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); this.getInstance().focus(); }, /** * Removes any selection & de-focuses this editor */ blur: function() { this.getInstance().selection.collapse(); }, /** * Add new undo point with the current DOM content. */ addUndo: function() { this.getInstance().undoManager.add(); } }; }); // Override this to switch editor wrappers ss.editorWrappers['default'] = ss.editorWrappers.tinyMCE; (function($) { $.entwine('ss', function($) { /** * Class: textarea.htmleditor * * Add tinymce to HtmlEditorFields within the CMS. Works in combination * with a TinyMCE.init() call which is prepopulated with the used HTMLEditorConfig settings, * and included in the page as an inline