discourse-legacysite-perl/site/common/static/js/spellcheck.js

561 lines
22 KiB
JavaScript
Raw Normal View History

2024-06-17 12:27:49 +00:00
/*
* =================================================================
* Gossamer Mail - enhanced email management system
*
* Website : http://gossamer-threads.com/
* Support : http://gossamer-threads.com/scripts/support/
* Revision : $Id: spellcheck.js,v 1.5 2006/11/18 14:22:58 brewt Exp $
*
* Copyright (c) 2004 Gossamer Threads Inc. All Rights Reserved.
* Redistribution in part or in whole strictly prohibited. Please
* see LICENSE file for full details.
* =================================================================
*/
function spellCheck(objName) {
this.config = {
// FIXME remove this?
menuWidth : 110, // The width of the misspelled words menu
menuMaxRows : 10, // The maximum number of suggestions (maximum is 10)
disableElements : [], // An array of html elements which should be disabled in spell check mode
addPermission : true, // Whether or not words can be added to the dictionary
textCheckButton : '', // Text of the button when in regular mode (uses original value)
editorObj : null // The advanced HTML editor object
};
this.lang = {
'add' : 'Add...',
'edit' : 'Edit...',
'delete' : 'Delete',
'replaceAll' : 'Replace All',
'resume' : 'Resume Compose',
'title' : 'Suggested Spellings',
'noMisspelled' : 'No words were misspelled.'
};
this.id = {
checkContent : 'compose_body',
checkFrame : 'spellcheck_frame',
checkButton : 'button_spellcheck',
checkResult : 'spellcheck_result',
menuSuggest : 'spellcheck_suggestions',
htmlCompose : 'compose_is_html',
extra : [] // Extra input values to pass through (these elements must exist both on the page and spellcheck-inline form)
};
this.name = objName;
this.words = [];
this.corrections = {};
this.selectedIndex = -1;
this.loaded = false;
this.formLoaded = false;
this.misspelled = false;
this.replaceAll = false;
this.loadingInterval;
}
spellCheck.prototype.load = function () {
if (this.config.editorObj) {
var self = this;
this.loadingInterval = setInterval(function () { self.init(); }, 250);
}
else
this.init();
}
spellCheck.prototype.init = function () {
if (this.config.editorObj) {
if (this.config.editorObj.loaded)
clearInterval(this.loadingInterval);
else
return;
}
this.objects = {
checkForm : null,
checkContent : document.getElementById(this.id.checkContent),
checkFrame : document.getElementById(this.id.checkFrame),
checkButton : [],
checkResult : document.getElementById(this.id.checkResult),
menuSuggest : document.getElementById(this.id.menuSuggest)
};
if (typeof this.id.checkButton == 'string')
this.objects.checkButton.push(document.getElementById(this.id.checkButton));
else
for (var i = 0; i < this.id.checkButton.length; i++)
this.objects.checkButton.push(document.getElementById(this.id.checkButton[i]));
if (!this.objects.menuSuggest) {
this.objects.menuSuggest = document.createElement('div');
this.objects.menuSuggest.id = this.id.menuSuggest;
this.objects.menuSuggest.style.display = 'none';
document.body.appendChild(this.objects.menuSuggest);
}
if (!this.objects.checkResult) {
this.objects.checkResult = document.createElement('div');
this.objects.checkResult.id = this.id.checkResult;
this.objects.checkResult.style.display = 'none';
document.body.appendChild(this.objects.checkResult);
}
if (!this.objects.checkContent) return;
var self = this;
for (var i = 0; i < this.objects.checkButton.length; i++)
registerEvent(this.objects.checkButton[i], 'click', function (e) {
if (!e) e = window.event;
if (self.formLoaded) {
if (self.objects.checkResult.style.display == 'none')
self.prepareCheck();
else
self.resumeCompose();
}
cancelEvent(e);
stopPropagation(e);
});
// This handler should only be called on a click on everything but the misspelled
// words, edit_word input, and the menu.
registerEvent(document, 'click', function (e) {
if (!e) e = window.event;
// FIXME this should call dMenuObj.hide();
if (self.objects.menuSuggest.style.display != 'none')
self.objects.menuSuggest.style.display = 'none';
self.checkEditWord();
});
registerEvent(window, 'resize', function () {
if (self.objects.checkResult.style.display != 'none')
self.showCheckResult();
});
this.loaded = true;
};
spellCheck.prototype.prepareCheck = function () {
this.objects.checkForm.elements.content.value = this.config.editorObj ? this.config.editorObj.getContent(true) : this.objects.checkContent.value;
if (this.objects.checkForm.elements.content.value == '') {
alert(this.lang.noMisspelled);
return;
}
var html = document.getElementById(this.id.htmlCompose);
if (this.objects.checkForm.elements[this.id.htmlCompose])
this.objects.checkForm.elements[this.id.htmlCompose].value = html ? html.value : 0;
for (var i = 0; i < this.id.extra.length; i++) {
var sourceObj = document.getElementById(this.id.extra[i]);
var destObj = this.objects.checkForm[this.id.extra[i]];
if (sourceObj && destObj) destObj.value = sourceObj.value;
}
this.objects.checkForm.submit();
}
spellCheck.prototype.resumeCompose = function () {
var content = '';
for (var i = 0; i < this.words.length; i++)
content += this.words[i].word;
if (this.config.editorObj)
content = content.replace(/<a dhref=/g, '<a href=');
if (this.config.editorObj)
this.config.editorObj.setContent(content);
else
this.objects.checkContent.value = content;
this.objects.menuSuggest.style.display = 'none';
this.hideCheckResult();
if (this.config.editorObj) {
this.config.editorObj.enable();
this.config.editorObj.focus();
}
else
this.objects.checkContent.focus();
for (var i = 0; i < this.objects.checkButton.length; i++)
this.objects.checkButton[i].value = this.config.textCheckButton;
this.disableElementsEnable(true);
}
spellCheck.prototype.processCheck = function () {
var self = this;
if (this.words.length == 0 || !this.misspelled) {
alert(this.lang.noMisspelled);
return;
}
if (this.config.editorObj)
this.config.editorObj.disable();
var content = '';
for (var i = 0; i < this.words.length; i++) {
if (!this.words[i].misspelled)
content += this.config.editorObj ? this.words[i].word : htmlEscape(this.words[i].word).replace(/\n/g, '<br>').replace(/ /g, ' &nbsp;');
else
content += '<span id="msw_' + i + '">' + this.words[i].word + '</span>';
}
if (this.config.editorObj)
cmntent = content.replace(/<a href=/gi, '<a dhref=');
else
content = '<span class="pre">' + content + '</span>';
this.objects.checkResult.innerHTML = content;
for (var i = 0; i < this.words.length; i++) {
if (!this.words[i].misspelled)
continue;
var msw = document.getElementById('msw_' + i);
msw.title = this.lang.title;
msw.className = 'misspelled';
registerEvent(msw, 'click', function (e) {
if (!e) e = window.event;
var targ = e.target ? e.target : e.srcElement;
if (targ.nodeType == 3) // defeat Safari bug
targ = targ.parentNode;
self.checkEditWord();
self.selectedIndex = parseInt(targ.id.replace(/^msw_/, ''));
var suggest = new dMenu(self, self.corrections[self.words[self.selectedIndex].oword.toLowerCase()]);
var selected = document.getElementById('msw_' + self.selectedIndex);
var posY = findPosY(selected) + selected.offsetHeight + 1 - self.objects.checkResult.scrollTop;
var posX = findPosX(selected);
suggest.show(posX, posY);
stopPropagation(e);
});
}
this.showCheckResult();
if (!this.config.textCheckButton)
this.config.textCheckButton = this.objects.checkButton[0].value;
for (var i = 0; i < this.objects.checkButton.length; i++)
this.objects.checkButton[i].value = this.lang.resume;
this.disableElementsEnable(false);
}
spellCheck.prototype.disableElementsEnable = function (flag) {
for (var i = 0; i < this.config.disableElements.length; i++) {
var element = document.getElementById(this.config.disableElements[i]);
if (!element)
continue;
if (element.tagName == 'INPUT')
element.disabled = !flag;
else
element.style.visibility = flag ? 'visible' : 'hidden';
}
}
spellCheck.prototype.showCheckResult = function () {
var width, height, left, top;
if (this.config.editorObj) {
width = this.config.editorObj.objects.editableFrame.offsetWidth -
getStyleLength(this.config.editorObj.objects.editableFrame, 'border-left-width') -
getStyleLength(this.config.editorObj.objects.editableFrame, 'border-right-width');
height = this.config.editorObj.objects.editableFrame.offsetHeight -
getStyleLength(this.config.editorObj.objects.editableFrame, 'border-top-width') -
getStyleLength(this.config.editorObj.objects.editableFrame, 'border-bottom-width');
left = findPosX(this.config.editorObj.objects.editorFrame) + findPosX(this.config.editorObj.objects.editableFrame) +
getStyleLength(this.config.editorObj.objects.editorFrame, 'border-left-width');
top = findPosY(this.config.editorObj.objects.editorFrame) + findPosY(this.config.editorObj.objects.editableFrame) +
getStyleLength(this.config.editorObj.objects.editorFrame, 'border-top-width');
}
else {
width = this.objects.checkContent.offsetWidth -
getStyleLength(this.objects.checkContent, 'border-left-width') -
getStyleLength(this.objects.checkContent, 'border-right-width');
height = this.objects.checkContent.offsetHeight -
getStyleLength(this.objects.checkContent, 'border-top-width') -
getStyleLength(this.objects.checkContent, 'border-bottom-width');
left = findPosX(this.objects.checkContent) + getStyleLength(this.objects.checkContent, 'border-left-width');
top = findPosY(this.objects.checkContent) + getStyleLength(this.objects.checkContent, 'border-top-width');
}
this.objects.checkResult.style.top = top + 'px';
this.objects.checkResult.style.left = left + 'px';
// set display: block before width and height because in Safari, you can't get current styles of hidden elements
this.objects.checkResult.style.display = '';
this.objects.checkResult.style.width = calcCSSWidth(this.objects.checkResult, width);
this.objects.checkResult.style.height = calcCSSHeight(this.objects.checkResult, height);
}
spellCheck.prototype.hideCheckResult = function () {
this.objects.checkResult.style.display = 'none';
}
spellCheck.prototype.checkEditWord = function () {
var edit_word = document.getElementById('edit_word');
if (edit_word) {
var suggest = new dMenu(this, this.corrections[this.words[edit_word.name].oword.toLowerCase()]);
suggest.updateWord(13);
}
}
function dMenu(spellcheck, corrections) {
var oMenu = spellcheck.objects.menuSuggest;
while (oMenu.hasChildNodes())
oMenu.removeChild(oMenu.firstChild);
this.object = oMenu;
this.corrections = corrections;
this.spellcheck = spellcheck;
if (corrections.length < 0) return;
var menu_rows = corrections.length > spellcheck.config.menuMaxRows ? spellcheck.config.menuMaxRows : corrections.length;
for (var i = 0; i < menu_rows; i++)
this.addNode('menu_' + i, corrections[i]);
var actions = [['hr', '', '<hr />'], ['add', 'wordAdd', spellcheck.lang.add], ['edit', 'wordEdit', spellcheck.lang.edit], ['delete', 'wordEdit', spellcheck.lang['delete']], ['option', 'wordOption', '<span id="replace_all">' + spellcheck.lang.replaceAll + '</span>']];
menu_rows += actions.length;
var new_word = this.newWord();
for (var i = 0; i < actions.length; i++) {
if (actions[i][0] == 'add' && (this.spellcheck.config.addPermission == false || !new_word)) {
menu_rows--;
continue;
}
this.addNode(actions[i][0], actions[i][2]);
}
oMenu.style.width = spellcheck.config.menuWidth + 'px';
}
dMenu.prototype.show = function (posX, posY) {
this.object.style.left = posX + 'px';
this.object.style.top = posY + 'px';
if (this.spellcheck.replaceAll && document.getElementById('replace_all'))
document.getElementById('replace_all').className = 'replace_all';
this.object.style.display = '';
}
dMenu.prototype.hide = function () {
this.object.style.display = 'none';
if (document.getElementById('replace_all'))
document.getElementById('replace_all').className = '';
}
dMenu.prototype.addNode = function (name, text) {
var oNode = document.createElement('div');
// FIXME text may contain html
oNode.innerHTML = text;
if (name != 'hr') {
oNode.id = name;
oNode.className = 'menu-item';
oNode.setAttribute('unselectable', 'on');
if (oNode.offsetWidth > this.spellcheck.config.menuWidth)
this.spellcheck.config.menuWidth = oNode.offsetWidth + 'px';
var self = this;
registerEvent(oNode, 'mouseover', function (e) {
if (!e) e = window.event;
var targ = e.target ? e.target : e.srcElement;
if (targ.nodeType == 3) // defeat Safari bug
targ = targ.parentNode;
// The target when clicking on "Replace All" will be the span
if (targ.tagName == 'SPAN')
targ = targ.parentNode;
targ.className = 'menu-item mouseover';
});
registerEvent(oNode, 'mouseout', function (e) {
if (!e) e = window.event;
var targ = e.target ? e.target : e.srcElement;
if (targ.nodeType == 3)
targ = targ.parentNode;
if (targ.tagName == 'SPAN')
targ = targ.parentNode;
targ.className = 'menu-item';
});
registerEvent(oNode, 'click', function (e) {
if (!e) e = window.event;
var targ = e.target ? e.target : e.srcElement;
if (targ.nodeType == 3)
targ = targ.parentNode;
if (targ.tagName == 'SPAN')
targ = targ.parentNode;
if (targ.id == 'add')
self.addWord();
else if (targ.id == 'edit')
self.editWord();
else if (targ.id == 'delete')
self.deleteWord();
else if (targ.id == 'option')
self.setOption();
else
self.replaceWord(targ.firstChild.nodeValue);
stopPropagation(e);
});
}
this.object.appendChild(oNode);
}
dMenu.prototype.addWord = function () {
this.hide();
if (!this.spellcheck.words[this.spellcheck.selectedIndex].misspelled) return;
if (!this.spellcheck.config.addPermission) {
alert("You cannot add words to the dictionary.");
return;
}
var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
if (!selected) return;
var word = this.spellcheck.words[this.spellcheck.selectedIndex].word;
if (confirm("Are you sure you want to add '" + word + "' to the dictionary?")) {
this.spellcheck.objects.checkForm.elements['do'].value = 'add_word';
this.spellcheck.objects.checkForm.elements.content.value = word;
this.spellcheck.objects.checkForm.submit();
selected.className = 'misspelled updated';
for (var i = 0; i < this.spellcheck.words.length; i++) {
if (i != this.spellcheck.selectedIndex && this.spellcheck.words[i].misspelled && this.spellcheck.words[i].word.toLowerCase() == word.toLowerCase()) {
var w = document.getElementById('msw_' + i);
w.className = 'misspelled updated';
}
}
var new_words = [word];
this.spellcheck.corrections[this.spellcheck.words[this.spellcheck.selectedIndex].oword.toLowerCase()] = new_words.concat(this.spellcheck.corrections[this.spellcheck.words[this.spellcheck.selectedIndex].oword.toLowerCase()]);
}
}
dMenu.prototype.editWord = function () {
this.hide();
var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
if (!selected) return;
var word = selected.firstChild.nodeValue;
var edit_word = document.createElement('input');
edit_word.id = 'edit_word';
edit_word.name = this.spellcheck.selectedIndex;
edit_word.value = word;
edit_word.size = word.length + 5;
edit_word.className = 'text';
selected.replaceChild(edit_word, selected.firstChild);
var self = this;
registerEvent(edit_word, 'keyup', function (e) {
if (!e) e = window.e;
self.updateWord(e.keyCode);
});
// Prevent the click event from propagating to the msw_* span (which shows the suggestions)
registerEvent(edit_word, 'click', function (e) {
if (!e) e = window.e;
stopPropagation(e);
});
edit_word.focus();
}
dMenu.prototype.updateWord = function (code) {
var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
var edit_word = document.getElementById('edit_word');
if (!edit_word) return;
if (code == 27) // esc
selected.replaceChild(document.createTextNode(this.spellcheck.words[this.spellcheck.selectedIndex].word), selected.firstChild);
else if (code == 13) { // enter
if (this.spellcheck.words[this.spellcheck.selectedIndex].word != edit_word.value) {
this.spellcheck.words[this.spellcheck.selectedIndex].word = edit_word.value;
selected.className = 'misspelled updated';
}
selected.replaceChild(document.createTextNode(edit_word.value), selected.firstChild);
}
}
dMenu.prototype.replaceWord = function (new_word) {
this.hide();
if (typeof(this.spellcheck.selectedIndex) == 'undefined' || typeof(new_word) == 'undefined') return;
if (this.corrections.length == 0) return;
var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
if (!selected) return;
var format = this.getFormat(this.spellcheck.words[this.spellcheck.selectedIndex].word);
new_word = this.setFormat(format, new_word);
if (new_word) {
var old_word = this.spellcheck.words[this.spellcheck.selectedIndex].word;
this.spellcheck.words[this.spellcheck.selectedIndex].word = new_word;
selected.replaceChild(document.createTextNode(new_word), selected.firstChild);
selected.className = 'misspelled updated';
if (this.spellcheck.replaceAll) {
// Update all other words that match with added word
for (var i = 0; i < this.spellcheck.words.length; i++) {
if (i != this.spellcheck.selectedIndex && this.spellcheck.words[i].misspelled && this.spellcheck.words[i].word.toLowerCase() == old_word.toLowerCase()) {
var w = document.getElementById('msw_' + i);
var f = this.getFormat(this.spellcheck.words[i].word);
var n = this.setFormat(f, new_word);
this.spellcheck.words[i].word = n;
w.replaceChild(document.createTextNode(n), w.firstChild);
w.className = 'misspelled updated';
}
}
}
}
}
dMenu.prototype.deleteWord = function () {
this.hide();
var selected = document.getElementById('msw_' + this.spellcheck.selectedIndex);
if (!selected) return;
if (confirm("Are you sure you want to delete '" + this.spellcheck.words[this.spellcheck.selectedIndex].word + "'?")) {
this.spellcheck.words[this.spellcheck.selectedIndex].word = '';
selected.parentNode.removeChild(selected);
// Delete the next 'word' if it's a single space
var next_word = this.spellcheck.selectedIndex + 1;
if (this.spellcheck.words.length > next_word && this.spellcheck.words[next_word].word == ' ')
this.spellcheck.words[next_word].word = '';
}
}
dMenu.prototype.getFormat = function (word) {
if (word == '') return 3;
if (word == word.toUpperCase()) return 1;
else if (word.substr(0, 1) == word.substr(0, 1).toUpperCase()) return 2;
else return 3;
}
dMenu.prototype.setFormat = function (format, word) {
if (format == 1) return word.toUpperCase();
else if (format == 2) return word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase();
else return word.toLowerCase();
}
dMenu.prototype.setOption = function () {
if (this.spellcheck.replaceAll) {
this.spellcheck.replaceAll = false;
document.getElementById('replace_all').className = '';
}
else {
this.spellcheck.replaceAll = true;
document.getElementById('replace_all').className = 'replace_all';
}
}
dMenu.prototype.newWord = function () {
var spellcheck = this.spellcheck;
if (spellcheck.config.selectedIndex == -1 || spellcheck.config.selectedIndex > spellcheck.words.length || !spellcheck.words[spellcheck.selectedIndex].misspelled) return false;
var words = spellcheck.corrections[spellcheck.words[spellcheck.selectedIndex].oword.toLowerCase()];
var current = document.getElementById('msw_' + spellcheck.selectedIndex);
if (!current || words.length == 0) return false;
var word = spellcheck.words[spellcheck.selectedIndex].word;
if (word == '') return false;
for (var i = 0; i < words.length; i++)
if (words[i].toLowerCase() == word.toLowerCase())
return false;
return true;
}