1/** 2 * DokuWiki Spellcheck AJAX clientside script 3 * 4 * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 5 * @author Andreas Gohr <andi@splitbrain.org> 6 */ 7 8/** 9 * Licence info: This spellchecker is inspired by code by Garrison Locke available 10 * at http://www.broken-notebook.com/spell_checker/index.php (licensed under the Terms 11 * of an BSD license). The code in this file was nearly completly rewritten for DokuWiki 12 * and is licensed under GPL version 2 (See COPYING for details). 13 * 14 * Original Copyright notice follows: 15 * 16 * Copyright (c) 2005, Garrison Locke 17 * All rights reserved. 18 * 19 * Redistribution and use in source and binary forms, with or without 20 * modification, are permitted provided that the following conditions are met: 21 * 22 * * Redistributions of source code must retain the above copyright notice, 23 * this list of conditions and the following disclaimer. 24 * * Redistributions in binary form must reproduce the above copyright notice, 25 * this list of conditions and the following disclaimer in the documentation 26 * and/or other materials provided with the distribution. 27 * * Neither the name of the http://www.broken-notebook.com nor the names of its 28 * contributors may be used to endorse or promote products derived from this 29 * software without specific prior written permission. 30 * 31 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 32 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 33 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. 34 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, 35 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT 36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR 37 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, 38 * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 39 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY 40 * OF SUCH DAMAGE. 41 */ 42 43/* 44 * Uses some general functions defined elsewhere. Here is a list: 45 * 46 * Defined in script.js: 47 * 48 * findPosX() 49 * findPosY() 50 * 51 * Defined in events.js: 52 * 53 * addEvent() 54 * 55 * Defined in edit.js: 56 * 57 * createToolButton() 58 */ 59 60/** 61 * quotes single quotes 62 * 63 * @author Andreas Gohr <andi@splitbrain.org> 64 */ 65function qquote(str){ 66 return str.split('\'').join('\\\''); 67} 68 69/** 70 * AJAX Spellchecker Class 71 * 72 * Note to some function use a hardcoded instance named ajax_spell to make 73 * references to object members. Used Object-IDs are hardcoded in the init() 74 * method. 75 * 76 * @author Andreas Gohr <andi@splitbrain.org> 77 * @author Garrison Locke <http://www.broken-notebook.com> 78 */ 79function ajax_spell_class(){ 80 this.inited = false; 81 this.utf8ok = 1; 82 this.handler = DOKU_BASE+'lib/plugins/spellcheck/spellcheck.php'; 83 // to hold the page objects (initialized with init()) 84 this.textboxObj = null; 85 this.showboxObj = null; 86 this.suggestObj = null; 87 this.editbarObj = null; 88 this.buttonObj = null; 89 this.imageObj = null; 90 91 // hold translations 92 this.txtStart = 'Check Spelling'; 93 this.txtStop = 'Resume Editing'; 94 this.txtRun = 'Checking...'; 95 this.txtNoErr = 'No Mistakes'; 96 this.txtNoSug = 'No Suggestions'; 97 this.txtChange= 'Change'; 98 99 this.timer = null; 100 101 /** 102 * Initializes everything 103 * 104 * Call after the page was setup. Hardcoded element IDs here. 105 * 106 * @author Andreas Gohr <andi@splitbrain.org> 107 */ 108 this.init = function(txtStart,txtStop,txtRun,txtNoErr,txtNoSug,txtChange){ 109 // don't run twice 110 if (this.inited){ return; } 111 this.inited = true; 112 113 // check for AJAX availability 114 var ajax = new sack(this.handler); 115 if(ajax.failed){ return; } 116 117 // get Elements 118 this.textboxObj = document.getElementById('wiki__text'); 119 this.editbarObj = document.getElementById('wiki__editbar'); 120 this.showboxObj = document.getElementById('spell__result'); 121 this.suggestObj = document.getElementById('spell__suggest'); 122 123 124 // set wordwrap style with browser propritary attributes 125 if(is_gecko){ 126 this.showboxObj.style.whiteSpace = '-moz-pre-wrap'; // Mozilla, since 1999 127 }else if(is_opera_preseven){ 128 this.showboxObj.style.whiteSpace = '-pre-wrap'; // Opera 4-6 129 }else if(is_opera_seven){ 130 this.showboxObj.style.whiteSpace = '-o-pre-wrap'; // Opera 7 131 }else{ 132 this.showboxObj.style['word-wrap'] = 'break-word'; //Internet Explorer 5.5+ 133 } 134 // Which browser supports this? 135 // this.showboxObj.style.whiteSpace = 'pre-wrap'; // css-3 136 137 138 // set Translation Strings 139 this.txtStart = txtStart; 140 this.txtStop = txtStop; 141 this.txtRun = txtRun; 142 this.txtNoErr = txtNoErr; 143 this.txtNoSug = txtNoSug; 144 this.txtChange= txtChange; 145 146 // create ToolBar Button with ID and add it to the toolbar with null action 147 var toolbarObj = document.getElementById('tool__bar'); 148 this.buttonObj = createToolButton(DOKU_BASE+'lib/plugins/spellcheck/images/spellcheck.png',txtStart,'k','spell__check'); 149 this.buttonObj.onclick = function(){return false;}; 150 toolbarObj.appendChild(this.buttonObj); 151 this.imageObj = document.getElementById('spell__check_ico'); 152 153 // start UTF-8 compliance test - send an UTF-8 char and see what comes back 154 ajax.AjaxFailedAlert = ''; 155 ajax.encodeURIString = false; 156 ajax.onCompletion = this.initReady; 157 ajax.runAJAX('call=utf8test&data='+encodeURIComponent('ü')); 158 159 // second part of initialisation is in initReady() function 160 }; 161 162 /** 163 * Eventhandler for click objects anywhere on the document 164 * 165 * Disables the suggestion box 166 * 167 * @author Andreas Gohr <andi@splitbrain.org> 168 * @author Garrison Locke <http://www.broken-notebook.com> 169 */ 170 this.docClick = function(e){ 171 // what was clicked? 172 try{ 173 target = window.event.srcElement; 174 }catch(ex){ 175 target = e.target; 176 } 177 178 if (target.id != ajax_spell.suggestObj.id){ 179 ajax_spell.suggestObj.style.display = "none"; 180 } 181 }; 182 183 /** 184 * Changes the Spellchecker link according to the given mode 185 * 186 * @author Andreas Gohr <andi@splitbrain.org> 187 */ 188 this.setState = function(state){ 189 switch (state){ 190 case 'stop': 191 ajax_spell.buttonObj.onclick = function(){ ajax_spell.resume(); return false; }; 192 ajax_spell.buttonObj.title = ajax_spell.txtStop; 193 ajax_spell.buttonObj.accessKey = ''; 194 ajax_spell.imageObj.src = DOKU_BASE+'lib/plugins/spellcheck/images/spellstop.png'; 195 break; 196 case 'noerr': 197 ajax_spell.buttonObj.onclick = function(){ajax_spell.setState('start'); return false; }; 198 ajax_spell.buttonObj.title = ajax_spell.txtNoErr; 199 ajax_spell.buttonObj.accessKey = ''; 200 ajax_spell.imageObj.src = DOKU_BASE+'lib/plugins/spellcheck/images/spellnoerr.png'; 201 break; 202 case 'run': 203 ajax_spell.buttonObj.onclick = function(){return false;}; 204 ajax_spell.buttonObj.title = ajax_spell.txtRun; 205 ajax_spell.buttonObj.accessKey = ''; 206 ajax_spell.imageObj.src = DOKU_BASE+'lib/plugins/spellcheck/images/spellwait.gif'; 207 break; 208 default: 209 ajax_spell.buttonObj.onclick = function(){ ajax_spell.run(); return false; }; 210 ajax_spell.buttonObj.title = ajax_spell.txtStart+' [ALT-K]'; 211 ajax_spell.buttonObj.accessKey = 'k'; 212 ajax_spell.imageObj.src = DOKU_BASE+'lib/plugins/spellcheck/images/spellcheck.png'; 213 break; 214 } 215 }; 216 217 /** 218 * Replaces a word identified by id with its correction given in word 219 * 220 * @author Garrison Locke <http://www.broken-notebook.com> 221 */ 222 this.correct = function (id, word){ 223 var obj = document.getElementById('spell__error'+id); 224 obj.innerHTML = decodeURIComponent(word); 225 obj.style.color = "#005500"; 226 this.suggestObj.style.display = "none"; 227 }; 228 229 /** 230 * Opens a prompt to let the user change the word her self 231 * 232 * @author Andreas Gohr <andi@splitbrain.org> 233 */ 234 this.ask = function(id){ 235 var word = document.getElementById('spell__error'+id).innerHTML; 236 word = prompt(this.txtChange,word); 237 if(word){ 238 this.correct(id,encodeURIComponent(word)); 239 } 240 }; 241 242 /** 243 * Displays the suggestions for a misspelled word 244 * 245 * @author Andreas Gohr <andi@splitbrain.org> 246 * @author Garrison Locke <http://www.broken-notebook.com> 247 */ 248 this.suggest = function(){ 249 var args = this.suggest.arguments; 250 if(!args[0]){ return; } 251 var id = args[0]; 252 253 // set position of the popup 254 this.suggestObj.style.display = "none"; 255 var x = findPosX('spell__error'+id); 256 var y = findPosY('spell__error'+id); 257 258 // handle scrolling 259 var scrollPos; 260 if(is_opera){ 261 scrollPos = 0; //FIXME how to do this without browser sniffing? 262 }else{ 263 scrollPos = this.showboxObj.scrollTop; 264 } 265 266 this.suggestObj.style.left = x+'px'; 267 this.suggestObj.style.top = (y+16-scrollPos)+'px'; 268 269 // handle suggestions 270 var text = ''; 271 if(args.length == 1){ 272 text += this.txtNoSug+'<br />'; 273 }else{ 274 for(var i=1; i<args.length; i++){ 275 text += '<a href="javascript:ajax_spell.correct('+id+',\''+ 276 qquote(args[i])+'\')">'; 277 text += args[i]; 278 text += '</a><br />'; 279 } 280 } 281 // add option for manual edit 282 text += '<a href="javascript:ajax_spell.ask('+id+')">'; 283 text += '['+this.txtChange+']'; 284 text += '</a><br />'; 285 286 this.suggestObj.innerHTML = text; 287 this.suggestObj.style.display = "block"; 288 }; 289 290 // --- Callbacks --- 291 292 /** 293 * Callback. Called after the object was initialized and UTF-8 tested 294 * Inside the callback 'this' is the SACK object!! 295 * 296 * @author Andreas Gohr <andi@splitbrain.org> 297 */ 298 this.initReady = function(){ 299 var data = this.response; 300 301 //test for UTF-8 compliance (will fail for konqueror) 302 if(data != 'ü'){ 303 ajax_spell.utf8ok = 0; 304 } 305 306 // register click event 307 addEvent(document,'click',ajax_spell.docClick); 308 309 // register focus event 310 addEvent(ajax_spell.textboxObj,'focus',ajax_spell.setState); 311 312 // get started 313 ajax_spell.setState('start'); 314 }; 315 316 /** 317 * Callback. Called after finishing spellcheck. 318 * Inside the callback 'this' is the SACK object!! 319 * 320 * @author Andreas Gohr <andi@splitbrain.org> 321 */ 322 this.start = function(){ 323 if(ajax_spell.timer !== null){ 324 window.clearTimeout(ajax_spell.timer); 325 ajax_spell.timer = null; 326 }else{ 327 // there is no timer set, we timed out already 328 return; 329 } 330 331 var data = this.response; 332 var error = data.charAt(0); 333 data = data.substring(1); 334 if(error == '1'){ 335 ajax_spell.setState('stop'); 336 337 // convert numeric entities back to UTF-8 if needed 338 if(!ajax_spell.utf8ok){ 339 data = data.replace(/&#(\d+);/g, 340 function(whole,match1) { 341 return String.fromCharCode(+match1); 342 }); 343 } 344 345 // replace textbox through div 346 ajax_spell.showboxObj.innerHTML = data; 347 ajax_spell.showboxObj.style.width = ajax_spell.textboxObj.style.width; 348 ajax_spell.showboxObj.style.height = ajax_spell.textboxObj.style.height; 349 ajax_spell.textboxObj.style.display = 'none'; 350 ajax_spell.showboxObj.style.display = 'block'; 351 }else{ 352 if(error == '2'){ 353 alert(data); 354 } 355 ajax_spell.textboxObj.disabled = false; 356 ajax_spell.editbarObj.style.visibility = 'visible'; 357 ajax_spell.setState('noerr'); 358 } 359 }; 360 361 /** 362 * Callback. Gets called by resume() - switches back to edit mode 363 * Inside the callback 'this' is the SACK object!! 364 * 365 * @author Andreas Gohr <andi@splitbrain.org> 366 */ 367 this.stop = function(){ 368 var data = this.response; 369 370 // convert numeric entities back to UTF-8 if needed 371 if(!ajax_spell.utf8ok){ 372 data = data.replace(/&#(\d+);/g, 373 function(whole,match1) { 374 return String.fromCharCode(+match1); 375 }); 376 // now remove & protection 377 data = data.replace(/&/g,'&'); 378 } 379 380 // replace div with textbox again 381 ajax_spell.textboxObj.value = data; 382 ajax_spell.textboxObj.disabled = false; 383 ajax_spell.showboxObj.style.display = 'none'; 384 ajax_spell.textboxObj.style.display = 'block'; 385 ajax_spell.editbarObj.style.visibility = 'visible'; 386 ajax_spell.showboxObj.innerHTML = ''; 387 ajax_spell.setState('start'); 388 }; 389 390 /** 391 * Calback for the timeout handling 392 * 393 * Will be called when the aspell backend didn't return 394 */ 395 this.timedOut = function(){ 396 if(ajax_spell.timer !== null){ 397 window.clearTimeout(ajax_spell.timer); 398 ajax_spell.timer = null; 399 400 ajax_spell.textboxObj.disabled = false; 401 ajax_spell.showboxObj.style.display = 'none'; 402 ajax_spell.textboxObj.style.display = 'block'; 403 ajax_spell.editbarObj.style.visibility = 'visible'; 404 ajax_spell.showboxObj.innerHTML = ''; 405 ajax_spell.setState('start'); 406 407 window.alert('Error: The spell checker did not respond'); 408 } 409 }; 410 411 // --- Callers --- 412 413 /** 414 * Starts the spellchecking by sending an AJAX request 415 * 416 * @author Andreas Gohr <andi@splitbrain.org> 417 */ 418 this.run = function(){ 419 ajax_spell.setState('run'); 420 ajax_spell.textboxObj.disabled = true; 421 ajax_spell.editbarObj.style.visibility = 'hidden'; 422 var ajax = new sack(ajax_spell.handler); 423 ajax.AjaxFailedAlert = ''; 424 ajax.encodeURIString = false; 425 ajax.onCompletion = this.start; 426 ajax.runAJAX('call=check&utf8='+ajax_spell.utf8ok+ 427 '&data='+encodeURIComponent(ajax_spell.textboxObj.value)); 428 429 // abort after 13 seconds 430 this.timer = window.setTimeout(ajax_spell.timedOut,13000); 431 }; 432 433 /** 434 * Rewrites the HTML back to text again using an AJAX request 435 * 436 * @author Andreas Gohr <andi@splitbrain.org> 437 */ 438 this.resume = function(){ 439 ajax_spell.setState('run'); 440 var text = ajax_spell.showboxObj.innerHTML; 441 if(text !== ''){ 442 var ajax = new sack(ajax_spell.handler); 443 ajax.AjaxFailedAlert = ''; 444 ajax.encodeURIString = false; 445 ajax.onCompletion = ajax_spell.stop; 446 ajax.runAJAX('call=resume&utf8='+ajax_spell.utf8ok+ 447 '&data='+encodeURIComponent(text)); 448 } 449 }; 450 451} 452 453// create the global object 454var ajax_spell = null; 455 456addInitEvent(function(){ 457 if(toolbar && toolbar[0]){ 458 ajax_spell = new ajax_spell_class(); 459 460 ajax_spell.init(LANG['plugins']['spellcheck']['start'], 461 LANG['plugins']['spellcheck']['stop'], 462 LANG['plugins']['spellcheck']['wait'], 463 LANG['plugins']['spellcheck']['noerr'], 464 LANG['plugins']['spellcheck']['nosug'], 465 LANG['plugins']['spellcheck']['change']); 466 } 467}); 468 469//Setup VIM: ex: et ts=2 enc=utf-8 : 470