428 lines
11 KiB
JavaScript
428 lines
11 KiB
JavaScript
|
/* global tinymce */
|
||
|
/*
|
||
|
* TinyMCE Writing Improvement Tool Plugin
|
||
|
* Author: Raphael Mudge (raffi@automattic.com)
|
||
|
*
|
||
|
* Updated for TinyMCE 4.0
|
||
|
*
|
||
|
* http://www.afterthedeadline.com
|
||
|
*
|
||
|
* Distributed under the LGPL
|
||
|
*
|
||
|
* Derived from:
|
||
|
* $Id: editor_plugin_src.js 425 2007-11-21 15:17:39Z spocke $
|
||
|
*
|
||
|
* @author Moxiecode
|
||
|
* @copyright Copyright (C) 2004-2008, Moxiecode Systems AB, All rights reserved.
|
||
|
*
|
||
|
* Moxiecode Spell Checker plugin released under the LGPL with TinyMCE
|
||
|
*/
|
||
|
tinymce.PluginManager.add( 'AtD', function( editor ) {
|
||
|
var suggestionsMenu, started, atdCore, dom,
|
||
|
each = tinymce.each;
|
||
|
|
||
|
/* initializes the functions used by the AtD Core UI Module */
|
||
|
function initAtDCore() {
|
||
|
|
||
|
atdCore = new window.AtDCore();
|
||
|
atdCore.map = each;
|
||
|
atdCore._isTinyMCE = true;
|
||
|
|
||
|
atdCore.getAttrib = function( node, key ) {
|
||
|
return dom.getAttrib( node, key );
|
||
|
};
|
||
|
|
||
|
atdCore.findSpans = function( parent ) {
|
||
|
if ( parent === undefined ) {
|
||
|
return dom.select('span');
|
||
|
} else {
|
||
|
return dom.select( 'span', parent );
|
||
|
}
|
||
|
};
|
||
|
|
||
|
atdCore.hasClass = function( node, className ) {
|
||
|
return dom.hasClass( node, className );
|
||
|
};
|
||
|
|
||
|
atdCore.contents = function( node ) {
|
||
|
return node.childNodes;
|
||
|
};
|
||
|
|
||
|
atdCore.replaceWith = function( old_node, new_node ) {
|
||
|
return dom.replace( new_node, old_node );
|
||
|
};
|
||
|
|
||
|
atdCore.create = function( node_html ) {
|
||
|
return dom.create( 'span', { 'class': 'mceItemHidden', 'data-mce-bogus': 1 }, node_html );
|
||
|
};
|
||
|
|
||
|
atdCore.removeParent = function( node ) {
|
||
|
dom.remove( node, true );
|
||
|
return node;
|
||
|
};
|
||
|
|
||
|
atdCore.remove = function( node ) {
|
||
|
dom.remove( node );
|
||
|
};
|
||
|
|
||
|
atdCore.setIgnoreStrings( editor.getParam( 'atd_ignore_strings', [] ).join(',') );
|
||
|
atdCore.showTypes( editor.getParam( 'atd_show_types', '' ) );
|
||
|
}
|
||
|
|
||
|
function getLang( key, defaultStr ) {
|
||
|
return ( window.AtD_l10n_r0ar && window.AtD_l10n_r0ar[key] ) || defaultStr;
|
||
|
}
|
||
|
|
||
|
function isMarkedNode( node ) {
|
||
|
return ( node.className && /\bhidden(GrammarError|SpellError|Suggestion)\b/.test( node.className ) );
|
||
|
}
|
||
|
|
||
|
function markMyWords( errors ) {
|
||
|
return atdCore.markMyWords( atdCore.contents( editor.getBody() ), errors );
|
||
|
}
|
||
|
|
||
|
// If no more suggestions, finish.
|
||
|
function checkIfFinished() {
|
||
|
if ( ! editor.dom.select('span.hiddenSpellError, span.hiddenGrammarError, span.hiddenSuggestion').length ) {
|
||
|
if ( suggestionsMenu ) {
|
||
|
suggestionsMenu.hideMenu();
|
||
|
}
|
||
|
|
||
|
finish();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
function ignoreWord( target, word, all ) {
|
||
|
var dom = editor.dom;
|
||
|
|
||
|
if ( all ) {
|
||
|
each( editor.dom.select( 'span.hiddenSpellError, span.hiddenGrammarError, span.hiddenSuggestion' ), function( node ) {
|
||
|
var text = node.innerText || node.textContent;
|
||
|
|
||
|
if ( text === word ) {
|
||
|
dom.remove( node, true );
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
dom.remove( target, true );
|
||
|
}
|
||
|
|
||
|
checkIfFinished();
|
||
|
}
|
||
|
|
||
|
// Called when the user clicks "Finish" or when no more suggestions left.
|
||
|
// Removes all remaining spans and fires custom event.
|
||
|
function finish() {
|
||
|
var node,
|
||
|
dom = editor.dom,
|
||
|
regex = new RegExp( 'mceItemHidden|hidden(((Grammar|Spell)Error)|Suggestion)' ),
|
||
|
nodes = dom.select('span'),
|
||
|
i = nodes.length;
|
||
|
|
||
|
while ( i-- ) { // reversed
|
||
|
node = nodes[i];
|
||
|
|
||
|
if ( node.className && regex.test( node.className ) ) {
|
||
|
dom.remove( node, true );
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// Rebuild the DOM so AtD core can find the text nodes
|
||
|
editor.setContent( editor.getContent({ format: 'raw' }), { format: 'raw' } );
|
||
|
|
||
|
started = false;
|
||
|
editor.nodeChanged();
|
||
|
editor.fire('SpellcheckEnd');
|
||
|
}
|
||
|
|
||
|
function sendRequest( file, data, success ) {
|
||
|
var id = editor.getParam( 'atd_rpc_id', '12345678' ),
|
||
|
url = editor.getParam( 'atd_rpc_url', '{backend}' );
|
||
|
|
||
|
if ( url === '{backend}' || id === '12345678' ) {
|
||
|
window.alert( 'Please specify: atd_rpc_url and atd_rpc_id' );
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// create the nifty spinny thing that says "hizzo, I'm doing something fo realz"
|
||
|
editor.setProgressState( true );
|
||
|
|
||
|
tinymce.util.XHR.send({
|
||
|
url: url + '/' + file,
|
||
|
content_type: 'text/xml',
|
||
|
type: 'POST',
|
||
|
data: 'data=' + encodeURI( data ).replace( /&/g, '%26' ) + '&key=' + id,
|
||
|
success: success,
|
||
|
error: function( type, req, o ) {
|
||
|
editor.setProgressState();
|
||
|
window.alert( type + '\n' + req.status + '\nAt: ' + o.url );
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
function storeIgnoredStrings( /*text*/ ) {
|
||
|
// Store in sessionStorage?
|
||
|
}
|
||
|
|
||
|
function setAlwaysIgnore( text ) {
|
||
|
var url = editor.getParam( 'atd_ignore_rpc_url' );
|
||
|
|
||
|
if ( ! url || url === '{backend}' ) {
|
||
|
// Store ignored words for this session only
|
||
|
storeIgnoredStrings( text );
|
||
|
} else {
|
||
|
// Plugin is configured to send ignore preferences to server, do that
|
||
|
tinymce.util.XHR.send({
|
||
|
url: url + encodeURIComponent( text ) + '&key=' + editor.getParam( 'atd_rpc_id', '12345678' ),
|
||
|
content_type: 'text/xml',
|
||
|
type: 'GET',
|
||
|
error: function() {
|
||
|
storeIgnoredStrings( text );
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Update atd_ignore_strings with the new value
|
||
|
atdCore.setIgnoreStrings( text );
|
||
|
}
|
||
|
|
||
|
// Create the suggestions menu
|
||
|
function showSuggestions( target ) {
|
||
|
var pos, root, targetPos,
|
||
|
items = [],
|
||
|
text = target.innerText || target.textContent,
|
||
|
errorDescription = atdCore.findSuggestion( target );
|
||
|
|
||
|
if ( ! errorDescription ) {
|
||
|
items.push({
|
||
|
text: getLang( 'menu_title_no_suggestions', 'No suggestions' ),
|
||
|
classes: 'atd-menu-title',
|
||
|
disabled: true
|
||
|
});
|
||
|
} else {
|
||
|
items.push({
|
||
|
text: errorDescription.description,
|
||
|
classes: 'atd-menu-title',
|
||
|
disabled: true
|
||
|
});
|
||
|
|
||
|
if ( errorDescription.suggestions.length ) {
|
||
|
items.push({ text: '-' }); // separator
|
||
|
|
||
|
each( errorDescription.suggestions, function( suggestion ) {
|
||
|
items.push({
|
||
|
text: suggestion,
|
||
|
onclick: function() {
|
||
|
atdCore.applySuggestion( target, suggestion );
|
||
|
checkIfFinished();
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
if ( errorDescription && errorDescription.moreinfo ) {
|
||
|
items.push({ text: '-' }); // separator
|
||
|
|
||
|
items.push({
|
||
|
text: getLang( 'menu_option_explain', 'Explain...' ),
|
||
|
onclick : function() {
|
||
|
editor.windowManager.open({
|
||
|
title: getLang( 'menu_option_explain', 'Explain...' ),
|
||
|
url: errorDescription.moreinfo,
|
||
|
width: 480,
|
||
|
height: 380,
|
||
|
inline: true
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
items.push.apply( items, [
|
||
|
{ text: '-' }, // separator
|
||
|
|
||
|
{ text: getLang( 'menu_option_ignore_once', 'Ignore suggestion' ), onclick: function() {
|
||
|
ignoreWord( target, text );
|
||
|
}}
|
||
|
]);
|
||
|
|
||
|
if ( editor.getParam( 'atd_ignore_enable' ) ) {
|
||
|
items.push({
|
||
|
text: getLang( 'menu_option_ignore_always', 'Ignore always' ),
|
||
|
onclick: function() {
|
||
|
setAlwaysIgnore( text );
|
||
|
ignoreWord( target, text, true );
|
||
|
}
|
||
|
});
|
||
|
} else {
|
||
|
items.push({
|
||
|
text: getLang( 'menu_option_ignore_all', 'Ignore all' ),
|
||
|
onclick: function() {
|
||
|
ignoreWord( target, text, true );
|
||
|
}
|
||
|
});
|
||
|
}
|
||
|
|
||
|
// Render menu
|
||
|
suggestionsMenu = new tinymce.ui.Menu({
|
||
|
items: items,
|
||
|
context: 'contextmenu',
|
||
|
onautohide: function( event ) {
|
||
|
if ( isMarkedNode( event.target ) ) {
|
||
|
event.preventDefault();
|
||
|
}
|
||
|
},
|
||
|
onhide: function() {
|
||
|
suggestionsMenu.remove();
|
||
|
suggestionsMenu = null;
|
||
|
}
|
||
|
});
|
||
|
|
||
|
suggestionsMenu.renderTo( document.body );
|
||
|
|
||
|
// Position menu
|
||
|
pos = tinymce.DOM.getPos( editor.getContentAreaContainer() );
|
||
|
targetPos = editor.dom.getPos( target );
|
||
|
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 + target.offsetHeight );
|
||
|
}
|
||
|
|
||
|
// Init everything
|
||
|
editor.on( 'init', function() {
|
||
|
if ( typeof window.AtDCore === 'undefined' ) {
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// Set dom and atdCore
|
||
|
dom = editor.dom;
|
||
|
initAtDCore();
|
||
|
|
||
|
// add a command to request a document check and process the results.
|
||
|
editor.addCommand( 'mceWritingImprovementTool', function( callback ) {
|
||
|
var results,
|
||
|
errorCount = 0;
|
||
|
|
||
|
if ( typeof callback !== 'function' ) {
|
||
|
callback = function(){};
|
||
|
}
|
||
|
|
||
|
// checks if a global var for click stats exists and increments it if it does...
|
||
|
if ( typeof window.AtD_proofread_click_count !== 'undefined' ) {
|
||
|
window.AtD_proofread_click_count++;
|
||
|
}
|
||
|
|
||
|
// remove the previous errors
|
||
|
if ( started ) {
|
||
|
finish();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// send request to our service
|
||
|
sendRequest( 'checkDocument', editor.getContent({ format: 'raw' }), function( data, request ) {
|
||
|
// turn off the spinning thingie
|
||
|
editor.setProgressState();
|
||
|
|
||
|
// if the server is not accepting requests, let the user know
|
||
|
if ( request.status !== 200 || request.responseText.substr( 1, 4 ) === 'html' || ! request.responseXML ) {
|
||
|
editor.windowManager.alert(
|
||
|
getLang( 'message_server_error', 'There was a problem communicating with the Proofreading service. Try again in one minute.' ),
|
||
|
callback(0)
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
// check to see if things are broken first and foremost
|
||
|
if ( request.responseXML.getElementsByTagName('message').item(0) !== null ) {
|
||
|
editor.windowManager.alert(
|
||
|
request.responseXML.getElementsByTagName('message').item(0).firstChild.data,
|
||
|
callback(0)
|
||
|
);
|
||
|
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
results = atdCore.processXML( request.responseXML );
|
||
|
|
||
|
if ( results.count > 0 ) {
|
||
|
errorCount = markMyWords( results.errors );
|
||
|
}
|
||
|
|
||
|
if ( ! errorCount ) {
|
||
|
editor.windowManager.alert( getLang( 'message_no_errors_found', 'No writing errors were found.' ) );
|
||
|
} else {
|
||
|
started = true;
|
||
|
editor.fire('SpellcheckStart');
|
||
|
}
|
||
|
|
||
|
callback( errorCount );
|
||
|
});
|
||
|
});
|
||
|
|
||
|
if ( editor.settings.content_css !== false ) {
|
||
|
// CSS for underlining suggestions
|
||
|
dom.addStyle( '.hiddenSpellError{border-bottom:2px solid red;cursor:default;}' +
|
||
|
'.hiddenGrammarError{border-bottom:2px solid green;cursor:default;}' +
|
||
|
'.hiddenSuggestion{border-bottom:2px solid blue;cursor:default;}' );
|
||
|
}
|
||
|
|
||
|
// Menu z-index > DFW
|
||
|
tinymce.DOM.addStyle( 'div.mce-floatpanel{z-index:150100 !important;}' );
|
||
|
|
||
|
// Click on misspelled word
|
||
|
editor.on( 'click', function( event ) {
|
||
|
if ( isMarkedNode( event.target ) ) {
|
||
|
event.preventDefault();
|
||
|
editor.selection.select( event.target );
|
||
|
// Create the suggestions menu
|
||
|
showSuggestions( event.target );
|
||
|
}
|
||
|
});
|
||
|
});
|
||
|
|
||
|
editor.addMenuItem( 'spellchecker', {
|
||
|
text: getLang( 'button_proofread_tooltip', 'Proofread Writing' ),
|
||
|
context: 'tools',
|
||
|
cmd: 'mceWritingImprovementTool',
|
||
|
onPostRender: function() {
|
||
|
var self = this;
|
||
|
|
||
|
editor.on('SpellcheckStart SpellcheckEnd', function() {
|
||
|
self.active( started );
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
editor.addButton( 'spellchecker', {
|
||
|
tooltip: getLang( 'button_proofread_tooltip', 'Proofread Writing' ),
|
||
|
cmd: 'mceWritingImprovementTool',
|
||
|
onPostRender: function() {
|
||
|
var self = this;
|
||
|
|
||
|
editor.on( 'SpellcheckStart SpellcheckEnd', function() {
|
||
|
self.active( started );
|
||
|
});
|
||
|
}
|
||
|
});
|
||
|
|
||
|
editor.on( 'remove', function() {
|
||
|
if ( suggestionsMenu ) {
|
||
|
suggestionsMenu.remove();
|
||
|
suggestionsMenu = null;
|
||
|
}
|
||
|
});
|
||
|
});
|