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| |<\/?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);
|