/* * ================================================================= * 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(/').replace(/ /g, '  '); else content += '' + this.words[i].word + ''; } if (this.config.editorObj) cmntent = content.replace(/' + content + ''; 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', '', '
'], ['add', 'wordAdd', spellcheck.lang.add], ['edit', 'wordEdit', spellcheck.lang.edit], ['delete', 'wordEdit', spellcheck.lang['delete']], ['option', 'wordOption', '' + spellcheck.lang.replaceAll + '']]; 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; }