Files
server/usr/share/psa-horde/js/spellchecker.js
2026-01-07 20:52:11 +01:00

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;
}
});