bgneal@312: /** bgneal@312: * editor_plugin_src.js bgneal@312: * bgneal@312: * Copyright 2009, Moxiecode Systems AB bgneal@312: * Released under LGPL License. bgneal@312: * bgneal@312: * License: http://tinymce.moxiecode.com/license bgneal@312: * Contributing: http://tinymce.moxiecode.com/contributing bgneal@312: * bgneal@312: * Adds auto-save capability to the TinyMCE text editor to rescue content bgneal@312: * inadvertently lost. This plugin was originally developed by Speednet bgneal@312: * and that project can be found here: http://code.google.com/p/tinyautosave/ bgneal@312: * bgneal@312: * TECHNOLOGY DISCUSSION: bgneal@312: * bgneal@312: * The plugin attempts to use the most advanced features available in the current browser to save bgneal@312: * as much content as possible. There are a total of four different methods used to autosave the bgneal@312: * content. In order of preference, they are: bgneal@312: * bgneal@312: * 1. localStorage - A new feature of HTML 5, localStorage can store megabytes of data per domain bgneal@312: * on the client computer. Data stored in the localStorage area has no expiration date, so we must bgneal@312: * manage expiring the data ourselves. localStorage is fully supported by IE8, and it is supposed bgneal@312: * to be working in Firefox 3 and Safari 3.2, but in reality is is flaky in those browsers. As bgneal@312: * HTML 5 gets wider support, the AutoSave plugin will use it automatically. In Windows Vista/7, bgneal@312: * localStorage is stored in the following folder: bgneal@312: * C:\Users\[username]\AppData\Local\Microsoft\Internet Explorer\DOMStore\[tempFolder] bgneal@312: * bgneal@312: * 2. sessionStorage - A new feature of HTML 5, sessionStorage works similarly to localStorage, bgneal@312: * except it is designed to expire after a certain amount of time. Because the specification bgneal@312: * around expiration date/time is very loosely-described, it is preferrable to use locaStorage and bgneal@312: * manage the expiration ourselves. sessionStorage has similar storage characteristics to bgneal@312: * localStorage, although it seems to have better support by Firefox 3 at the moment. (That will bgneal@312: * certainly change as Firefox continues getting better at HTML 5 adoption.) bgneal@312: * bgneal@312: * 3. UserData - A very under-exploited feature of Microsoft Internet Explorer, UserData is a bgneal@312: * way to store up to 128K of data per "document", or up to 1MB of data per domain, on the client bgneal@312: * computer. The feature is available for IE 5+, which makes it available for every version of IE bgneal@312: * supported by TinyMCE. The content is persistent across browser restarts and expires on the bgneal@312: * date/time specified, just like a cookie. However, the data is not cleared when the user clears bgneal@312: * cookies on the browser, which makes it well-suited for rescuing autosaved content. UserData, bgneal@312: * like other Microsoft IE browser technologies, is implemented as a behavior attached to a bgneal@312: * specific DOM object, so in this case we attach the behavior to the same DOM element that the bgneal@312: * TinyMCE editor instance is attached to. bgneal@312: */ bgneal@312: bgneal@312: (function(tinymce) { bgneal@312: // Setup constants to help the compressor to reduce script size bgneal@312: var PLUGIN_NAME = 'autosave', bgneal@312: RESTORE_DRAFT = 'restoredraft', bgneal@312: TRUE = true, bgneal@312: undefined, bgneal@312: unloadHandlerAdded, bgneal@312: Dispatcher = tinymce.util.Dispatcher; bgneal@312: bgneal@312: /** bgneal@312: * This plugin adds auto-save capability to the TinyMCE text editor to rescue content bgneal@312: * inadvertently lost. By using localStorage. bgneal@312: * bgneal@312: * @class tinymce.plugins.AutoSave bgneal@312: */ bgneal@312: tinymce.create('tinymce.plugins.AutoSave', { bgneal@312: /** bgneal@312: * Initializes the plugin, this will be executed after the plugin has been created. bgneal@312: * This call is done before the editor instance has finished it's initialization so use the onInit event bgneal@312: * of the editor instance to intercept that event. bgneal@312: * bgneal@312: * @method init bgneal@312: * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in. bgneal@312: * @param {string} url Absolute URL to where the plugin is located. bgneal@312: */ bgneal@312: init : function(ed, url) { bgneal@312: var self = this, settings = ed.settings; bgneal@312: bgneal@312: self.editor = ed; bgneal@312: bgneal@312: // Parses the specified time string into a milisecond number 10m, 10s etc. bgneal@312: function parseTime(time) { bgneal@312: var multipels = { bgneal@312: s : 1000, bgneal@312: m : 60000 bgneal@312: }; bgneal@312: bgneal@312: time = /^(\d+)([ms]?)$/.exec('' + time); bgneal@312: bgneal@312: return (time[2] ? multipels[time[2]] : 1) * parseInt(time); bgneal@312: }; bgneal@312: bgneal@312: // Default config bgneal@312: tinymce.each({ bgneal@312: ask_before_unload : TRUE, bgneal@312: interval : '30s', bgneal@312: retention : '20m', bgneal@312: minlength : 50 bgneal@312: }, function(value, key) { bgneal@312: key = PLUGIN_NAME + '_' + key; bgneal@312: bgneal@312: if (settings[key] === undefined) bgneal@312: settings[key] = value; bgneal@312: }); bgneal@312: bgneal@312: // Parse times bgneal@312: settings.autosave_interval = parseTime(settings.autosave_interval); bgneal@312: settings.autosave_retention = parseTime(settings.autosave_retention); bgneal@312: bgneal@312: // Register restore button bgneal@312: ed.addButton(RESTORE_DRAFT, { bgneal@312: title : PLUGIN_NAME + ".restore_content", bgneal@312: onclick : function() { bgneal@312: if (ed.getContent({draft: true}).replace(/\s| |<\/?p[^>]*>|]*>/gi, "").length > 0) { bgneal@312: // Show confirm dialog if the editor isn't empty bgneal@312: ed.windowManager.confirm( bgneal@312: PLUGIN_NAME + ".warning_message", bgneal@312: function(ok) { bgneal@312: if (ok) bgneal@312: self.restoreDraft(); bgneal@312: } bgneal@312: ); bgneal@312: } else bgneal@312: self.restoreDraft(); bgneal@312: } bgneal@312: }); bgneal@312: bgneal@312: // Enable/disable restoredraft button depending on if there is a draft stored or not bgneal@312: ed.onNodeChange.add(function() { bgneal@312: var controlManager = ed.controlManager; bgneal@312: bgneal@312: if (controlManager.get(RESTORE_DRAFT)) bgneal@312: controlManager.setDisabled(RESTORE_DRAFT, !self.hasDraft()); bgneal@312: }); bgneal@312: bgneal@312: ed.onInit.add(function() { bgneal@312: // Check if the user added the restore button, then setup auto storage logic bgneal@312: if (ed.controlManager.get(RESTORE_DRAFT)) { bgneal@312: // Setup storage engine bgneal@312: self.setupStorage(ed); bgneal@312: bgneal@312: // Auto save contents each interval time bgneal@312: setInterval(function() { bgneal@312: self.storeDraft(); bgneal@312: ed.nodeChanged(); bgneal@312: }, settings.autosave_interval); bgneal@312: } bgneal@312: }); bgneal@312: bgneal@312: /** bgneal@312: * This event gets fired when a draft is stored to local storage. bgneal@312: * bgneal@312: * @event onStoreDraft bgneal@312: * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event. bgneal@312: * @param {Object} draft Draft object containing the HTML contents of the editor. bgneal@312: */ bgneal@312: self.onStoreDraft = new Dispatcher(self); bgneal@312: bgneal@312: /** bgneal@312: * This event gets fired when a draft is restored from local storage. bgneal@312: * bgneal@312: * @event onStoreDraft bgneal@312: * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event. bgneal@312: * @param {Object} draft Draft object containing the HTML contents of the editor. bgneal@312: */ bgneal@312: self.onRestoreDraft = new Dispatcher(self); bgneal@312: bgneal@312: /** bgneal@312: * This event gets fired when a draft removed/expired. bgneal@312: * bgneal@312: * @event onRemoveDraft bgneal@312: * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event. bgneal@312: * @param {Object} draft Draft object containing the HTML contents of the editor. bgneal@312: */ bgneal@312: self.onRemoveDraft = new Dispatcher(self); bgneal@312: bgneal@312: // Add ask before unload dialog only add one unload handler bgneal@312: if (!unloadHandlerAdded) { bgneal@312: window.onbeforeunload = tinymce.plugins.AutoSave._beforeUnloadHandler; bgneal@312: unloadHandlerAdded = TRUE; bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * Returns information about the plugin as a name/value array. bgneal@312: * The current keys are longname, author, authorurl, infourl and version. bgneal@312: * bgneal@312: * @method getInfo bgneal@312: * @return {Object} Name/value array containing information about the plugin. bgneal@312: */ bgneal@312: getInfo : function() { bgneal@312: return { bgneal@312: longname : 'Auto save', bgneal@312: author : 'Moxiecode Systems AB', bgneal@312: authorurl : 'http://tinymce.moxiecode.com', bgneal@312: infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autosave', bgneal@312: version : tinymce.majorVersion + "." + tinymce.minorVersion bgneal@312: }; bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * Returns an expiration date UTC string. bgneal@312: * bgneal@312: * @method getExpDate bgneal@312: * @return {String} Expiration date UTC string. bgneal@312: */ bgneal@312: getExpDate : function() { bgneal@312: return new Date( bgneal@312: new Date().getTime() + this.editor.settings.autosave_retention bgneal@312: ).toUTCString(); bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * This method will setup the storage engine. If the browser has support for it. bgneal@312: * bgneal@312: * @method setupStorage bgneal@312: */ bgneal@312: setupStorage : function(ed) { bgneal@312: var self = this, testKey = PLUGIN_NAME + '_test', testVal = "OK"; bgneal@312: bgneal@312: self.key = PLUGIN_NAME + ed.id; bgneal@312: bgneal@312: // Loop though each storage engine type until we find one that works bgneal@312: tinymce.each([ bgneal@312: function() { bgneal@312: // Try HTML5 Local Storage bgneal@312: if (localStorage) { bgneal@312: localStorage.setItem(testKey, testVal); bgneal@312: bgneal@312: if (localStorage.getItem(testKey) === testVal) { bgneal@312: localStorage.removeItem(testKey); bgneal@312: bgneal@312: return localStorage; bgneal@312: } bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: function() { bgneal@312: // Try HTML5 Session Storage bgneal@312: if (sessionStorage) { bgneal@312: sessionStorage.setItem(testKey, testVal); bgneal@312: bgneal@312: if (sessionStorage.getItem(testKey) === testVal) { bgneal@312: sessionStorage.removeItem(testKey); bgneal@312: bgneal@312: return sessionStorage; bgneal@312: } bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: function() { bgneal@312: // Try IE userData bgneal@312: if (tinymce.isIE) { bgneal@312: ed.getElement().style.behavior = "url('#default#userData')"; bgneal@312: bgneal@312: // Fake localStorage on old IE bgneal@312: return { bgneal@312: autoExpires : TRUE, bgneal@312: bgneal@312: setItem : function(key, value) { bgneal@312: var userDataElement = ed.getElement(); bgneal@312: bgneal@312: userDataElement.setAttribute(key, value); bgneal@312: userDataElement.expires = self.getExpDate(); bgneal@442: bgneal@442: try { bgneal@442: userDataElement.save("TinyMCE"); bgneal@442: } catch (e) { bgneal@442: // Ignore, saving might fail if "Userdata Persistence" is disabled in IE bgneal@442: } bgneal@312: }, bgneal@312: bgneal@312: getItem : function(key) { bgneal@312: var userDataElement = ed.getElement(); bgneal@312: bgneal@442: try { bgneal@442: userDataElement.load("TinyMCE"); bgneal@442: return userDataElement.getAttribute(key); bgneal@442: } catch (e) { bgneal@442: // Ignore, loading might fail if "Userdata Persistence" is disabled in IE bgneal@442: return null; bgneal@442: } bgneal@312: }, bgneal@312: bgneal@312: removeItem : function(key) { bgneal@312: ed.getElement().removeAttribute(key); bgneal@312: } bgneal@312: }; bgneal@312: } bgneal@312: }, bgneal@312: ], function(setup) { bgneal@312: // Try executing each function to find a suitable storage engine bgneal@312: try { bgneal@312: self.storage = setup(); bgneal@312: bgneal@312: if (self.storage) bgneal@312: return false; bgneal@312: } catch (e) { bgneal@312: // Ignore bgneal@312: } bgneal@312: }); bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * This method will store the current contents in the the storage engine. bgneal@312: * bgneal@312: * @method storeDraft bgneal@312: */ bgneal@312: storeDraft : function() { bgneal@312: var self = this, storage = self.storage, editor = self.editor, expires, content; bgneal@312: bgneal@312: // Is the contents dirty bgneal@312: if (storage) { bgneal@312: // If there is no existing key and the contents hasn't been changed since bgneal@312: // it's original value then there is no point in saving a draft bgneal@312: if (!storage.getItem(self.key) && !editor.isDirty()) bgneal@312: return; bgneal@312: bgneal@312: // Store contents if the contents if longer than the minlength of characters bgneal@312: content = editor.getContent({draft: true}); bgneal@312: if (content.length > editor.settings.autosave_minlength) { bgneal@312: expires = self.getExpDate(); bgneal@312: bgneal@312: // Store expiration date if needed IE userData has auto expire built in bgneal@312: if (!self.storage.autoExpires) bgneal@312: self.storage.setItem(self.key + "_expires", expires); bgneal@312: bgneal@312: self.storage.setItem(self.key, content); bgneal@312: self.onStoreDraft.dispatch(self, { bgneal@312: expires : expires, bgneal@312: content : content bgneal@312: }); bgneal@312: } bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * This method will restore the contents from the storage engine back to the editor. bgneal@312: * bgneal@312: * @method restoreDraft bgneal@312: */ bgneal@312: restoreDraft : function() { bgneal@312: var self = this, storage = self.storage; bgneal@312: bgneal@312: if (storage) { bgneal@312: content = storage.getItem(self.key); bgneal@312: bgneal@312: if (content) { bgneal@312: self.editor.setContent(content); bgneal@312: self.onRestoreDraft.dispatch(self, { bgneal@312: content : content bgneal@312: }); bgneal@312: } bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * This method will return true/false if there is a local storage draft available. bgneal@312: * bgneal@312: * @method hasDraft bgneal@312: * @return {boolean} true/false state if there is a local draft. bgneal@312: */ bgneal@312: hasDraft : function() { bgneal@312: var self = this, storage = self.storage, expDate, exists; bgneal@312: bgneal@312: if (storage) { bgneal@312: // Does the item exist at all bgneal@312: exists = !!storage.getItem(self.key); bgneal@312: if (exists) { bgneal@312: // Storage needs autoexpire bgneal@312: if (!self.storage.autoExpires) { bgneal@312: expDate = new Date(storage.getItem(self.key + "_expires")); bgneal@312: bgneal@312: // Contents hasn't expired bgneal@312: if (new Date().getTime() < expDate.getTime()) bgneal@312: return TRUE; bgneal@312: bgneal@312: // Remove it if it has bgneal@312: self.removeDraft(); bgneal@312: } else bgneal@312: return TRUE; bgneal@312: } bgneal@312: } bgneal@312: bgneal@312: return false; bgneal@312: }, bgneal@312: bgneal@312: /** bgneal@312: * Removes the currently stored draft. bgneal@312: * bgneal@312: * @method removeDraft bgneal@312: */ bgneal@312: removeDraft : function() { bgneal@312: var self = this, storage = self.storage, key = self.key, content; bgneal@312: bgneal@312: if (storage) { bgneal@312: // Get current contents and remove the existing draft bgneal@312: content = storage.getItem(key); bgneal@312: storage.removeItem(key); bgneal@312: storage.removeItem(key + "_expires"); bgneal@312: bgneal@312: // Dispatch remove event if we had any contents bgneal@312: if (content) { bgneal@312: self.onRemoveDraft.dispatch(self, { bgneal@312: content : content bgneal@312: }); bgneal@312: } bgneal@312: } bgneal@312: }, bgneal@312: bgneal@312: "static" : { bgneal@312: // Internal unload handler will be called before the page is unloaded bgneal@312: _beforeUnloadHandler : function(e) { bgneal@312: var msg; bgneal@312: bgneal@312: tinymce.each(tinyMCE.editors, function(ed) { bgneal@312: // Store a draft for each editor instance bgneal@312: if (ed.plugins.autosave) bgneal@312: ed.plugins.autosave.storeDraft(); bgneal@312: bgneal@312: // Never ask in fullscreen mode bgneal@312: if (ed.getParam("fullscreen_is_enabled")) bgneal@312: return; bgneal@312: bgneal@312: // Setup a return message if the editor is dirty bgneal@312: if (!msg && ed.isDirty() && ed.getParam("autosave_ask_before_unload")) bgneal@312: msg = ed.getLang("autosave.unload_msg"); bgneal@312: }); bgneal@312: bgneal@312: return msg; bgneal@312: } bgneal@312: } bgneal@312: }); bgneal@312: bgneal@312: tinymce.PluginManager.add('autosave', tinymce.plugins.AutoSave); bgneal@312: })(tinymce);