editor_plugin_src.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  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. (function(tinymce) {
  46. // Setup constants to help the compressor to reduce script size
  47. var PLUGIN_NAME = 'autosave',
  48. RESTORE_DRAFT = 'restoredraft',
  49. TRUE = true,
  50. undefined,
  51. unloadHandlerAdded,
  52. Dispatcher = tinymce.util.Dispatcher;
  53. /**
  54. * This plugin adds auto-save capability to the TinyMCE text editor to rescue content
  55. * inadvertently lost. By using localStorage.
  56. *
  57. * @class tinymce.plugins.AutoSave
  58. */
  59. tinymce.create('tinymce.plugins.AutoSave', {
  60. /**
  61. * Initializes the plugin, this will be executed after the plugin has been created.
  62. * This call is done before the editor instance has finished it's initialization so use the onInit event
  63. * of the editor instance to intercept that event.
  64. *
  65. * @method init
  66. * @param {tinymce.Editor} ed Editor instance that the plugin is initialized in.
  67. * @param {string} url Absolute URL to where the plugin is located.
  68. */
  69. init : function(ed, url) {
  70. var self = this, settings = ed.settings;
  71. self.editor = ed;
  72. // Parses the specified time string into a milisecond number 10m, 10s etc.
  73. function parseTime(time) {
  74. var multipels = {
  75. s : 1000,
  76. m : 60000
  77. };
  78. time = /^(\d+)([ms]?)$/.exec('' + time);
  79. return (time[2] ? multipels[time[2]] : 1) * parseInt(time);
  80. };
  81. // Default config
  82. tinymce.each({
  83. ask_before_unload : TRUE,
  84. interval : '30s',
  85. retention : '20m',
  86. minlength : 50
  87. }, function(value, key) {
  88. key = PLUGIN_NAME + '_' + key;
  89. if (settings[key] === undefined)
  90. settings[key] = value;
  91. });
  92. // Parse times
  93. settings.autosave_interval = parseTime(settings.autosave_interval);
  94. settings.autosave_retention = parseTime(settings.autosave_retention);
  95. // Register restore button
  96. ed.addButton(RESTORE_DRAFT, {
  97. title : PLUGIN_NAME + ".restore_content",
  98. onclick : function() {
  99. if (ed.getContent({draft: true}).replace(/\s|&nbsp;|<\/?p[^>]*>|<br[^>]*>/gi, "").length > 0) {
  100. // Show confirm dialog if the editor isn't empty
  101. ed.windowManager.confirm(
  102. PLUGIN_NAME + ".warning_message",
  103. function(ok) {
  104. if (ok)
  105. self.restoreDraft();
  106. }
  107. );
  108. } else
  109. self.restoreDraft();
  110. }
  111. });
  112. // Enable/disable restoredraft button depending on if there is a draft stored or not
  113. ed.onNodeChange.add(function() {
  114. var controlManager = ed.controlManager;
  115. if (controlManager.get(RESTORE_DRAFT))
  116. controlManager.setDisabled(RESTORE_DRAFT, !self.hasDraft());
  117. });
  118. ed.onInit.add(function() {
  119. // Check if the user added the restore button, then setup auto storage logic
  120. if (ed.controlManager.get(RESTORE_DRAFT)) {
  121. // Setup storage engine
  122. self.setupStorage(ed);
  123. // Auto save contents each interval time
  124. setInterval(function() {
  125. if (!ed.removed) {
  126. self.storeDraft();
  127. ed.nodeChanged();
  128. }
  129. }, settings.autosave_interval);
  130. }
  131. });
  132. /**
  133. * This event gets fired when a draft is stored to local storage.
  134. *
  135. * @event onStoreDraft
  136. * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
  137. * @param {Object} draft Draft object containing the HTML contents of the editor.
  138. */
  139. self.onStoreDraft = new Dispatcher(self);
  140. /**
  141. * This event gets fired when a draft is restored from local storage.
  142. *
  143. * @event onStoreDraft
  144. * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
  145. * @param {Object} draft Draft object containing the HTML contents of the editor.
  146. */
  147. self.onRestoreDraft = new Dispatcher(self);
  148. /**
  149. * This event gets fired when a draft removed/expired.
  150. *
  151. * @event onRemoveDraft
  152. * @param {tinymce.plugins.AutoSave} sender Plugin instance sending the event.
  153. * @param {Object} draft Draft object containing the HTML contents of the editor.
  154. */
  155. self.onRemoveDraft = new Dispatcher(self);
  156. // Add ask before unload dialog only add one unload handler
  157. if (!unloadHandlerAdded) {
  158. window.onbeforeunload = tinymce.plugins.AutoSave._beforeUnloadHandler;
  159. unloadHandlerAdded = TRUE;
  160. }
  161. },
  162. /**
  163. * Returns information about the plugin as a name/value array.
  164. * The current keys are longname, author, authorurl, infourl and version.
  165. *
  166. * @method getInfo
  167. * @return {Object} Name/value array containing information about the plugin.
  168. */
  169. getInfo : function() {
  170. return {
  171. longname : 'Auto save',
  172. author : 'Moxiecode Systems AB',
  173. authorurl : 'http://tinymce.moxiecode.com',
  174. infourl : 'http://wiki.moxiecode.com/index.php/TinyMCE:Plugins/autosave',
  175. version : tinymce.majorVersion + "." + tinymce.minorVersion
  176. };
  177. },
  178. /**
  179. * Returns an expiration date UTC string.
  180. *
  181. * @method getExpDate
  182. * @return {String} Expiration date UTC string.
  183. */
  184. getExpDate : function() {
  185. return new Date(
  186. new Date().getTime() + this.editor.settings.autosave_retention
  187. ).toUTCString();
  188. },
  189. /**
  190. * This method will setup the storage engine. If the browser has support for it.
  191. *
  192. * @method setupStorage
  193. */
  194. setupStorage : function(ed) {
  195. var self = this, testKey = PLUGIN_NAME + '_test', testVal = "OK";
  196. self.key = PLUGIN_NAME + ed.id;
  197. // Loop though each storage engine type until we find one that works
  198. tinymce.each([
  199. function() {
  200. // Try HTML5 Local Storage
  201. if (localStorage) {
  202. localStorage.setItem(testKey, testVal);
  203. if (localStorage.getItem(testKey) === testVal) {
  204. localStorage.removeItem(testKey);
  205. return localStorage;
  206. }
  207. }
  208. },
  209. function() {
  210. // Try HTML5 Session Storage
  211. if (sessionStorage) {
  212. sessionStorage.setItem(testKey, testVal);
  213. if (sessionStorage.getItem(testKey) === testVal) {
  214. sessionStorage.removeItem(testKey);
  215. return sessionStorage;
  216. }
  217. }
  218. },
  219. function() {
  220. // Try IE userData
  221. if (tinymce.isIE) {
  222. ed.getElement().style.behavior = "url('#default#userData')";
  223. // Fake localStorage on old IE
  224. return {
  225. autoExpires : TRUE,
  226. setItem : function(key, value) {
  227. var userDataElement = ed.getElement();
  228. userDataElement.setAttribute(key, value);
  229. userDataElement.expires = self.getExpDate();
  230. try {
  231. userDataElement.save("TinyMCE");
  232. } catch (e) {
  233. // Ignore, saving might fail if "Userdata Persistence" is disabled in IE
  234. }
  235. },
  236. getItem : function(key) {
  237. var userDataElement = ed.getElement();
  238. try {
  239. userDataElement.load("TinyMCE");
  240. return userDataElement.getAttribute(key);
  241. } catch (e) {
  242. // Ignore, loading might fail if "Userdata Persistence" is disabled in IE
  243. return null;
  244. }
  245. },
  246. removeItem : function(key) {
  247. ed.getElement().removeAttribute(key);
  248. }
  249. };
  250. }
  251. },
  252. ], function(setup) {
  253. // Try executing each function to find a suitable storage engine
  254. try {
  255. self.storage = setup();
  256. if (self.storage)
  257. return false;
  258. } catch (e) {
  259. // Ignore
  260. }
  261. });
  262. },
  263. /**
  264. * This method will store the current contents in the the storage engine.
  265. *
  266. * @method storeDraft
  267. */
  268. storeDraft : function() {
  269. var self = this, storage = self.storage, editor = self.editor, expires, content;
  270. // Is the contents dirty
  271. if (storage) {
  272. // If there is no existing key and the contents hasn't been changed since
  273. // it's original value then there is no point in saving a draft
  274. if (!storage.getItem(self.key) && !editor.isDirty())
  275. return;
  276. // Store contents if the contents if longer than the minlength of characters
  277. content = editor.getContent({draft: true});
  278. if (content.length > editor.settings.autosave_minlength) {
  279. expires = self.getExpDate();
  280. // Store expiration date if needed IE userData has auto expire built in
  281. if (!self.storage.autoExpires)
  282. self.storage.setItem(self.key + "_expires", expires);
  283. self.storage.setItem(self.key, content);
  284. self.onStoreDraft.dispatch(self, {
  285. expires : expires,
  286. content : content
  287. });
  288. }
  289. }
  290. },
  291. /**
  292. * This method will restore the contents from the storage engine back to the editor.
  293. *
  294. * @method restoreDraft
  295. */
  296. restoreDraft : function() {
  297. var self = this, storage = self.storage, content;
  298. if (storage) {
  299. content = storage.getItem(self.key);
  300. if (content) {
  301. self.editor.setContent(content);
  302. self.onRestoreDraft.dispatch(self, {
  303. content : content
  304. });
  305. }
  306. }
  307. },
  308. /**
  309. * This method will return true/false if there is a local storage draft available.
  310. *
  311. * @method hasDraft
  312. * @return {boolean} true/false state if there is a local draft.
  313. */
  314. hasDraft : function() {
  315. var self = this, storage = self.storage, expDate, exists;
  316. if (storage) {
  317. // Does the item exist at all
  318. exists = !!storage.getItem(self.key);
  319. if (exists) {
  320. // Storage needs autoexpire
  321. if (!self.storage.autoExpires) {
  322. expDate = new Date(storage.getItem(self.key + "_expires"));
  323. // Contents hasn't expired
  324. if (new Date().getTime() < expDate.getTime())
  325. return TRUE;
  326. // Remove it if it has
  327. self.removeDraft();
  328. } else
  329. return TRUE;
  330. }
  331. }
  332. return false;
  333. },
  334. /**
  335. * Removes the currently stored draft.
  336. *
  337. * @method removeDraft
  338. */
  339. removeDraft : function() {
  340. var self = this, storage = self.storage, key = self.key, content;
  341. if (storage) {
  342. // Get current contents and remove the existing draft
  343. content = storage.getItem(key);
  344. storage.removeItem(key);
  345. storage.removeItem(key + "_expires");
  346. // Dispatch remove event if we had any contents
  347. if (content) {
  348. self.onRemoveDraft.dispatch(self, {
  349. content : content
  350. });
  351. }
  352. }
  353. },
  354. "static" : {
  355. // Internal unload handler will be called before the page is unloaded
  356. _beforeUnloadHandler : function(e) {
  357. var msg;
  358. tinymce.each(tinyMCE.editors, function(ed) {
  359. // Store a draft for each editor instance
  360. if (ed.plugins.autosave)
  361. ed.plugins.autosave.storeDraft();
  362. // Never ask in fullscreen mode
  363. if (ed.getParam("fullscreen_is_enabled"))
  364. return;
  365. // Setup a return message if the editor is dirty
  366. if (!msg && ed.isDirty() && ed.getParam("autosave_ask_before_unload"))
  367. msg = ed.getLang("autosave.unload_msg");
  368. });
  369. return msg;
  370. }
  371. }
  372. });
  373. tinymce.PluginManager.add('autosave', tinymce.plugins.AutoSave);
  374. })(tinymce);