plugin.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935
  1. /**
  2. * Compiled inline version. (Library mode)
  3. */
  4. /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */
  5. /*globals $code */
  6. (function(exports, undefined) {
  7. "use strict";
  8. var modules = {};
  9. function require(ids, callback) {
  10. var module, defs = [];
  11. for (var i = 0; i < ids.length; ++i) {
  12. module = modules[ids[i]] || resolve(ids[i]);
  13. if (!module) {
  14. throw 'module definition dependecy not found: ' + ids[i];
  15. }
  16. defs.push(module);
  17. }
  18. callback.apply(null, defs);
  19. }
  20. function define(id, dependencies, definition) {
  21. if (typeof id !== 'string') {
  22. throw 'invalid module definition, module id must be defined and be a string';
  23. }
  24. if (dependencies === undefined) {
  25. throw 'invalid module definition, dependencies must be specified';
  26. }
  27. if (definition === undefined) {
  28. throw 'invalid module definition, definition function must be specified';
  29. }
  30. require(dependencies, function() {
  31. modules[id] = definition.apply(null, arguments);
  32. });
  33. }
  34. function defined(id) {
  35. return !!modules[id];
  36. }
  37. function resolve(id) {
  38. var target = exports;
  39. var fragments = id.split(/[.\/]/);
  40. for (var fi = 0; fi < fragments.length; ++fi) {
  41. if (!target[fragments[fi]]) {
  42. return;
  43. }
  44. target = target[fragments[fi]];
  45. }
  46. return target;
  47. }
  48. function expose(ids) {
  49. for (var i = 0; i < ids.length; i++) {
  50. var target = exports;
  51. var id = ids[i];
  52. var fragments = id.split(/[.\/]/);
  53. for (var fi = 0; fi < fragments.length - 1; ++fi) {
  54. if (target[fragments[fi]] === undefined) {
  55. target[fragments[fi]] = {};
  56. }
  57. target = target[fragments[fi]];
  58. }
  59. target[fragments[fragments.length - 1]] = modules[id];
  60. }
  61. }
  62. // Included from: js/tinymce/plugins/spellchecker/classes/DomTextMatcher.js
  63. /**
  64. * DomTextMatcher.js
  65. *
  66. * Copyright, Moxiecode Systems AB
  67. * Released under LGPL License.
  68. *
  69. * License: http://www.tinymce.com/license
  70. * Contributing: http://www.tinymce.com/contributing
  71. */
  72. /*eslint no-labels:0, no-constant-condition: 0 */
  73. /**
  74. * This class logic for filtering text and matching words.
  75. *
  76. * @class tinymce.spellcheckerplugin.TextFilter
  77. * @private
  78. */
  79. define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() {
  80. // Based on work developed by: James Padolsey http://james.padolsey.com
  81. // released under UNLICENSE that is compatible with LGPL
  82. // TODO: Handle contentEditable edgecase:
  83. // <p>text<span contentEditable="false">text<span contentEditable="true">text</span>text</span>text</p>
  84. return function(node, editor) {
  85. var m, matches = [], text, dom = editor.dom;
  86. var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap;
  87. blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc
  88. hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT
  89. shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT
  90. function createMatch(m, data) {
  91. if (!m[0]) {
  92. throw 'findAndReplaceDOMText cannot handle zero-length matches';
  93. }
  94. return {
  95. start: m.index,
  96. end: m.index + m[0].length,
  97. text: m[0],
  98. data: data
  99. };
  100. }
  101. function getText(node) {
  102. var txt;
  103. if (node.nodeType === 3) {
  104. return node.data;
  105. }
  106. if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) {
  107. return '';
  108. }
  109. txt = '';
  110. if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) {
  111. txt += '\n';
  112. }
  113. if ((node = node.firstChild)) {
  114. do {
  115. txt += getText(node);
  116. } while ((node = node.nextSibling));
  117. }
  118. return txt;
  119. }
  120. function stepThroughMatches(node, matches, replaceFn) {
  121. var startNode, endNode, startNodeIndex,
  122. endNodeIndex, innerNodes = [], atIndex = 0, curNode = node,
  123. matchLocation, matchIndex = 0;
  124. matches = matches.slice(0);
  125. matches.sort(function(a, b) {
  126. return a.start - b.start;
  127. });
  128. matchLocation = matches.shift();
  129. out: while (true) {
  130. if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName]) {
  131. atIndex++;
  132. }
  133. if (curNode.nodeType === 3) {
  134. if (!endNode && curNode.length + atIndex >= matchLocation.end) {
  135. // We've found the ending
  136. endNode = curNode;
  137. endNodeIndex = matchLocation.end - atIndex;
  138. } else if (startNode) {
  139. // Intersecting node
  140. innerNodes.push(curNode);
  141. }
  142. if (!startNode && curNode.length + atIndex > matchLocation.start) {
  143. // We've found the match start
  144. startNode = curNode;
  145. startNodeIndex = matchLocation.start - atIndex;
  146. }
  147. atIndex += curNode.length;
  148. }
  149. if (startNode && endNode) {
  150. curNode = replaceFn({
  151. startNode: startNode,
  152. startNodeIndex: startNodeIndex,
  153. endNode: endNode,
  154. endNodeIndex: endNodeIndex,
  155. innerNodes: innerNodes,
  156. match: matchLocation.text,
  157. matchIndex: matchIndex
  158. });
  159. // replaceFn has to return the node that replaced the endNode
  160. // and then we step back so we can continue from the end of the
  161. // match:
  162. atIndex -= (endNode.length - endNodeIndex);
  163. startNode = null;
  164. endNode = null;
  165. innerNodes = [];
  166. matchLocation = matches.shift();
  167. matchIndex++;
  168. if (!matchLocation) {
  169. break; // no more matches
  170. }
  171. } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) {
  172. // Move down
  173. curNode = curNode.firstChild;
  174. continue;
  175. } else if (curNode.nextSibling) {
  176. // Move forward:
  177. curNode = curNode.nextSibling;
  178. continue;
  179. }
  180. // Move forward or up:
  181. while (true) {
  182. if (curNode.nextSibling) {
  183. curNode = curNode.nextSibling;
  184. break;
  185. } else if (curNode.parentNode !== node) {
  186. curNode = curNode.parentNode;
  187. } else {
  188. break out;
  189. }
  190. }
  191. }
  192. }
  193. /**
  194. * Generates the actual replaceFn which splits up text nodes
  195. * and inserts the replacement element.
  196. */
  197. function genReplacer(callback) {
  198. function makeReplacementNode(fill, matchIndex) {
  199. var match = matches[matchIndex];
  200. if (!match.stencil) {
  201. match.stencil = callback(match);
  202. }
  203. var clone = match.stencil.cloneNode(false);
  204. clone.setAttribute('data-mce-index', matchIndex);
  205. if (fill) {
  206. clone.appendChild(dom.doc.createTextNode(fill));
  207. }
  208. return clone;
  209. }
  210. return function(range) {
  211. var before, after, parentNode, startNode = range.startNode,
  212. endNode = range.endNode, matchIndex = range.matchIndex,
  213. doc = dom.doc;
  214. if (startNode === endNode) {
  215. var node = startNode;
  216. parentNode = node.parentNode;
  217. if (range.startNodeIndex > 0) {
  218. // Add "before" text node (before the match)
  219. before = doc.createTextNode(node.data.substring(0, range.startNodeIndex));
  220. parentNode.insertBefore(before, node);
  221. }
  222. // Create the replacement node:
  223. var el = makeReplacementNode(range.match, matchIndex);
  224. parentNode.insertBefore(el, node);
  225. if (range.endNodeIndex < node.length) {
  226. // Add "after" text node (after the match)
  227. after = doc.createTextNode(node.data.substring(range.endNodeIndex));
  228. parentNode.insertBefore(after, node);
  229. }
  230. node.parentNode.removeChild(node);
  231. return el;
  232. } else {
  233. // Replace startNode -> [innerNodes...] -> endNode (in that order)
  234. before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex));
  235. after = doc.createTextNode(endNode.data.substring(range.endNodeIndex));
  236. var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex);
  237. var innerEls = [];
  238. for (var i = 0, l = range.innerNodes.length; i < l; ++i) {
  239. var innerNode = range.innerNodes[i];
  240. var innerEl = makeReplacementNode(innerNode.data, matchIndex);
  241. innerNode.parentNode.replaceChild(innerEl, innerNode);
  242. innerEls.push(innerEl);
  243. }
  244. var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex);
  245. parentNode = startNode.parentNode;
  246. parentNode.insertBefore(before, startNode);
  247. parentNode.insertBefore(elA, startNode);
  248. parentNode.removeChild(startNode);
  249. parentNode = endNode.parentNode;
  250. parentNode.insertBefore(elB, endNode);
  251. parentNode.insertBefore(after, endNode);
  252. parentNode.removeChild(endNode);
  253. return elB;
  254. }
  255. };
  256. }
  257. function unwrapElement(element) {
  258. var parentNode = element.parentNode;
  259. parentNode.insertBefore(element.firstChild, element);
  260. element.parentNode.removeChild(element);
  261. }
  262. function getWrappersByIndex(index) {
  263. var elements = node.getElementsByTagName('*'), wrappers = [];
  264. index = typeof(index) == "number" ? "" + index : null;
  265. for (var i = 0; i < elements.length; i++) {
  266. var element = elements[i], dataIndex = element.getAttribute('data-mce-index');
  267. if (dataIndex !== null && dataIndex.length) {
  268. if (dataIndex === index || index === null) {
  269. wrappers.push(element);
  270. }
  271. }
  272. }
  273. return wrappers;
  274. }
  275. /**
  276. * Returns the index of a specific match object or -1 if it isn't found.
  277. *
  278. * @param {Match} match Text match object.
  279. * @return {Number} Index of match or -1 if it isn't found.
  280. */
  281. function indexOf(match) {
  282. var i = matches.length;
  283. while (i--) {
  284. if (matches[i] === match) {
  285. return i;
  286. }
  287. }
  288. return -1;
  289. }
  290. /**
  291. * Filters the matches. If the callback returns true it stays if not it gets removed.
  292. *
  293. * @param {Function} callback Callback to execute for each match.
  294. * @return {DomTextMatcher} Current DomTextMatcher instance.
  295. */
  296. function filter(callback) {
  297. var filteredMatches = [];
  298. each(function(match, i) {
  299. if (callback(match, i)) {
  300. filteredMatches.push(match);
  301. }
  302. });
  303. matches = filteredMatches;
  304. /*jshint validthis:true*/
  305. return this;
  306. }
  307. /**
  308. * Executes the specified callback for each match.
  309. *
  310. * @param {Function} callback Callback to execute for each match.
  311. * @return {DomTextMatcher} Current DomTextMatcher instance.
  312. */
  313. function each(callback) {
  314. for (var i = 0, l = matches.length; i < l; i++) {
  315. if (callback(matches[i], i) === false) {
  316. break;
  317. }
  318. }
  319. /*jshint validthis:true*/
  320. return this;
  321. }
  322. /**
  323. * Wraps the current matches with nodes created by the specified callback.
  324. * Multiple clones of these matches might occur on matches that are on multiple nodex.
  325. *
  326. * @param {Function} callback Callback to execute in order to create elements for matches.
  327. * @return {DomTextMatcher} Current DomTextMatcher instance.
  328. */
  329. function wrap(callback) {
  330. if (matches.length) {
  331. stepThroughMatches(node, matches, genReplacer(callback));
  332. }
  333. /*jshint validthis:true*/
  334. return this;
  335. }
  336. /**
  337. * Finds the specified regexp and adds them to the matches collection.
  338. *
  339. * @param {RegExp} regex Global regexp to search the current node by.
  340. * @param {Object} [data] Optional custom data element for the match.
  341. * @return {DomTextMatcher} Current DomTextMatcher instance.
  342. */
  343. function find(regex, data) {
  344. if (text && regex.global) {
  345. while ((m = regex.exec(text))) {
  346. matches.push(createMatch(m, data));
  347. }
  348. }
  349. return this;
  350. }
  351. /**
  352. * Unwraps the specified match object or all matches if unspecified.
  353. *
  354. * @param {Object} [match] Optional match object.
  355. * @return {DomTextMatcher} Current DomTextMatcher instance.
  356. */
  357. function unwrap(match) {
  358. var i, elements = getWrappersByIndex(match ? indexOf(match) : null);
  359. i = elements.length;
  360. while (i--) {
  361. unwrapElement(elements[i]);
  362. }
  363. return this;
  364. }
  365. /**
  366. * Returns a match object by the specified DOM element.
  367. *
  368. * @param {DOMElement} element Element to return match object for.
  369. * @return {Object} Match object for the specified element.
  370. */
  371. function matchFromElement(element) {
  372. return matches[element.getAttribute('data-mce-index')];
  373. }
  374. /**
  375. * Returns a DOM element from the specified match element. This will be the first element if it's split
  376. * on multiple nodes.
  377. *
  378. * @param {Object} match Match element to get first element of.
  379. * @return {DOMElement} DOM element for the specified match object.
  380. */
  381. function elementFromMatch(match) {
  382. return getWrappersByIndex(indexOf(match))[0];
  383. }
  384. /**
  385. * Adds match the specified range for example a grammar line.
  386. *
  387. * @param {Number} start Start offset.
  388. * @param {Number} length Length of the text.
  389. * @param {Object} data Custom data object for match.
  390. * @return {DomTextMatcher} Current DomTextMatcher instance.
  391. */
  392. function add(start, length, data) {
  393. matches.push({
  394. start: start,
  395. end: start + length,
  396. text: text.substr(start, length),
  397. data: data
  398. });
  399. return this;
  400. }
  401. /**
  402. * Returns a DOM range for the specified match.
  403. *
  404. * @param {Object} match Match object to get range for.
  405. * @return {DOMRange} DOM Range for the specified match.
  406. */
  407. function rangeFromMatch(match) {
  408. var wrappers = getWrappersByIndex(indexOf(match));
  409. var rng = editor.dom.createRng();
  410. rng.setStartBefore(wrappers[0]);
  411. rng.setEndAfter(wrappers[wrappers.length - 1]);
  412. return rng;
  413. }
  414. /**
  415. * Replaces the specified match with the specified text.
  416. *
  417. * @param {Object} match Match object to replace.
  418. * @param {String} text Text to replace the match with.
  419. * @return {DOMRange} DOM range produced after the replace.
  420. */
  421. function replace(match, text) {
  422. var rng = rangeFromMatch(match);
  423. rng.deleteContents();
  424. if (text.length > 0) {
  425. rng.insertNode(editor.dom.doc.createTextNode(text));
  426. }
  427. return rng;
  428. }
  429. /**
  430. * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches.
  431. *
  432. * @return {[type]} [description]
  433. */
  434. function reset() {
  435. matches.splice(0, matches.length);
  436. unwrap();
  437. return this;
  438. }
  439. text = getText(node);
  440. return {
  441. text: text,
  442. matches: matches,
  443. each: each,
  444. filter: filter,
  445. reset: reset,
  446. matchFromElement: matchFromElement,
  447. elementFromMatch: elementFromMatch,
  448. find: find,
  449. add: add,
  450. wrap: wrap,
  451. unwrap: unwrap,
  452. replace: replace,
  453. rangeFromMatch: rangeFromMatch,
  454. indexOf: indexOf
  455. };
  456. };
  457. });
  458. // Included from: js/tinymce/plugins/spellchecker/classes/Plugin.js
  459. /**
  460. * Plugin.js
  461. *
  462. * Copyright, Moxiecode Systems AB
  463. * Released under LGPL License.
  464. *
  465. * License: http://www.tinymce.com/license
  466. * Contributing: http://www.tinymce.com/contributing
  467. */
  468. /*jshint camelcase:false */
  469. /**
  470. * This class contains all core logic for the spellchecker plugin.
  471. *
  472. * @class tinymce.spellcheckerplugin.Plugin
  473. * @private
  474. */
  475. define("tinymce/spellcheckerplugin/Plugin", [
  476. "tinymce/spellcheckerplugin/DomTextMatcher",
  477. "tinymce/PluginManager",
  478. "tinymce/util/Tools",
  479. "tinymce/ui/Menu",
  480. "tinymce/dom/DOMUtils",
  481. "tinymce/util/XHR",
  482. "tinymce/util/URI",
  483. "tinymce/util/JSON"
  484. ], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, XHR, URI, JSON) {
  485. PluginManager.add('spellchecker', function(editor, url) {
  486. var languageMenuItems, self = this, lastSuggestions, started, suggestionsMenu, settings = editor.settings;
  487. function getTextMatcher() {
  488. if (!self.textMatcher) {
  489. self.textMatcher = new DomTextMatcher(editor.getBody(), editor);
  490. }
  491. return self.textMatcher;
  492. }
  493. function buildMenuItems(listName, languageValues) {
  494. var items = [];
  495. Tools.each(languageValues, function(languageValue) {
  496. items.push({
  497. selectable: true,
  498. text: languageValue.name,
  499. data: languageValue.value
  500. });
  501. });
  502. return items;
  503. }
  504. var languagesString = settings.spellchecker_languages ||
  505. 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,' +
  506. 'German=de,Italian=it,Polish=pl,Portuguese=pt_BR,' +
  507. 'Spanish=es,Swedish=sv';
  508. languageMenuItems = buildMenuItems('Language',
  509. Tools.map(languagesString.split(','),
  510. function(lang_pair) {
  511. var lang = lang_pair.split('=');
  512. return {
  513. name: lang[0],
  514. value: lang[1]
  515. };
  516. }
  517. )
  518. );
  519. function isEmpty(obj) {
  520. /*jshint unused:false*/
  521. /*eslint no-unused-vars:0 */
  522. for (var name in obj) {
  523. return false;
  524. }
  525. return true;
  526. }
  527. function showSuggestions(word, spans) {
  528. var items = [], suggestions = lastSuggestions[word];
  529. Tools.each(suggestions, function(suggestion) {
  530. items.push({
  531. text: suggestion,
  532. onclick: function() {
  533. editor.insertContent(editor.dom.encode(suggestion));
  534. editor.dom.remove(spans);
  535. checkIfFinished();
  536. }
  537. });
  538. });
  539. items.push.apply(items, [
  540. {text: '-'},
  541. {text: 'Ignore', onclick: function() {
  542. ignoreWord(word, spans);
  543. }},
  544. {text: 'Ignore all', onclick: function() {
  545. ignoreWord(word, spans, true);
  546. }},
  547. {text: 'Finish', onclick: finish}
  548. ]);
  549. // Render menu
  550. suggestionsMenu = new Menu({
  551. items: items,
  552. context: 'contextmenu',
  553. onautohide: function(e) {
  554. if (e.target.className.indexOf('spellchecker') != -1) {
  555. e.preventDefault();
  556. }
  557. },
  558. onhide: function() {
  559. suggestionsMenu.remove();
  560. suggestionsMenu = null;
  561. }
  562. });
  563. suggestionsMenu.renderTo(document.body);
  564. // Position menu
  565. var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer());
  566. var targetPos = editor.dom.getPos(spans[0]);
  567. var root = editor.dom.getRoot();
  568. // Adjust targetPos for scrolling in the editor
  569. if (root.nodeName == 'BODY') {
  570. targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft;
  571. targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop;
  572. } else {
  573. targetPos.x -= root.scrollLeft;
  574. targetPos.y -= root.scrollTop;
  575. }
  576. pos.x += targetPos.x;
  577. pos.y += targetPos.y;
  578. suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight);
  579. }
  580. function getWordCharPattern() {
  581. // Regexp for finding word specific characters this will split words by
  582. // spaces, quotes, copy right characters etc. It's escaped with unicode characters
  583. // to make it easier to output scripts on servers using different encodings
  584. // so if you add any characters outside the 128 byte range make sure to escape it
  585. return editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" +
  586. "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" +
  587. "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" +
  588. "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e" +
  589. "]+", "g");
  590. }
  591. function spellcheck() {
  592. if (started) {
  593. finish();
  594. return;
  595. } else {
  596. finish();
  597. }
  598. started = true;
  599. function doneCallback(suggestions) {
  600. editor.setProgressState(false);
  601. if (isEmpty(suggestions)) {
  602. editor.windowManager.alert('No misspellings found');
  603. started = false;
  604. return;
  605. }
  606. lastSuggestions = suggestions;
  607. getTextMatcher().find(getWordCharPattern()).filter(function(match) {
  608. return !!suggestions[match.text];
  609. }).wrap(function(match) {
  610. return editor.dom.create('span', {
  611. "class": 'mce-spellchecker-word',
  612. "data-mce-bogus": 1,
  613. "data-mce-word": match.text
  614. });
  615. });
  616. editor.fire('SpellcheckStart');
  617. }
  618. function errorCallback(message) {
  619. editor.windowManager.alert(message);
  620. editor.setProgressState(false);
  621. finish();
  622. }
  623. function defaultSpellcheckCallback(method, text, doneCallback) {
  624. XHR.send({
  625. url: new URI(url).toAbsolute(settings.spellchecker_rpc_url),
  626. type: "post",
  627. content_type: 'application/x-www-form-urlencoded',
  628. data: "text=" + encodeURIComponent(text) + "&lang=" + settings.spellchecker_language,
  629. success: function(result) {
  630. result = JSON.parse(result);
  631. if (!result) {
  632. errorCallback("Sever response wasn't proper JSON.");
  633. } else if (result.error) {
  634. errorCallback(result.error);
  635. } else {
  636. doneCallback(result.words);
  637. }
  638. },
  639. error: function(type, xhr) {
  640. errorCallback("Spellchecker request error: " + xhr.status);
  641. }
  642. });
  643. }
  644. editor.setProgressState(true);
  645. var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback;
  646. spellCheckCallback.call(self, "spellcheck", getTextMatcher().text, doneCallback, errorCallback);
  647. editor.focus();
  648. }
  649. function checkIfFinished() {
  650. if (!editor.dom.select('span.mce-spellchecker-word').length) {
  651. finish();
  652. }
  653. }
  654. function ignoreWord(word, spans, all) {
  655. editor.selection.collapse();
  656. if (all) {
  657. Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(span) {
  658. if (span.getAttribute('data-mce-word') == word) {
  659. editor.dom.remove(span, true);
  660. }
  661. });
  662. } else {
  663. editor.dom.remove(spans, true);
  664. }
  665. checkIfFinished();
  666. }
  667. function finish() {
  668. getTextMatcher().reset();
  669. self.textMatcher = null;
  670. if (started) {
  671. started = false;
  672. editor.fire('SpellcheckEnd');
  673. }
  674. }
  675. function getElmIndex(elm) {
  676. var value = elm.getAttribute('data-mce-index');
  677. if (typeof(value) == "number") {
  678. return "" + value;
  679. }
  680. return value;
  681. }
  682. function findSpansByIndex(index) {
  683. var nodes, spans = [];
  684. nodes = Tools.toArray(editor.getBody().getElementsByTagName('span'));
  685. if (nodes.length) {
  686. for (var i = 0; i < nodes.length; i++) {
  687. var nodeIndex = getElmIndex(nodes[i]);
  688. if (nodeIndex === null || !nodeIndex.length) {
  689. continue;
  690. }
  691. if (nodeIndex === index.toString()) {
  692. spans.push(nodes[i]);
  693. }
  694. }
  695. }
  696. return spans;
  697. }
  698. editor.on('click', function(e) {
  699. var target = e.target;
  700. if (target.className == "mce-spellchecker-word") {
  701. e.preventDefault();
  702. var spans = findSpansByIndex(getElmIndex(target));
  703. if (spans.length > 0) {
  704. var rng = editor.dom.createRng();
  705. rng.setStartBefore(spans[0]);
  706. rng.setEndAfter(spans[spans.length - 1]);
  707. editor.selection.setRng(rng);
  708. showSuggestions(target.getAttribute('data-mce-word'), spans);
  709. }
  710. }
  711. });
  712. editor.addMenuItem('spellchecker', {
  713. text: 'Spellcheck',
  714. context: 'tools',
  715. onclick: spellcheck,
  716. selectable: true,
  717. onPostRender: function() {
  718. var self = this;
  719. editor.on('SpellcheckStart SpellcheckEnd', function() {
  720. self.active(started);
  721. });
  722. }
  723. });
  724. function updateSelection(e) {
  725. var selectedLanguage = settings.spellchecker_language;
  726. e.control.items().each(function(ctrl) {
  727. ctrl.active(ctrl.settings.data === selectedLanguage);
  728. });
  729. }
  730. var buttonArgs = {
  731. tooltip: 'Spellcheck',
  732. onclick: spellcheck,
  733. onPostRender: function() {
  734. var self = this;
  735. editor.on('SpellcheckStart SpellcheckEnd', function() {
  736. self.active(started);
  737. });
  738. }
  739. };
  740. if (languageMenuItems.length > 1) {
  741. buttonArgs.type = 'splitbutton';
  742. buttonArgs.menu = languageMenuItems;
  743. buttonArgs.onshow = updateSelection;
  744. buttonArgs.onselect = function(e) {
  745. settings.spellchecker_language = e.control.settings.data;
  746. };
  747. }
  748. editor.addButton('spellchecker', buttonArgs);
  749. editor.addCommand('mceSpellCheck', spellcheck);
  750. editor.on('remove', function() {
  751. if (suggestionsMenu) {
  752. suggestionsMenu.remove();
  753. suggestionsMenu = null;
  754. }
  755. });
  756. editor.on('change', checkIfFinished);
  757. this.getTextMatcher = getTextMatcher;
  758. this.getWordCharPattern = getWordCharPattern;
  759. this.getLanguage = function() {
  760. return settings.spellchecker_language;
  761. };
  762. // Set default spellchecker language if it's not specified
  763. settings.spellchecker_language = settings.spellchecker_language || settings.language || 'en';
  764. });
  765. });
  766. expose(["tinymce/spellcheckerplugin/DomTextMatcher","tinymce/spellcheckerplugin/Plugin"]);
  767. })(this);