Statistics
| Branch: | Tag: | Revision:

root / static / js / tinymce / plugins / autosave / editor_plugin_src.js @ 6ecbf4ec

History | View | Annotate | Download (13.1 kB)

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
                                                                userDataElement.save("TinyMCE");
261
                                                        },
262

    
263
                                                        getItem : function(key) {
264
                                                                var userDataElement = ed.getElement();
265

    
266
                                                                userDataElement.load("TinyMCE");
267

    
268
                                                                return userDataElement.getAttribute(key);
269
                                                        },
270

    
271
                                                        removeItem : function(key) {
272
                                                                ed.getElement().removeAttribute(key);
273
                                                        }
274
                                                };
275
                                        }
276
                                },
277
                        ], function(setup) {
278
                                // Try executing each function to find a suitable storage engine
279
                                try {
280
                                        self.storage = setup();
281

    
282
                                        if (self.storage)
283
                                                return false;
284
                                } catch (e) {
285
                                        // Ignore
286
                                }
287
                        });
288
                },
289

    
290
                /**
291
                 * This method will store the current contents in the the storage engine.
292
                 *
293
                 * @method storeDraft
294
                 */
295
                storeDraft : function() {
296
                        var self = this, storage = self.storage, editor = self.editor, expires, content;
297

    
298
                        // Is the contents dirty
299
                        if (storage) {
300
                                // If there is no existing key and the contents hasn't been changed since
301
                                // it's original value then there is no point in saving a draft
302
                                if (!storage.getItem(self.key) && !editor.isDirty())
303
                                        return;
304

    
305
                                // Store contents if the contents if longer than the minlength of characters
306
                                content = editor.getContent({draft: true});
307
                                if (content.length > editor.settings.autosave_minlength) {
308
                                        expires = self.getExpDate();
309

    
310
                                        // Store expiration date if needed IE userData has auto expire built in
311
                                        if (!self.storage.autoExpires)
312
                                                self.storage.setItem(self.key + "_expires", expires);
313

    
314
                                        self.storage.setItem(self.key, content);
315
                                        self.onStoreDraft.dispatch(self, {
316
                                                expires : expires,
317
                                                content : content
318
                                        });
319
                                }
320
                        }
321
                },
322

    
323
                /**
324
                 * This method will restore the contents from the storage engine back to the editor.
325
                 *
326
                 * @method restoreDraft
327
                 */
328
                restoreDraft : function() {
329
                        var self = this, storage = self.storage;
330

    
331
                        if (storage) {
332
                                content = storage.getItem(self.key);
333

    
334
                                if (content) {
335
                                        self.editor.setContent(content);
336
                                        self.onRestoreDraft.dispatch(self, {
337
                                                content : content
338
                                        });
339
                                }
340
                        }
341
                },
342

    
343
                /**
344
                 * This method will return true/false if there is a local storage draft available.
345
                 *
346
                 * @method hasDraft
347
                 * @return {boolean} true/false state if there is a local draft.
348
                 */
349
                hasDraft : function() {
350
                        var self = this, storage = self.storage, expDate, exists;
351

    
352
                        if (storage) {
353
                                // Does the item exist at all
354
                                exists = !!storage.getItem(self.key);
355
                                if (exists) {
356
                                        // Storage needs autoexpire
357
                                        if (!self.storage.autoExpires) {
358
                                                expDate = new Date(storage.getItem(self.key + "_expires"));
359

    
360
                                                // Contents hasn't expired
361
                                                if (new Date().getTime() < expDate.getTime())
362
                                                        return TRUE;
363

    
364
                                                // Remove it if it has
365
                                                self.removeDraft();
366
                                        } else
367
                                                return TRUE;
368
                                }
369
                        }
370

    
371
                        return false;
372
                },
373

    
374
                /**
375
                 * Removes the currently stored draft.
376
                 *
377
                 * @method removeDraft
378
                 */
379
                removeDraft : function() {
380
                        var self = this, storage = self.storage, key = self.key, content;
381

    
382
                        if (storage) {
383
                                // Get current contents and remove the existing draft
384
                                content = storage.getItem(key);
385
                                storage.removeItem(key);
386
                                storage.removeItem(key + "_expires");
387

    
388
                                // Dispatch remove event if we had any contents
389
                                if (content) {
390
                                        self.onRemoveDraft.dispatch(self, {
391
                                                content : content
392
                                        });
393
                                }
394
                        }
395
                },
396

    
397
                "static" : {
398
                        // Internal unload handler will be called before the page is unloaded
399
                        _beforeUnloadHandler : function(e) {
400
                                var msg;
401

    
402
                                tinymce.each(tinyMCE.editors, function(ed) {
403
                                        // Store a draft for each editor instance
404
                                        if (ed.plugins.autosave)
405
                                                ed.plugins.autosave.storeDraft();
406

    
407
                                        // Never ask in fullscreen mode
408
                                        if (ed.getParam("fullscreen_is_enabled"))
409
                                                return;
410

    
411
                                        // Setup a return message if the editor is dirty
412
                                        if (!msg && ed.isDirty() && ed.getParam("autosave_ask_before_unload"))
413
                                                msg = ed.getLang("autosave.unload_msg");
414
                                });
415

    
416
                                return msg;
417
                        }
418
                }
419
        });
420

    
421
        tinymce.PluginManager.add('autosave', tinymce.plugins.AutoSave);
422
})(tinymce);