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|&nbsp;|<\/?p[^>]*>|<br[^>]*>/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);