jquery.atd.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436
  1. /*
  2. * jquery.atd.js - jQuery powered writing check with After the Deadline
  3. * Author : Raphael Mudge, Automattic Inc.
  4. * License : LGPL or MIT License (take your pick)
  5. * Project : http://www.afterthedeadline.com/development.slp
  6. * Contact : raffi@automattic.com
  7. *
  8. * Derived from:
  9. *
  10. * jquery.spellchecker.js - a simple jQuery Spell Checker
  11. * Copyright (c) 2008 Richard Willis
  12. * MIT license : http://www.opensource.org/licenses/mit-license.php
  13. * Project : http://jquery-spellchecker.googlecode.com
  14. * Contact : willis.rh@gmail.com
  15. */
  16. /* jshint onevar: false, sub: true, smarttabs: true, loopfunc: true */
  17. /* global AtDCore, AtD_proofread_click_count, CSSHttpRequest, ActiveXObject */
  18. var AtD =
  19. {
  20. rpc : '', /* see the proxy.php that came with the AtD/TinyMCE plugin */
  21. rpc_css : 'http://www.polishmywriting.com/atd-jquery/server/proxycss.php?data=', /* you may use this, but be nice! */
  22. rpc_css_lang : 'en',
  23. api_key : '',
  24. i18n : {}, // Back-compat
  25. listener : {}
  26. };
  27. AtD.getLang = function( key, defaultk ) {
  28. return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultk;
  29. };
  30. AtD.addI18n = function( obj ) {
  31. // Back-compat
  32. window.AtD_l10n_r0ar = obj;
  33. };
  34. AtD.setIgnoreStrings = function(string) {
  35. AtD.core.setIgnoreStrings(string);
  36. };
  37. AtD.showTypes = function(string) {
  38. AtD.core.showTypes(string);
  39. };
  40. AtD.checkCrossAJAX = function(container_id, callback_f) {
  41. /* checks if a global var for click stats exists and increments it if it does... */
  42. if (typeof AtD_proofread_click_count !== 'undefined') {
  43. AtD_proofread_click_count++;
  44. }
  45. AtD.callback_f = callback_f; /* remember the callback for later */
  46. AtD.remove(container_id);
  47. var container = jQuery('#' + container_id);
  48. var text = jQuery.trim(container.html());
  49. text = text.replace(/\&lt;/g, '<').replace(/\&gt;/g, '>').replace(/\&amp;/g, '&');
  50. text = encodeURIComponent( text.replace( /\%/g, '%25' ) ); /* % not being escaped here creates problems, I don't know why. */
  51. /* do some sanity checks based on the browser */
  52. if ((text.length > 2000 && navigator.appName === 'Microsoft Internet Explorer') || text.length > 7800) {
  53. if (callback_f !== undefined && callback_f.error !== undefined) {
  54. callback_f.error('Maximum text length for this browser exceeded');
  55. }
  56. return;
  57. }
  58. /* do some cross-domain AJAX action with CSSHttpRequest */
  59. CSSHttpRequest.get(AtD.rpc_css + text + '&lang=' + AtD.rpc_css_lang + '&nocache=' + (new Date().getTime()), function(response) {
  60. /* do some magic to convert the response into an XML document */
  61. var xml;
  62. if (navigator.appName === 'Microsoft Internet Explorer') {
  63. xml = new ActiveXObject('Microsoft.XMLDOM');
  64. xml.async = false;
  65. xml.loadXML(response);
  66. } else {
  67. xml = (new DOMParser()).parseFromString(response, 'text/xml');
  68. }
  69. /* check for and display error messages from the server */
  70. if (AtD.core.hasErrorMessage(xml)) {
  71. if (AtD.callback_f !== undefined && AtD.callback_f.error !== undefined) {
  72. AtD.callback_f.error(AtD.core.getErrorMessage(xml));
  73. }
  74. return;
  75. }
  76. /* highlight the errors */
  77. AtD.container = container_id;
  78. var count = Number( AtD.processXML(container_id, xml) );
  79. if (AtD.callback_f !== undefined && AtD.callback_f.ready !== undefined) {
  80. AtD.callback_f.ready(count);
  81. }
  82. if (count === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  83. AtD.callback_f.success(count);
  84. }
  85. AtD.counter = count;
  86. AtD.count = count;
  87. });
  88. };
  89. /* check a div for any incorrectly spelled words */
  90. AtD.check = function(container_id, callback_f) {
  91. /* checks if a global var for click stats exists and increments it if it does... */
  92. if (typeof AtD_proofread_click_count !== 'undefined') {
  93. AtD_proofread_click_count++;
  94. }
  95. AtD.callback_f = callback_f; /* remember the callback for later */
  96. AtD.remove(container_id);
  97. var container = jQuery('#' + container_id);
  98. var text = jQuery.trim(container.html());
  99. text = text.replace(/\&lt;/g, '<').replace(/\&gt;/g, '>').replace(/\&amp;/g, '&');
  100. text = encodeURIComponent( text ); /* re-escaping % is not necessary here. don't do it */
  101. jQuery.ajax({
  102. type : 'POST',
  103. url : AtD.rpc + '/checkDocument',
  104. data : 'key=' + AtD.api_key + '&data=' + text,
  105. format : 'raw',
  106. dataType : (jQuery.browser.msie) ? 'text' : 'xml',
  107. error : function(XHR, status, error) {
  108. if (AtD.callback_f !== undefined && AtD.callback_f.error !== undefined) {
  109. AtD.callback_f.error(status + ': ' + error);
  110. }
  111. },
  112. success : function(data) {
  113. /* apparently IE likes to return XML as plain text-- work around from:
  114. http://docs.jquery.com/Specifying_the_Data_Type_for_AJAX_Requests */
  115. var xml;
  116. if (typeof data === 'string') {
  117. xml = new ActiveXObject('Microsoft.XMLDOM');
  118. xml.async = false;
  119. xml.loadXML(data);
  120. }
  121. else {
  122. xml = data;
  123. }
  124. if (AtD.core.hasErrorMessage(xml)) {
  125. if (AtD.callback_f !== undefined && AtD.callback_f.error !== undefined) {
  126. AtD.callback_f.error(AtD.core.getErrorMessage(xml));
  127. }
  128. return;
  129. }
  130. /* on with the task of processing and highlighting errors */
  131. AtD.container = container_id;
  132. var count = Number( AtD.processXML(container_id, xml) );
  133. if (AtD.callback_f !== undefined && AtD.callback_f.ready !== undefined) {
  134. AtD.callback_f.ready(count);
  135. }
  136. if (count === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  137. AtD.callback_f.success(count);
  138. }
  139. AtD.counter = count;
  140. AtD.count = count;
  141. }
  142. });
  143. };
  144. AtD.remove = function(container_id) {
  145. AtD._removeWords(container_id, null);
  146. };
  147. AtD.clickListener = function(event) {
  148. if (AtD.core.isMarkedNode(event.target)) {
  149. AtD.suggest(event.target);
  150. }
  151. };
  152. AtD.processXML = function(container_id, responseXML) {
  153. var results = AtD.core.processXML(responseXML);
  154. if (results.count > 0) {
  155. results.count = AtD.core.markMyWords(jQuery('#' + container_id).contents(), results.errors);
  156. }
  157. jQuery('#' + container_id).unbind('click', AtD.clickListener);
  158. jQuery('#' + container_id).click(AtD.clickListener);
  159. return results.count;
  160. };
  161. AtD.useSuggestion = function(word) {
  162. this.core.applySuggestion(AtD.errorElement, word);
  163. AtD.counter --;
  164. if (AtD.counter === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  165. AtD.callback_f.success(AtD.count);
  166. }
  167. };
  168. AtD.editSelection = function() {
  169. var parent = AtD.errorElement.parent();
  170. if (AtD.callback_f !== undefined && AtD.callback_f.editSelection !== undefined) {
  171. AtD.callback_f.editSelection(AtD.errorElement);
  172. }
  173. if (AtD.errorElement.parent() !== parent) {
  174. AtD.counter --;
  175. if (AtD.counter === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  176. AtD.callback_f.success(AtD.count);
  177. }
  178. }
  179. };
  180. AtD.ignoreSuggestion = function() {
  181. AtD.core.removeParent(AtD.errorElement);
  182. AtD.counter --;
  183. if (AtD.counter === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  184. AtD.callback_f.success(AtD.count);
  185. }
  186. };
  187. AtD.ignoreAll = function(container_id) {
  188. var target = AtD.errorElement.text();
  189. var removed = AtD._removeWords(container_id, target);
  190. AtD.counter -= removed;
  191. if (AtD.counter === 0 && AtD.callback_f !== undefined && AtD.callback_f.success !== undefined) {
  192. AtD.callback_f.success(AtD.count);
  193. }
  194. if (AtD.callback_f !== undefined && AtD.callback_f.ignore !== undefined) {
  195. AtD.callback_f.ignore(target);
  196. AtD.core.setIgnoreStrings(target);
  197. }
  198. };
  199. AtD.explainError = function() {
  200. if (AtD.callback_f !== undefined && AtD.callback_f.explain !== undefined) {
  201. AtD.callback_f.explain(AtD.explainURL);
  202. }
  203. };
  204. AtD.suggest = function(element) {
  205. /* construct the menu if it doesn't already exist */
  206. var suggest;
  207. if (jQuery('#suggestmenu').length === 0) {
  208. suggest = jQuery('<div id="suggestmenu"></div>');
  209. suggest.prependTo('body');
  210. } else {
  211. suggest = jQuery('#suggestmenu');
  212. suggest.hide();
  213. }
  214. /* find the correct suggestions object */
  215. var errorDescription = AtD.core.findSuggestion(element);
  216. /* build up the menu y0 */
  217. AtD.errorElement = jQuery(element);
  218. suggest.empty();
  219. if (errorDescription === undefined) {
  220. suggest.append('<strong>' + AtD.getLang('menu_title_no_suggestions', 'No suggestions') + '</strong>');
  221. } else if (errorDescription['suggestions'].length === 0) {
  222. suggest.append('<strong>' + errorDescription['description'] + '</strong>');
  223. } else {
  224. suggest.append('<strong>' + errorDescription['description'] + '</strong>');
  225. for (var i = 0; i < errorDescription['suggestions'].length; i++) {
  226. (function(sugg) {
  227. suggest.append('<a href="javascript:AtD.useSuggestion(\'' + sugg.replace(/'/, '\\\'') + '\')">' + sugg + '</a>');
  228. })(errorDescription['suggestions'][i]); // jshint ignore:line
  229. }
  230. }
  231. /* do the explain menu if configured */
  232. if (AtD.callback_f !== undefined && AtD.callback_f.explain !== undefined && errorDescription['moreinfo'] !== undefined) {
  233. suggest.append('<a href="javascript:AtD.explainError()" class="spell_sep_top">' + AtD.getLang('menu_option_explain', 'Explain...') + '</a>');
  234. AtD.explainURL = errorDescription['moreinfo'];
  235. }
  236. /* do the ignore option */
  237. suggest.append('<a href="javascript:AtD.ignoreSuggestion()" class="spell_sep_top">' + AtD.getLang('menu_option_ignore_once', 'Ignore suggestion') + '</a>');
  238. /* add the edit in place and ignore always option */
  239. if (AtD.callback_f !== undefined && AtD.callback_f.editSelection !== undefined) {
  240. if (AtD.callback_f !== undefined && AtD.callback_f.ignore !== undefined) {
  241. suggest.append('<a href="javascript:AtD.ignoreAll(\'' + AtD.container + '\')">' + AtD.getLang('menu_option_ignore_always', 'Ignore always') + '</a>');
  242. } else {
  243. suggest.append('<a href="javascript:AtD.ignoreAll(\'' + AtD.container + '\')">' + AtD.getLang('menu_option_ignore_all', 'Ignore all') + '</a>');
  244. }
  245. suggest.append('<a href="javascript:AtD.editSelection(\'' + AtD.container + '\')" class="spell_sep_bottom spell_sep_top">' + AtD.getLang('menu_option_edit_selection', 'Edit Selection...') + '</a>');
  246. }
  247. else {
  248. if (AtD.callback_f !== undefined && AtD.callback_f.ignore !== undefined) {
  249. suggest.append('<a href="javascript:AtD.ignoreAll(\'' + AtD.container + '\')" class="spell_sep_bottom">' + AtD.getLang('menu_option_ignore_always', 'Ignore always') + '</a>');
  250. } else {
  251. suggest.append('<a href="javascript:AtD.ignoreAll(\'' + AtD.container + '\')" class="spell_sep_bottom">' + AtD.getLang('menu_option_ignore_all', 'Ignore all') + '</a>');
  252. }
  253. }
  254. /* show the menu */
  255. var pos = jQuery(element).offset();
  256. var width = jQuery(element).width();
  257. /* a sanity check for Internet Explorer--my favorite browser in every possible way */
  258. if (width > 100) {
  259. width = 50;
  260. }
  261. jQuery(suggest).css({ left: (pos.left + width) + 'px', top: pos.top + 'px' });
  262. jQuery(suggest).fadeIn(200);
  263. /* bind events to make the menu disappear when the user clicks outside of it */
  264. AtD.suggestShow = true;
  265. setTimeout(function() {
  266. jQuery('body').bind('click', function() {
  267. if (!AtD.suggestShow) {
  268. jQuery('#suggestmenu').fadeOut(200);
  269. }
  270. });
  271. }, 1);
  272. setTimeout(function() {
  273. AtD.suggestShow = false;
  274. }, 2);
  275. };
  276. AtD._removeWords = function(container_id, w) {
  277. return this.core.removeWords(jQuery('#' + container_id), w);
  278. };
  279. /*
  280. * Set prototypes used by AtD Core UI
  281. */
  282. AtD.initCoreModule = function() {
  283. var core = new AtDCore();
  284. core.hasClass = function(node, className) {
  285. return jQuery(node).hasClass(className);
  286. };
  287. core.map = jQuery.map;
  288. core.contents = function(node) {
  289. return jQuery(node).contents();
  290. };
  291. core.replaceWith = function(old_node, new_node) {
  292. return jQuery(old_node).replaceWith(new_node);
  293. };
  294. core.findSpans = function(parent) {
  295. return jQuery.makeArray(parent.find('span'));
  296. };
  297. core.create = function(string/*, isTextNode*/) {
  298. // replace out all tags with &-equivalents so that we preserve tag text.
  299. string = string.replace(/\&/g, '&amp;');
  300. string = string.replace(/</g, '&lt;').replace(/\>/g, '&gt;');
  301. // find all instances of AtD-created spans
  302. var matches = string.match(/\&lt;span class="hidden\w+?" pre="[^"]*"\&gt;.*?\&lt;\/span\&gt;/g);
  303. var x;
  304. // ... and fix the tags in those substrings.
  305. if (matches) {
  306. for (x = 0; x < matches.length; x++) {
  307. string = string.replace(matches[x], matches[x].replace(/\&lt;/gi, '<').replace(/\&gt;/gi, '>'));
  308. }
  309. }
  310. if (core.isIE()) {
  311. // and... one more round of corrections for our friends over at the Internet Explorer
  312. matches = string.match(/\&lt;span class="mceItemHidden"\&gt;\&amp;nbsp;\&lt;\/span&gt;/g, string);
  313. //|&lt;BR.*?class.*?atd_remove_me.*?\&gt;/gi, string);
  314. if (matches) {
  315. for (x = 0; x < matches.length; x++) {
  316. string = string.replace(matches[x], matches[x].replace(/\&lt;/gi, '<').replace(/\&gt;/gi, '>').replace(/\&amp;/gi, '&'));
  317. }
  318. }
  319. }
  320. var node = jQuery('<span class="mceItemHidden"></span>');
  321. node.html(string);
  322. return node;
  323. };
  324. core.remove = function(node) {
  325. return jQuery(node).remove();
  326. };
  327. core.removeParent = function(node) {
  328. /* unwrap exists in jQuery 1.4+ only. Thankfully because replaceWith as-used here won't work in 1.4 */
  329. if (jQuery(node).unwrap) {
  330. return jQuery(node).contents().unwrap();
  331. } else {
  332. return jQuery(node).replaceWith(jQuery(node).html());
  333. }
  334. };
  335. core.getAttrib = function(node, name) {
  336. return jQuery(node).attr(name);
  337. };
  338. return core;
  339. };
  340. AtD.core = AtD.initCoreModule();