Subversion-Projekte lars-tiefland.content-management

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

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