PageRenderTime 57ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

/thirdparty/tinymce/plugins/spellchecker/plugin.js

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