291 lines
8.2 KiB
JavaScript
291 lines
8.2 KiB
JavaScript
/**
|
|
* This spell checker was inspired by work done by Garrison Locke, but
|
|
* was rewritten almost completely by Chuck Hagenbuch to use
|
|
* Prototype/Scriptaculous.
|
|
*
|
|
* Requires: prototype.js (v1.6.1+), KeyNavList.js
|
|
*
|
|
* Custom Events:
|
|
* --------------
|
|
* Custom events are triggered on the target element.
|
|
*
|
|
* 'SpellChecker:after'
|
|
* Fired when the spellcheck processing ends.
|
|
*
|
|
* 'SpellChecker:before'
|
|
* Fired before the spellcheck is performed.
|
|
*
|
|
* 'SpellChecker:error'
|
|
* Fired when at least 1 spellcheck error was found.
|
|
*
|
|
* 'SpellChecker:noerror'
|
|
* Fired when no spellcheck errors are found.
|
|
*
|
|
* @author Chuck Hagenbuch <chuck@horde.org>
|
|
* @copyright 2005-2015 Horde LLC
|
|
* @license LGPL-2.1 (http://www.horde.org/licenses/lgpl21)
|
|
*/
|
|
|
|
var SpellChecker = Class.create({
|
|
|
|
// Vars used and defaulting to null:
|
|
// bad, choices, disabled, htmlAreaParent, lc, locale, reviewDiv,
|
|
// statusButton, statusClass, suggestions, target, url
|
|
|
|
delimiter: "\1\0\1",
|
|
options: {},
|
|
resumeOnDblClick: true,
|
|
state: 'CheckSpelling',
|
|
|
|
// Options:
|
|
// bs = (array) Button states
|
|
// locales = (array) List of locales. See KeyNavList for structure.
|
|
// sc = (string) Status class
|
|
// statusButton = (string/element) DOM ID or element of the status
|
|
// button
|
|
// target = (string|Element) DOM element containing data
|
|
// url = (string) URL of specllchecker handler
|
|
initialize: function(opts)
|
|
{
|
|
this.url = opts.url;
|
|
this.target = $(opts.target);
|
|
this.statusButton = $(opts.statusButton);
|
|
this.buttonStates = opts.bs;
|
|
this.statusClass = opts.sc || this.statusButton.className;
|
|
this.disabled = false;
|
|
|
|
this.options.onComplete = this.onComplete.bind(this);
|
|
|
|
document.observe('click', this.onClick.bindAsEventListener(this));
|
|
|
|
if (opts.locales) {
|
|
this.lc = new KeyNavList(this.statusButton, {
|
|
list: opts.locales,
|
|
onChoose: this.setLocale.bindAsEventListener(this)
|
|
});
|
|
|
|
this.statusButton.insert({ after: new Element('SPAN', { className: 'horde-popdown horde-spellcheck-popdown' }) });
|
|
}
|
|
|
|
this.setStatus('CheckSpelling');
|
|
},
|
|
|
|
setLocale: function(locale)
|
|
{
|
|
this.locale = locale;
|
|
},
|
|
|
|
targetValue: function()
|
|
{
|
|
return Object.isUndefined(this.target.value)
|
|
? this.target.innerHTML
|
|
: this.target.value;
|
|
},
|
|
|
|
spellCheck: function()
|
|
{
|
|
this.target.fire('SpellChecker:before');
|
|
|
|
var opts = Object.clone(this.options),
|
|
p = $H();
|
|
|
|
this.setStatus('Checking');
|
|
|
|
p.set(this.target.identify(), this.targetValue());
|
|
|
|
if (this.locale) {
|
|
p.set('locale', this.locale);
|
|
}
|
|
if (this.htmlAreaParent) {
|
|
p.set('html', 1);
|
|
}
|
|
|
|
opts.parameters = p.toQueryString();
|
|
|
|
new Ajax.Request(this.url, opts);
|
|
},
|
|
|
|
onComplete: function(request)
|
|
{
|
|
var bad, content, washidden,
|
|
i = 0,
|
|
result = request.responseJSON;
|
|
|
|
if (Object.isUndefined(result)) {
|
|
this.setStatus('Error');
|
|
return;
|
|
}
|
|
|
|
this.suggestions = result.suggestions || [];
|
|
|
|
if (!this.suggestions.size()) {
|
|
this.setStatus('CheckSpelling');
|
|
this.target.fire('SpellChecker:noerror');
|
|
return;
|
|
}
|
|
|
|
bad = result.bad || [];
|
|
|
|
content = this.targetValue();
|
|
content = this.htmlAreaParent
|
|
? content.replace(/\r?\n/g, '')
|
|
: content.replace(/\r?\n/g, this.delimiter).escapeHTML();
|
|
|
|
$A(bad).each(function(node) {
|
|
var re_text = '<span index="' + (i++) + '" class="spellcheckIncorrect">' + node + '</span>';
|
|
content = content.replace(new RegExp("(?:^|\\b)" + RegExp.escape(node) + "(?:\\b|$)", 'g'), re_text);
|
|
|
|
// Go through and see if we matched anything inside a tag (i.e.
|
|
// class/spellcheckIncorrect is often matched if using a
|
|
// non-English lang).
|
|
content = content.replace(new RegExp("(<[^>]*)" + RegExp.escape(re_text) + "([^>]*>)", 'g'), '$1' + node + '$2');
|
|
}, this);
|
|
|
|
if (!this.reviewDiv) {
|
|
this.reviewDiv = new Element('DIV', { className: this.target.readAttribute('class') }).addClassName('spellcheck').setStyle({ overflow: 'auto' });
|
|
if (this.resumeOnDblClick) {
|
|
this.reviewDiv.observe('dblclick', this.resume.bind(this));
|
|
}
|
|
}
|
|
|
|
if (!this.target.visible()) {
|
|
this.target.show();
|
|
washidden = true;
|
|
}
|
|
this.reviewDiv.setStyle({ width: this.target.clientWidth + 'px', height: this.target.clientHeight + 'px'});
|
|
if (washidden) {
|
|
this.target.hide();
|
|
}
|
|
|
|
if (!this.htmlAreaParent) {
|
|
content = content.replace(new RegExp(this.delimiter, 'g'), '<br />');
|
|
}
|
|
this.reviewDiv.update(content);
|
|
|
|
if (this.htmlAreaParent) {
|
|
$(this.htmlAreaParent).insert({ bottom: this.reviewDiv });
|
|
} else {
|
|
this.target.hide().insert({ before: this.reviewDiv });
|
|
}
|
|
|
|
this.setStatus('ResumeEdit');
|
|
|
|
this.target.fire('SpellChecker:error');
|
|
},
|
|
|
|
onClick: function(e)
|
|
{
|
|
var data = [], index, elt = e.element();
|
|
|
|
if (this.disabled) {
|
|
return;
|
|
}
|
|
|
|
if (elt == this.statusButton) {
|
|
switch (this.state) {
|
|
case 'CheckSpelling':
|
|
this.spellCheck();
|
|
break;
|
|
|
|
case 'ResumeEdit':
|
|
this.resume();
|
|
break;
|
|
}
|
|
|
|
e.stop();
|
|
} else if (elt.hasClassName('horde-spellcheck-popdown')) {
|
|
this.lc.show();
|
|
this.lc.ignoreClick(e);
|
|
e.stop();
|
|
} else if (elt.hasClassName('spellcheckIncorrect')) {
|
|
index = e.element().readAttribute('index');
|
|
|
|
$A(this.suggestions[index]).each(function(node) {
|
|
data.push({ l: node, v: node });
|
|
});
|
|
|
|
if (this.choices) {
|
|
this.choices.updateBase(elt);
|
|
this.choices.opts.onChoose = function(val) {elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });};
|
|
} else {
|
|
this.choices = new KeyNavList(elt, {
|
|
esc: true,
|
|
onChoose: function(val) {
|
|
elt.update(val).writeAttribute({ className: 'spellcheckCorrected' });
|
|
}
|
|
});
|
|
}
|
|
|
|
this.choices.show(data);
|
|
this.choices.ignoreClick(e);
|
|
e.stop();
|
|
}
|
|
},
|
|
|
|
resume: function()
|
|
{
|
|
if (!this.reviewDiv) {
|
|
return;
|
|
}
|
|
|
|
var t;
|
|
|
|
[ 'Corrected', 'Incorrect' ].each(function(i) {
|
|
this.reviewDiv.select('span.spellcheck' + i).each(function(n) {
|
|
n.insert({ before: n.innerHTML }).remove();
|
|
});
|
|
}, this);
|
|
|
|
t = this.reviewDiv.innerHTML;
|
|
if (!this.htmlAreaParent) {
|
|
t = t.replace(/<br *\/?>/gi, this.delimiter).unescapeHTML().replace(new RegExp(this.delimiter, 'g'), "\n");
|
|
}
|
|
this.target.setValue(t);
|
|
this.target.enable();
|
|
|
|
if (this.resumeOnDblClick) {
|
|
this.reviewDiv.stopObserving('dblclick');
|
|
}
|
|
this.reviewDiv.remove();
|
|
this.reviewDiv = null;
|
|
|
|
this.setStatus('CheckSpelling');
|
|
|
|
if (!this.htmlAreaParent) {
|
|
this.target.show();
|
|
}
|
|
|
|
this.target.fire('SpellChecker:after');
|
|
},
|
|
|
|
setStatus: function(state)
|
|
{
|
|
if (!this.statusButton) {
|
|
return;
|
|
}
|
|
|
|
this.state = state;
|
|
switch (this.statusButton.tagName) {
|
|
case 'INPUT':
|
|
this.statusButton.setValue(this.buttonStates[state]);
|
|
break;
|
|
|
|
case 'A':
|
|
this.statusButton.update(this.buttonStates[state]);
|
|
break;
|
|
}
|
|
this.statusButton.className = this.statusClass + ' spellcheck' + state;
|
|
},
|
|
|
|
isActive: function()
|
|
{
|
|
return this.reviewDiv;
|
|
},
|
|
|
|
disable: function(disable)
|
|
{
|
|
this.disabled = disable;
|
|
}
|
|
|
|
});
|