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