annotate media/js/tiny_mce/plugins/autosave/editor_plugin_src.js @ 271:4746df47a538

Follow on to last rev (r292) for #126. Missed updating a shoutbox template. Also the repoze.timeago package uses UTC time by default. Change this to local time for now until we decide to switch over to UTC for everything.
author Brian Neal <bgneal@gmail.com>
date Sun, 26 Sep 2010 17:42:00 +0000
parents 6ed2932901fa
children
rev   line source
bgneal@45 1 /**
bgneal@183 2 * editor_plugin_src.js
bgneal@45 3 *
bgneal@183 4 * Copyright 2009, Moxiecode Systems AB
bgneal@183 5 * Released under LGPL License.
bgneal@183 6 *
bgneal@183 7 * License: http://tinymce.moxiecode.com/license
bgneal@183 8 * Contributing: http://tinymce.moxiecode.com/contributing
bgneal@183 9 *
bgneal@183 10 * Adds auto-save capability to the TinyMCE text editor to rescue content
bgneal@183 11 * inadvertently lost. This plugin was originally developed by Speednet
bgneal@183 12 * and that project can be found here: http://code.google.com/p/tinyautosave/
bgneal@183 13 *
bgneal@183 14 * TECHNOLOGY DISCUSSION:
bgneal@183 15 *
bgneal@183 16 * The plugin attempts to use the most advanced features available in the current browser to save
bgneal@183 17 * as much content as possible. There are a total of four different methods used to autosave the
bgneal@183 18 * content. In order of preference, they are:
bgneal@183 19 *
bgneal@183 20 * 1. localStorage - A new feature of HTML 5, localStorage can store megabytes of data per domain
bgneal@183 21 * on the client computer. Data stored in the localStorage area has no expiration date, so we must
bgneal@183 22 * manage expiring the data ourselves. localStorage is fully supported by IE8, and it is supposed
bgneal@183 23 * to be working in Firefox 3 and Safari 3.2, but in reality is is flaky in those browsers. As
bgneal@183 24 * HTML 5 gets wider support, the AutoSave plugin will use it automatically. In Windows Vista/7,
bgneal@183 25 * localStorage is stored in the following folder:
bgneal@183 26 * C:\Users\[username]\AppData\Local\Microsoft\Internet Explorer\DOMStore\[tempFolder]
bgneal@183 27 *
bgneal@183 28 * 2. sessionStorage - A new feature of HTML 5, sessionStorage works similarly to localStorage,
bgneal@183 29 * except it is designed to expire after a certain amount of time. Because the specification
bgneal@183 30 * around expiration date/time is very loosely-described, it is preferrable to use locaStorage and
bgneal@183 31 * manage the expiration ourselves. sessionStorage has similar storage characteristics to
bgneal@183 32 * localStorage, although it seems to have better support by Firefox 3 at the moment. (That will
bgneal@183 33 * certainly change as Firefox continues getting better at HTML 5 adoption.)
bgneal@183 34 *
bgneal@183 35 * 3. UserData - A very under-exploited feature of Microsoft Internet Explorer, UserData is a
bgneal@183 36 * way to store up to 128K of data per "document", or up to 1MB of data per domain, on the client
bgneal@183 37 * computer. The feature is available for IE 5+, which makes it available for every version of IE
bgneal@183 38 * supported by TinyMCE. The content is persistent across browser restarts and expires on the
bgneal@183 39 * date/time specified, just like a cookie. However, the data is not cleared when the user clears
bgneal@183 40 * cookies on the browser, which makes it well-suited for rescuing autosaved content. UserData,
bgneal@183 41 * like other Microsoft IE browser technologies, is implemented as a behavior attached to a
bgneal@183 42 * specific DOM object, so in this case we attach the behavior to the same DOM element that the
bgneal@183 43 * TinyMCE editor instance is attached to.
bgneal@45 44 */
bgneal@45 45
bgneal@183 46 (function(tinymce) {
bgneal@183 47 // Setup constants to help the compressor to reduce script size
bgneal@183 48 var PLUGIN_NAME = 'autosave',
bgneal@183 49 RESTORE_DRAFT = 'restoredraft',
bgneal@183 50 TRUE = true,
bgneal@183 51 undefined,
bgneal@183 52 unloadHandlerAdded,
bgneal@183 53 Dispatcher = tinymce.util.Dispatcher;
bgneal@183 54
bgneal@183 55 /**
bgneal@183 56 * This plugin adds auto-save capability to the TinyMCE text editor to rescue content
bgneal@183 57 * inadvertently lost. By using localStorage.
bgneal@183 58 *
bgneal@183 59 * @class tinymce.plugins.AutoSave
bgneal@183 60 */
bgneal@183 61 tinymce.create('tinymce.plugins.AutoSave', {
bgneal@183 62 /**
bgneal@183 63 * Initializes the plugin, this will be executed after the plugin has been created.
bgneal@183 64 * This call is done before the editor instance has finished it's initialization so use the onInit event
bgneal@183 65 * of the editor instance to intercept that event.
bgneal@183 66 *
bgneal@183 67 * @method init
bgneal@183 68 * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
bgneal@183 69 * @param {string} url Absolute URL to where the plugin is located.
bgneal@183 70 */
bgneal@45 71 init : function(ed, url) {
bgneal@183 72 var self = this, settings = ed.settings;
bgneal@45 73
bgneal@183 74 self.editor = ed;
bgneal@45 75
bgneal@183 76 // Parses the specified time string into a milisecond number 10m, 10s etc.
bgneal@183 77 function parseTime(time) {
bgneal@183 78 var multipels = {
bgneal@183 79 s : 1000,
bgneal@183 80 m : 60000
bgneal@183 81 };
bgneal@183 82
bgneal@183 83 time = /^(\d+)([ms]?)$/.exec('' + time);
bgneal@183 84
bgneal@183 85 return (time[2] ? multipels[time[2]] : 1) * parseInt(time);
bgneal@183 86 };
bgneal@183 87
bgneal@183 88 // Default config
bgneal@183 89 tinymce.each({
bgneal@183 90 ask_before_unload : TRUE,
bgneal@183 91 interval : '30s',
bgneal@183 92 retention : '20m',
bgneal@183 93 minlength : 50
bgneal@183 94 }, function(value, key) {
bgneal@183 95 key = PLUGIN_NAME + '_' + key;
bgneal@183 96
bgneal@183 97 if (settings[key] === undefined)
bgneal@183 98 settings[key] = value;
bgneal@183 99 });
bgneal@183 100
bgneal@183 101 // Parse times
bgneal@183 102 settings.autosave_interval = parseTime(settings.autosave_interval);
bgneal@183 103 settings.autosave_retention = parseTime(settings.autosave_retention);
bgneal@183 104
bgneal@183 105 // Register restore button
bgneal@183 106 ed.addButton(RESTORE_DRAFT, {
bgneal@183 107 title : PLUGIN_NAME + ".restore_content",
bgneal@183 108 onclick : function() {
bgneal@247 109 if (ed.getContent({draft: true}).replace(/\s|&nbsp;|<\/?p[^>]*>|<br[^>]*>/gi, "").length > 0) {
bgneal@183 110 // Show confirm dialog if the editor isn't empty
bgneal@183 111 ed.windowManager.confirm(
bgneal@183 112 PLUGIN_NAME + ".warning_message",
bgneal@183 113 function(ok) {
bgneal@183 114 if (ok)
bgneal@183 115 self.restoreDraft();
bgneal@183 116 }
bgneal@183 117 );
bgneal@183 118 } else
bgneal@183 119 self.restoreDraft();
bgneal@183 120 }
bgneal@183 121 });
bgneal@183 122
bgneal@183 123 // Enable/disable restoredraft button depending on if there is a draft stored or not
bgneal@183 124 ed.onNodeChange.add(function() {
bgneal@183 125 var controlManager = ed.controlManager;
bgneal@183 126
bgneal@183 127 if (controlManager.get(RESTORE_DRAFT))
bgneal@183 128 controlManager.setDisabled(RESTORE_DRAFT, !self.hasDraft());
bgneal@183 129 });
bgneal@183 130
bgneal@183 131 ed.onInit.add(function() {
bgneal@183 132 // Check if the user added the restore button, then setup auto storage logic
bgneal@183 133 if (ed.controlManager.get(RESTORE_DRAFT)) {
bgneal@183 134 // Setup storage engine
bgneal@183 135 self.setupStorage(ed);
bgneal@183 136
bgneal@183 137 // Auto save contents each interval time
bgneal@183 138 setInterval(function() {
bgneal@183 139 self.storeDraft();
bgneal@183 140 ed.nodeChanged();
bgneal@183 141 }, settings.autosave_interval);
bgneal@183 142 }
bgneal@183 143 });
bgneal@183 144
bgneal@183 145 /**
bgneal@183 146 * This event gets fired when a draft is stored to local storage.
bgneal@183 147 *
bgneal@183 148 * @event onStoreDraft
bgneal@183 149 * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
bgneal@183 150 * @param {Object} draft Draft object containing the HTML contents of the editor.
bgneal@183 151 */
bgneal@183 152 self.onStoreDraft = new Dispatcher(self);
bgneal@183 153
bgneal@183 154 /**
bgneal@183 155 * This event gets fired when a draft is restored from local storage.
bgneal@183 156 *
bgneal@183 157 * @event onStoreDraft
bgneal@183 158 * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
bgneal@183 159 * @param {Object} draft Draft object containing the HTML contents of the editor.
bgneal@183 160 */
bgneal@183 161 self.onRestoreDraft = new Dispatcher(self);
bgneal@183 162
bgneal@183 163 /**
bgneal@183 164 * This event gets fired when a draft removed/expired.
bgneal@183 165 *
bgneal@183 166 * @event onRemoveDraft
bgneal@183 167 * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
bgneal@183 168 * @param {Object} draft Draft object containing the HTML contents of the editor.
bgneal@183 169 */
bgneal@183 170 self.onRemoveDraft = new Dispatcher(self);
bgneal@183 171
bgneal@183 172 // Add ask before unload dialog only add one unload handler
bgneal@183 173 if (!unloadHandlerAdded) {
bgneal@183 174 window.onbeforeunload = tinymce.plugins.AutoSave._beforeUnloadHandler;
bgneal@183 175 unloadHandlerAdded = TRUE;
bgneal@183 176 }
bgneal@45 177 },
bgneal@45 178
bgneal@183 179 /**
bgneal@183 180 * Returns information about the plugin as a name/value array.
bgneal@183 181 * The current keys are longname, author, authorurl, infourl and version.
bgneal@183 182 *
bgneal@183 183 * @method getInfo
bgneal@183 184 * @return {Object} Name/value array containing information about the plugin.
bgneal@183 185 */
bgneal@45 186 getInfo : function() {
bgneal@45 187 return {
bgneal@45 188 longname : 'Auto save',
bgneal@45 189 author : 'Moxiecode Systems AB',
bgneal@45 190 authorurl : 'http://tinymce.moxiecode.com',
bgneal@45 191 infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autosave',
bgneal@45 192 version : tinymce.majorVersion + "." + tinymce.minorVersion
bgneal@45 193 };
bgneal@45 194 },
bgneal@45 195
bgneal@183 196 /**
bgneal@183 197 * Returns an expiration date UTC string.
bgneal@183 198 *
bgneal@183 199 * @method getExpDate
bgneal@183 200 * @return {String} Expiration date UTC string.
bgneal@183 201 */
bgneal@183 202 getExpDate : function() {
bgneal@183 203 return new Date(
bgneal@183 204 new Date().getTime() + this.editor.settings.autosave_retention
bgneal@183 205 ).toUTCString();
bgneal@183 206 },
bgneal@45 207
bgneal@183 208 /**
bgneal@183 209 * This method will setup the storage engine. If the browser has support for it.
bgneal@183 210 *
bgneal@183 211 * @method setupStorage
bgneal@183 212 */
bgneal@183 213 setupStorage : function(ed) {
bgneal@183 214 var self = this, testKey = PLUGIN_NAME + '_test', testVal = "OK";
bgneal@183 215
bgneal@183 216 self.key = PLUGIN_NAME + ed.id;
bgneal@183 217
bgneal@183 218 // Loop though each storage engine type until we find one that works
bgneal@183 219 tinymce.each([
bgneal@183 220 function() {
bgneal@183 221 // Try HTML5 Local Storage
bgneal@183 222 if (localStorage) {
bgneal@183 223 localStorage.setItem(testKey, testVal);
bgneal@183 224
bgneal@183 225 if (localStorage.getItem(testKey) === testVal) {
bgneal@183 226 localStorage.removeItem(testKey);
bgneal@183 227
bgneal@183 228 return localStorage;
bgneal@183 229 }
bgneal@183 230 }
bgneal@183 231 },
bgneal@183 232
bgneal@183 233 function() {
bgneal@183 234 // Try HTML5 Session Storage
bgneal@183 235 if (sessionStorage) {
bgneal@183 236 sessionStorage.setItem(testKey, testVal);
bgneal@183 237
bgneal@183 238 if (sessionStorage.getItem(testKey) === testVal) {
bgneal@183 239 sessionStorage.removeItem(testKey);
bgneal@183 240
bgneal@183 241 return sessionStorage;
bgneal@183 242 }
bgneal@183 243 }
bgneal@183 244 },
bgneal@183 245
bgneal@183 246 function() {
bgneal@183 247 // Try IE userData
bgneal@183 248 if (tinymce.isIE) {
bgneal@183 249 ed.getElement().style.behavior = "url('#default#userData')";
bgneal@183 250
bgneal@183 251 // Fake localStorage on old IE
bgneal@183 252 return {
bgneal@183 253 autoExpires : TRUE,
bgneal@183 254
bgneal@183 255 setItem : function(key, value) {
bgneal@183 256 var userDataElement = ed.getElement();
bgneal@183 257
bgneal@183 258 userDataElement.setAttribute(key, value);
bgneal@183 259 userDataElement.expires = self.getExpDate();
bgneal@183 260 userDataElement.save("TinyMCE");
bgneal@183 261 },
bgneal@183 262
bgneal@183 263 getItem : function(key) {
bgneal@183 264 var userDataElement = ed.getElement();
bgneal@183 265
bgneal@183 266 userDataElement.load("TinyMCE");
bgneal@183 267
bgneal@183 268 return userDataElement.getAttribute(key);
bgneal@183 269 },
bgneal@183 270
bgneal@183 271 removeItem : function(key) {
bgneal@183 272 ed.getElement().removeAttribute(key);
bgneal@183 273 }
bgneal@183 274 };
bgneal@183 275 }
bgneal@183 276 },
bgneal@183 277 ], function(setup) {
bgneal@183 278 // Try executing each function to find a suitable storage engine
bgneal@183 279 try {
bgneal@183 280 self.storage = setup();
bgneal@183 281
bgneal@183 282 if (self.storage)
bgneal@183 283 return false;
bgneal@183 284 } catch (e) {
bgneal@183 285 // Ignore
bgneal@183 286 }
bgneal@183 287 });
bgneal@183 288 },
bgneal@183 289
bgneal@183 290 /**
bgneal@183 291 * This method will store the current contents in the the storage engine.
bgneal@183 292 *
bgneal@183 293 * @method storeDraft
bgneal@183 294 */
bgneal@183 295 storeDraft : function() {
bgneal@183 296 var self = this, storage = self.storage, editor = self.editor, expires, content;
bgneal@183 297
bgneal@183 298 // Is the contents dirty
bgneal@183 299 if (storage) {
bgneal@183 300 // If there is no existing key and the contents hasn't been changed since
bgneal@183 301 // it's original value then there is no point in saving a draft
bgneal@183 302 if (!storage.getItem(self.key) && !editor.isDirty())
bgneal@183 303 return;
bgneal@183 304
bgneal@183 305 // Store contents if the contents if longer than the minlength of characters
bgneal@247 306 content = editor.getContent({draft: true});
bgneal@183 307 if (content.length > editor.settings.autosave_minlength) {
bgneal@183 308 expires = self.getExpDate();
bgneal@183 309
bgneal@183 310 // Store expiration date if needed IE userData has auto expire built in
bgneal@183 311 if (!self.storage.autoExpires)
bgneal@183 312 self.storage.setItem(self.key + "_expires", expires);
bgneal@183 313
bgneal@183 314 self.storage.setItem(self.key, content);
bgneal@183 315 self.onStoreDraft.dispatch(self, {
bgneal@183 316 expires : expires,
bgneal@183 317 content : content
bgneal@183 318 });
bgneal@183 319 }
bgneal@183 320 }
bgneal@183 321 },
bgneal@183 322
bgneal@183 323 /**
bgneal@183 324 * This method will restore the contents from the storage engine back to the editor.
bgneal@183 325 *
bgneal@183 326 * @method restoreDraft
bgneal@183 327 */
bgneal@183 328 restoreDraft : function() {
bgneal@183 329 var self = this, storage = self.storage;
bgneal@183 330
bgneal@183 331 if (storage) {
bgneal@183 332 content = storage.getItem(self.key);
bgneal@183 333
bgneal@183 334 if (content) {
bgneal@183 335 self.editor.setContent(content);
bgneal@183 336 self.onRestoreDraft.dispatch(self, {
bgneal@183 337 content : content
bgneal@183 338 });
bgneal@183 339 }
bgneal@183 340 }
bgneal@183 341 },
bgneal@183 342
bgneal@183 343 /**
bgneal@183 344 * This method will return true/false if there is a local storage draft available.
bgneal@183 345 *
bgneal@183 346 * @method hasDraft
bgneal@183 347 * @return {boolean} true/false state if there is a local draft.
bgneal@183 348 */
bgneal@183 349 hasDraft : function() {
bgneal@183 350 var self = this, storage = self.storage, expDate, exists;
bgneal@183 351
bgneal@183 352 if (storage) {
bgneal@183 353 // Does the item exist at all
bgneal@183 354 exists = !!storage.getItem(self.key);
bgneal@183 355 if (exists) {
bgneal@183 356 // Storage needs autoexpire
bgneal@183 357 if (!self.storage.autoExpires) {
bgneal@183 358 expDate = new Date(storage.getItem(self.key + "_expires"));
bgneal@183 359
bgneal@183 360 // Contents hasn't expired
bgneal@183 361 if (new Date().getTime() < expDate.getTime())
bgneal@183 362 return TRUE;
bgneal@183 363
bgneal@183 364 // Remove it if it has
bgneal@183 365 self.removeDraft();
bgneal@183 366 } else
bgneal@183 367 return TRUE;
bgneal@183 368 }
bgneal@183 369 }
bgneal@183 370
bgneal@183 371 return false;
bgneal@183 372 },
bgneal@183 373
bgneal@183 374 /**
bgneal@183 375 * Removes the currently stored draft.
bgneal@183 376 *
bgneal@183 377 * @method removeDraft
bgneal@183 378 */
bgneal@183 379 removeDraft : function() {
bgneal@183 380 var self = this, storage = self.storage, key = self.key, content;
bgneal@183 381
bgneal@183 382 if (storage) {
bgneal@183 383 // Get current contents and remove the existing draft
bgneal@183 384 content = storage.getItem(key);
bgneal@183 385 storage.removeItem(key);
bgneal@183 386 storage.removeItem(key + "_expires");
bgneal@183 387
bgneal@183 388 // Dispatch remove event if we had any contents
bgneal@183 389 if (content) {
bgneal@183 390 self.onRemoveDraft.dispatch(self, {
bgneal@183 391 content : content
bgneal@183 392 });
bgneal@183 393 }
bgneal@183 394 }
bgneal@183 395 },
bgneal@183 396
bgneal@183 397 "static" : {
bgneal@183 398 // Internal unload handler will be called before the page is unloaded
bgneal@183 399 _beforeUnloadHandler : function(e) {
bgneal@45 400 var msg;
bgneal@45 401
bgneal@45 402 tinymce.each(tinyMCE.editors, function(ed) {
bgneal@183 403 // Store a draft for each editor instance
bgneal@183 404 if (ed.plugins.autosave)
bgneal@183 405 ed.plugins.autosave.storeDraft();
bgneal@183 406
bgneal@183 407 // Never ask in fullscreen mode
bgneal@45 408 if (ed.getParam("fullscreen_is_enabled"))
bgneal@45 409 return;
bgneal@45 410
bgneal@183 411 // Setup a return message if the editor is dirty
bgneal@183 412 if (!msg && ed.isDirty() && ed.getParam("autosave_ask_before_unload"))
bgneal@45 413 msg = ed.getLang("autosave.unload_msg");
bgneal@45 414 });
bgneal@45 415
bgneal@45 416 return msg;
bgneal@45 417 }
bgneal@45 418 }
bgneal@45 419 });
bgneal@45 420
bgneal@183 421 tinymce.PluginManager.add('autosave', tinymce.plugins.AutoSave);
bgneal@183 422 })(tinymce);