1 |
3 |
ahitrov@rambler.ru |
/** |
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().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 |
|
|
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(); |
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); |