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