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