/** * Compiled inline version. (Library mode) */ /*jshint smarttabs:true, undef:true, latedef:true, curly:true, bitwise:true, camelcase:true */ /*globals $code */ (function(exports, undefined) { "use strict"; var modules = {}; function require(ids, callback) { var module, defs = []; for (var i = 0; i < ids.length; ++i) { module = modules[ids[i]] || resolve(ids[i]); if (!module) { throw 'module definition dependecy not found: ' + ids[i]; } defs.push(module); } callback.apply(null, defs); } function define(id, dependencies, definition) { if (typeof id !== 'string') { throw 'invalid module definition, module id must be defined and be a string'; } if (dependencies === undefined) { throw 'invalid module definition, dependencies must be specified'; } if (definition === undefined) { throw 'invalid module definition, definition function must be specified'; } require(dependencies, function() { modules[id] = definition.apply(null, arguments); }); } function defined(id) { return !!modules[id]; } function resolve(id) { var target = exports; var fragments = id.split(/[.\/]/); for (var fi = 0; fi < fragments.length; ++fi) { if (!target[fragments[fi]]) { return; } target = target[fragments[fi]]; } return target; } function expose(ids) { var i, target, id, fragments, privateModules; for (i = 0; i < ids.length; i++) { target = exports; id = ids[i]; fragments = id.split(/[.\/]/); for (var fi = 0; fi < fragments.length - 1; ++fi) { if (target[fragments[fi]] === undefined) { target[fragments[fi]] = {}; } target = target[fragments[fi]]; } target[fragments[fragments.length - 1]] = modules[id]; } // Expose private modules for unit tests if (exports.AMDLC_TESTS) { privateModules = exports.privateModules || {}; for (id in modules) { privateModules[id] = modules[id]; } for (i = 0; i < ids.length; i++) { delete privateModules[ids[i]]; } exports.privateModules = privateModules; } } // Included from: js/tinymce/plugins/spellchecker/classes/DomTextMatcher.js /** * DomTextMatcher.js * * Released under LGPL License. * Copyright (c) 1999-2015 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /*eslint no-labels:0, no-constant-condition: 0 */ /** * This class logic for filtering text and matching words. * * @class tinymce.spellcheckerplugin.TextFilter * @private */ define("tinymce/spellcheckerplugin/DomTextMatcher", [], function() { function isContentEditableFalse(node) { return node && node.nodeType == 1 && node.contentEditable === "false"; } // Based on work developed by: James Padolsey http://james.padolsey.com // released under UNLICENSE that is compatible with LGPL // TODO: Handle contentEditable edgecase: //

texttexttexttexttext

return function(node, editor) { var m, matches = [], text, dom = editor.dom; var blockElementsMap, hiddenTextElementsMap, shortEndedElementsMap; blockElementsMap = editor.schema.getBlockElements(); // H1-H6, P, TD etc hiddenTextElementsMap = editor.schema.getWhiteSpaceElements(); // TEXTAREA, PRE, STYLE, SCRIPT shortEndedElementsMap = editor.schema.getShortEndedElements(); // BR, IMG, INPUT function createMatch(m, data) { if (!m[0]) { throw 'findAndReplaceDOMText cannot handle zero-length matches'; } return { start: m.index, end: m.index + m[0].length, text: m[0], data: data }; } function getText(node) { var txt; if (node.nodeType === 3) { return node.data; } if (hiddenTextElementsMap[node.nodeName] && !blockElementsMap[node.nodeName]) { return ''; } if (isContentEditableFalse(node)) { return '\n'; } txt = ''; if (blockElementsMap[node.nodeName] || shortEndedElementsMap[node.nodeName]) { txt += '\n'; } if ((node = node.firstChild)) { do { txt += getText(node); } while ((node = node.nextSibling)); } return txt; } function stepThroughMatches(node, matches, replaceFn) { var startNode, endNode, startNodeIndex, endNodeIndex, innerNodes = [], atIndex = 0, curNode = node, matchLocation, matchIndex = 0; matches = matches.slice(0); matches.sort(function(a, b) { return a.start - b.start; }); matchLocation = matches.shift(); out: while (true) { if (blockElementsMap[curNode.nodeName] || shortEndedElementsMap[curNode.nodeName] || isContentEditableFalse(curNode)) { atIndex++; } if (curNode.nodeType === 3) { if (!endNode && curNode.length + atIndex >= matchLocation.end) { // We've found the ending endNode = curNode; endNodeIndex = matchLocation.end - atIndex; } else if (startNode) { // Intersecting node innerNodes.push(curNode); } if (!startNode && curNode.length + atIndex > matchLocation.start) { // We've found the match start startNode = curNode; startNodeIndex = matchLocation.start - atIndex; } atIndex += curNode.length; } if (startNode && endNode) { curNode = replaceFn({ startNode: startNode, startNodeIndex: startNodeIndex, endNode: endNode, endNodeIndex: endNodeIndex, innerNodes: innerNodes, match: matchLocation.text, matchIndex: matchIndex }); // replaceFn has to return the node that replaced the endNode // and then we step back so we can continue from the end of the // match: atIndex -= (endNode.length - endNodeIndex); startNode = null; endNode = null; innerNodes = []; matchLocation = matches.shift(); matchIndex++; if (!matchLocation) { break; // no more matches } } else if ((!hiddenTextElementsMap[curNode.nodeName] || blockElementsMap[curNode.nodeName]) && curNode.firstChild) { if (!isContentEditableFalse(curNode)) { // Move down curNode = curNode.firstChild; continue; } } else if (curNode.nextSibling) { // Move forward: curNode = curNode.nextSibling; continue; } // Move forward or up: while (true) { if (curNode.nextSibling) { curNode = curNode.nextSibling; break; } else if (curNode.parentNode !== node) { curNode = curNode.parentNode; } else { break out; } } } } /** * Generates the actual replaceFn which splits up text nodes * and inserts the replacement element. */ function genReplacer(callback) { function makeReplacementNode(fill, matchIndex) { var match = matches[matchIndex]; if (!match.stencil) { match.stencil = callback(match); } var clone = match.stencil.cloneNode(false); clone.setAttribute('data-mce-index', matchIndex); if (fill) { clone.appendChild(dom.doc.createTextNode(fill)); } return clone; } return function(range) { var before, after, parentNode, startNode = range.startNode, endNode = range.endNode, matchIndex = range.matchIndex, doc = dom.doc; if (startNode === endNode) { var node = startNode; parentNode = node.parentNode; if (range.startNodeIndex > 0) { // Add "before" text node (before the match) before = doc.createTextNode(node.data.substring(0, range.startNodeIndex)); parentNode.insertBefore(before, node); } // Create the replacement node: var el = makeReplacementNode(range.match, matchIndex); parentNode.insertBefore(el, node); if (range.endNodeIndex < node.length) { // Add "after" text node (after the match) after = doc.createTextNode(node.data.substring(range.endNodeIndex)); parentNode.insertBefore(after, node); } node.parentNode.removeChild(node); return el; } // Replace startNode -> [innerNodes...] -> endNode (in that order) before = doc.createTextNode(startNode.data.substring(0, range.startNodeIndex)); after = doc.createTextNode(endNode.data.substring(range.endNodeIndex)); var elA = makeReplacementNode(startNode.data.substring(range.startNodeIndex), matchIndex); var innerEls = []; for (var i = 0, l = range.innerNodes.length; i < l; ++i) { var innerNode = range.innerNodes[i]; var innerEl = makeReplacementNode(innerNode.data, matchIndex); innerNode.parentNode.replaceChild(innerEl, innerNode); innerEls.push(innerEl); } var elB = makeReplacementNode(endNode.data.substring(0, range.endNodeIndex), matchIndex); parentNode = startNode.parentNode; parentNode.insertBefore(before, startNode); parentNode.insertBefore(elA, startNode); parentNode.removeChild(startNode); parentNode = endNode.parentNode; parentNode.insertBefore(elB, endNode); parentNode.insertBefore(after, endNode); parentNode.removeChild(endNode); return elB; }; } function unwrapElement(element) { var parentNode = element.parentNode; parentNode.insertBefore(element.firstChild, element); element.parentNode.removeChild(element); } function getWrappersByIndex(index) { var elements = node.getElementsByTagName('*'), wrappers = []; index = typeof index == "number" ? "" + index : null; for (var i = 0; i < elements.length; i++) { var element = elements[i], dataIndex = element.getAttribute('data-mce-index'); if (dataIndex !== null && dataIndex.length) { if (dataIndex === index || index === null) { wrappers.push(element); } } } return wrappers; } /** * Returns the index of a specific match object or -1 if it isn't found. * * @param {Match} match Text match object. * @return {Number} Index of match or -1 if it isn't found. */ function indexOf(match) { var i = matches.length; while (i--) { if (matches[i] === match) { return i; } } return -1; } /** * Filters the matches. If the callback returns true it stays if not it gets removed. * * @param {Function} callback Callback to execute for each match. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function filter(callback) { var filteredMatches = []; each(function(match, i) { if (callback(match, i)) { filteredMatches.push(match); } }); matches = filteredMatches; /*jshint validthis:true*/ return this; } /** * Executes the specified callback for each match. * * @param {Function} callback Callback to execute for each match. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function each(callback) { for (var i = 0, l = matches.length; i < l; i++) { if (callback(matches[i], i) === false) { break; } } /*jshint validthis:true*/ return this; } /** * Wraps the current matches with nodes created by the specified callback. * Multiple clones of these matches might occur on matches that are on multiple nodex. * * @param {Function} callback Callback to execute in order to create elements for matches. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function wrap(callback) { if (matches.length) { stepThroughMatches(node, matches, genReplacer(callback)); } /*jshint validthis:true*/ return this; } /** * Finds the specified regexp and adds them to the matches collection. * * @param {RegExp} regex Global regexp to search the current node by. * @param {Object} [data] Optional custom data element for the match. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function find(regex, data) { if (text && regex.global) { while ((m = regex.exec(text))) { matches.push(createMatch(m, data)); } } return this; } /** * Unwraps the specified match object or all matches if unspecified. * * @param {Object} [match] Optional match object. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function unwrap(match) { var i, elements = getWrappersByIndex(match ? indexOf(match) : null); i = elements.length; while (i--) { unwrapElement(elements[i]); } return this; } /** * Returns a match object by the specified DOM element. * * @param {DOMElement} element Element to return match object for. * @return {Object} Match object for the specified element. */ function matchFromElement(element) { return matches[element.getAttribute('data-mce-index')]; } /** * Returns a DOM element from the specified match element. This will be the first element if it's split * on multiple nodes. * * @param {Object} match Match element to get first element of. * @return {DOMElement} DOM element for the specified match object. */ function elementFromMatch(match) { return getWrappersByIndex(indexOf(match))[0]; } /** * Adds match the specified range for example a grammar line. * * @param {Number} start Start offset. * @param {Number} length Length of the text. * @param {Object} data Custom data object for match. * @return {DomTextMatcher} Current DomTextMatcher instance. */ function add(start, length, data) { matches.push({ start: start, end: start + length, text: text.substr(start, length), data: data }); return this; } /** * Returns a DOM range for the specified match. * * @param {Object} match Match object to get range for. * @return {DOMRange} DOM Range for the specified match. */ function rangeFromMatch(match) { var wrappers = getWrappersByIndex(indexOf(match)); var rng = editor.dom.createRng(); rng.setStartBefore(wrappers[0]); rng.setEndAfter(wrappers[wrappers.length - 1]); return rng; } /** * Replaces the specified match with the specified text. * * @param {Object} match Match object to replace. * @param {String} text Text to replace the match with. * @return {DOMRange} DOM range produced after the replace. */ function replace(match, text) { var rng = rangeFromMatch(match); rng.deleteContents(); if (text.length > 0) { rng.insertNode(editor.dom.doc.createTextNode(text)); } return rng; } /** * Resets the DomTextMatcher instance. This will remove any wrapped nodes and remove any matches. * * @return {[type]} [description] */ function reset() { matches.splice(0, matches.length); unwrap(); return this; } text = getText(node); return { text: text, matches: matches, each: each, filter: filter, reset: reset, matchFromElement: matchFromElement, elementFromMatch: elementFromMatch, find: find, add: add, wrap: wrap, unwrap: unwrap, replace: replace, rangeFromMatch: rangeFromMatch, indexOf: indexOf }; }; }); // Included from: js/tinymce/plugins/spellchecker/classes/Plugin.js /** * Plugin.js * * Released under LGPL License. * Copyright (c) 1999-2015 Ephox Corp. All rights reserved * * License: http://www.tinymce.com/license * Contributing: http://www.tinymce.com/contributing */ /*jshint camelcase:false */ /** * This class contains all core logic for the spellchecker plugin. * * @class tinymce.spellcheckerplugin.Plugin * @private */ define("tinymce/spellcheckerplugin/Plugin", [ "tinymce/spellcheckerplugin/DomTextMatcher", "tinymce/PluginManager", "tinymce/util/Tools", "tinymce/ui/Menu", "tinymce/dom/DOMUtils", "tinymce/util/XHR", "tinymce/util/URI", "tinymce/util/JSON" ], function(DomTextMatcher, PluginManager, Tools, Menu, DOMUtils, XHR, URI, JSON) { PluginManager.add('spellchecker', function(editor, url) { var languageMenuItems, self = this, lastSuggestions, started, suggestionsMenu, settings = editor.settings; var hasDictionarySupport; function getTextMatcher() { if (!self.textMatcher) { self.textMatcher = new DomTextMatcher(editor.getBody(), editor); } return self.textMatcher; } function buildMenuItems(listName, languageValues) { var items = []; Tools.each(languageValues, function(languageValue) { items.push({ selectable: true, text: languageValue.name, data: languageValue.value }); }); return items; } var languagesString = settings.spellchecker_languages || 'English=en,Danish=da,Dutch=nl,Finnish=fi,French=fr_FR,' + 'German=de,Italian=it,Polish=pl,Portuguese=pt_BR,' + 'Spanish=es,Swedish=sv'; languageMenuItems = buildMenuItems('Language', Tools.map(languagesString.split(','), function(langPair) { langPair = langPair.split('='); return { name: langPair[0], value: langPair[1] }; }) ); function isEmpty(obj) { /*jshint unused:false*/ /*eslint no-unused-vars:0 */ for (var name in obj) { return false; } return true; } function showSuggestions(word, spans) { var items = [], suggestions = lastSuggestions[word]; Tools.each(suggestions, function(suggestion) { items.push({ text: suggestion, onclick: function() { editor.insertContent(editor.dom.encode(suggestion)); editor.dom.remove(spans); checkIfFinished(); } }); }); items.push({text: '-'}); if (hasDictionarySupport) { items.push({text: 'Add to Dictionary', onclick: function() { addToDictionary(word, spans); }}); } items.push.apply(items, [ {text: 'Ignore', onclick: function() { ignoreWord(word, spans); }}, {text: 'Ignore all', onclick: function() { ignoreWord(word, spans, true); }} ]); // Render menu suggestionsMenu = new Menu({ items: items, context: 'contextmenu', onautohide: function(e) { if (e.target.className.indexOf('spellchecker') != -1) { e.preventDefault(); } }, onhide: function() { suggestionsMenu.remove(); suggestionsMenu = null; } }); suggestionsMenu.renderTo(document.body); // Position menu var pos = DOMUtils.DOM.getPos(editor.getContentAreaContainer()); var targetPos = editor.dom.getPos(spans[0]); var root = editor.dom.getRoot(); // Adjust targetPos for scrolling in the editor if (root.nodeName == 'BODY') { targetPos.x -= root.ownerDocument.documentElement.scrollLeft || root.scrollLeft; targetPos.y -= root.ownerDocument.documentElement.scrollTop || root.scrollTop; } else { targetPos.x -= root.scrollLeft; targetPos.y -= root.scrollTop; } pos.x += targetPos.x; pos.y += targetPos.y; suggestionsMenu.moveTo(pos.x, pos.y + spans[0].offsetHeight); } function getWordCharPattern() { // Regexp for finding word specific characters this will split words by // spaces, quotes, copy right characters etc. It's escaped with unicode characters // to make it easier to output scripts on servers using different encodings // so if you add any characters outside the 128 byte range make sure to escape it return editor.getParam('spellchecker_wordchar_pattern') || new RegExp("[^" + "\\s!\"#$%&()*+,-./:;<=>?@[\\]^_{|}`" + "\u00a7\u00a9\u00ab\u00ae\u00b1\u00b6\u00b7\u00b8\u00bb" + "\u00bc\u00bd\u00be\u00bf\u00d7\u00f7\u00a4\u201d\u201c\u201e\u00a0\u2002\u2003\u2009" + "]+", "g"); } function defaultSpellcheckCallback(method, text, doneCallback, errorCallback) { var data = {method: method, lang: settings.spellchecker_language}, postData = ''; data[method == "addToDictionary" ? "word" : "text"] = text; Tools.each(data, function(value, key) { if (postData) { postData += '&'; } postData += key + '=' + encodeURIComponent(value); }); XHR.send({ url: new URI(url).toAbsolute(settings.spellchecker_rpc_url), type: "post", content_type: 'application/x-www-form-urlencoded', data: postData, success: function(result) { result = JSON.parse(result); if (!result) { var message = editor.translate("Server response wasn't proper JSON."); errorCallback(message); } else if (result.error) { errorCallback(result.error); } else { doneCallback(result); } }, error: function() { var message = editor.translate("The spelling service was not found: (") + settings.spellchecker_rpc_url + editor.translate(")"); errorCallback(message); } }); } function sendRpcCall(name, data, successCallback, errorCallback) { var spellCheckCallback = settings.spellchecker_callback || defaultSpellcheckCallback; spellCheckCallback.call(self, name, data, successCallback, errorCallback); } function spellcheck() { if (finish()) { return; } function errorCallback(message) { editor.notificationManager.open({text: message, type: 'error'}); editor.setProgressState(false); finish(); } editor.setProgressState(true); sendRpcCall("spellcheck", getTextMatcher().text, markErrors, errorCallback); editor.focus(); } function checkIfFinished() { if (!editor.dom.select('span.mce-spellchecker-word').length) { finish(); } } function addToDictionary(word, spans) { editor.setProgressState(true); sendRpcCall("addToDictionary", word, function() { editor.setProgressState(false); editor.dom.remove(spans, true); checkIfFinished(); }, function(message) { editor.notificationManager.open({text: message, type: 'error'}); editor.setProgressState(false); }); } function ignoreWord(word, spans, all) { editor.selection.collapse(); if (all) { Tools.each(editor.dom.select('span.mce-spellchecker-word'), function(span) { if (span.getAttribute('data-mce-word') == word) { editor.dom.remove(span, true); } }); } else { editor.dom.remove(spans, true); } checkIfFinished(); } function finish() { getTextMatcher().reset(); self.textMatcher = null; if (started) { started = false; editor.fire('SpellcheckEnd'); return true; } } function getElmIndex(elm) { var value = elm.getAttribute('data-mce-index'); if (typeof value == "number") { return "" + value; } return value; } function findSpansByIndex(index) { var nodes, spans = []; nodes = Tools.toArray(editor.getBody().getElementsByTagName('span')); if (nodes.length) { for (var i = 0; i < nodes.length; i++) { var nodeIndex = getElmIndex(nodes[i]); if (nodeIndex === null || !nodeIndex.length) { continue; } if (nodeIndex === index.toString()) { spans.push(nodes[i]); } } } return spans; } editor.on('click', function(e) { var target = e.target; if (target.className == "mce-spellchecker-word") { e.preventDefault(); var spans = findSpansByIndex(getElmIndex(target)); if (spans.length > 0) { var rng = editor.dom.createRng(); rng.setStartBefore(spans[0]); rng.setEndAfter(spans[spans.length - 1]); editor.selection.setRng(rng); showSuggestions(target.getAttribute('data-mce-word'), spans); } } }); editor.addMenuItem('spellchecker', { text: 'Spellcheck', context: 'tools', onclick: spellcheck, selectable: true, onPostRender: function() { var self = this; self.active(started); editor.on('SpellcheckStart SpellcheckEnd', function() { self.active(started); }); } }); function updateSelection(e) { var selectedLanguage = settings.spellchecker_language; e.control.items().each(function(ctrl) { ctrl.active(ctrl.settings.data === selectedLanguage); }); } /** * Find the specified words and marks them. It will also show suggestions for those words. * * @example * editor.plugins.spellchecker.markErrors({ * dictionary: true, * words: { * "word1": ["suggestion 1", "Suggestion 2"] * } * }); * @param {Object} data Data object containing the words with suggestions. */ function markErrors(data) { var suggestions; if (data.words) { hasDictionarySupport = !!data.dictionary; suggestions = data.words; } else { // Fallback to old format suggestions = data; } editor.setProgressState(false); if (isEmpty(suggestions)) { var message = editor.translate('No misspellings found.'); editor.notificationManager.open({text: message, type: 'info'}); started = false; return; } lastSuggestions = suggestions; getTextMatcher().find(getWordCharPattern()).filter(function(match) { return !!suggestions[match.text]; }).wrap(function(match) { return editor.dom.create('span', { "class": 'mce-spellchecker-word', "data-mce-bogus": 1, "data-mce-word": match.text }); }); started = true; editor.fire('SpellcheckStart'); } var buttonArgs = { tooltip: 'Spellcheck', onclick: spellcheck, onPostRender: function() { var self = this; editor.on('SpellcheckStart SpellcheckEnd', function() { self.active(started); }); } }; if (languageMenuItems.length > 1) { buttonArgs.type = 'splitbutton'; buttonArgs.menu = languageMenuItems; buttonArgs.onshow = updateSelection; buttonArgs.onselect = function(e) { settings.spellchecker_language = e.control.settings.data; }; } editor.addButton('spellchecker', buttonArgs); editor.addCommand('mceSpellCheck', spellcheck); editor.on('remove', function() { if (suggestionsMenu) { suggestionsMenu.remove(); suggestionsMenu = null; } }); editor.on('change', checkIfFinished); this.getTextMatcher = getTextMatcher; this.getWordCharPattern = getWordCharPattern; this.markErrors = markErrors; this.getLanguage = function() { return settings.spellchecker_language; }; // Set default spellchecker language if it's not specified settings.spellchecker_language = settings.spellchecker_language || settings.language || 'en'; }); }); expose(["tinymce/spellcheckerplugin/DomTextMatcher"]); })(this);