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 &amp; protection
377      data = data.replace(/&amp;/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