*** empty log message ***
[m17n/m17n-lib-js.git] / mim.js
1 // -* coding: utf-8; -*
2
3 /* URL to scan JavaScript files defining MIM (M17N Input Method).  */
4 var MIM_url = "http://www.m17n.org/common/mim-js";
5
6 /* Boolean flag to tell if MIM is active or not.  */
7 var MIM_enabled = true;
8
9 var MIM_debug = false;
10
11 var MIM_current_im;
12
13 var MIM_list = new Array ();
14
15 function M17N_im (url, lang, name)
16 {
17   this.status = 0; /* 0: not-yet-loaded, 1:loading, 2:loaded, -1:error */
18   this.url = url;
19   this.lang = lang;
20   this.name = name;
21   this.keymap = false;
22
23   function add_keystring (map, keystring, str)
24   {
25     var i, c;
26     var newmap;
27     var intermediate_string = "";
28
29     for (i = 0; i < keystring.length; i++)
30       {
31         c = keystring.charAt (i);
32         if (c in map)
33           {
34             map = map[c];
35             if ('_target_text' in map)
36               intermediate_string = map['_target_text'];
37             else
38               intermediate_string += c;
39           }
40         else
41           {
42             newmap = new Array ();
43             map[c] = newmap;
44             map['_has_child'] = true;
45             map = newmap;
46             intermediate_string += c;
47             map['_target_text'] = intermediate_string;
48           }
49       }
50     map['_target_text'] = str;
51   }
52
53   function lookup_keyseq (keyseq, limit)
54   {
55     var map = this.keymap;
56
57     if (limit > keyseq.length)
58       limit = keyseq.length;
59     for (var i = 0; i < limit; i++)
60       {
61         var c = keyseq[i];
62         if (! (c in map))
63           return i;
64         map = map[c];
65       }
66     return map;
67   }
68
69   function load_map (mapdef)
70   {
71     this.keymap = new Array ();
72     for (var keystring in mapdef)
73       add_keystring (this.keymap, keystring, mapdef[keystring]);
74   }
75
76   this.load_mapdef = load_map;
77   this.lookup = lookup_keyseq;
78 }
79
80 function MIM_register (url, lang, name)
81 {
82   var im = new M17N_im (url, lang, name);
83   if (! (lang in MIM_list))
84     MIM_list[lang] = new Array ();
85   MIM_list[lang][name] = im;
86   return im;
87 }
88
89 function MIM_find (lang, name)
90 {
91   if (! (lang in MIM_list))
92     return false;
93   if (! (name in MIM_list[lang]))
94     return false;
95   return MIM_list[lang][name];
96 }
97
98
99 function MIM_load_async (im)
100 {
101   var filename = MIM_url + "/" + im.url;
102   var obj = (window.XMLHttpRequest ? new XMLHttpRequest ()
103              : window.ActiveXObject ? new ActiveXObject ("Msxml2.XMLHTTP")
104              : null);
105
106   if (! obj)
107     alert ("XMLHttpRequest not supported");
108   alert ("loading " + filename);
109   obj.open ('GET', filename, true);
110   im.status = 1; /* loading */
111   obj.onreadystatechange = function () { 
112     alert (obj.readyState + ":" + obj.statusText);
113     if (obj.readyState == 4)
114       {
115         try {
116           eval (obj.responseText);
117           im.status = 2; /* loaded */
118           alert ("loaded:"+obj.responseXML);
119         } catch (e) {
120           alert ("load error:" + e.message + " at " + e.lineNumber
121                  + " " + obj.responseText);
122           im.status = -1; /* load fail */
123         }
124       }
125   };
126   obj.send (null);
127   return im;
128 }
129
130 function MIM_load (im)
131 {
132   var filename = MIM_url + "/" + im.url;
133   var s = document.createElement ('script');
134
135   s.charset = 'UTF-8';
136   s.src = filename;
137   document.body.appendChild (s);
138   document.body.removeChild (s);
139   im.status = 2;
140   return im;
141 }
142
143 function MIM_add_event_listener (target, type, listener)
144 {
145   if (target.addEventListener)
146     target.addEventListener (type, listener, false);
147   else if (target.attachEvent)
148     target.attachEvent ('on' + type,
149                         function() { listener.call (target, window.event); });
150   else
151     target['on' + type]
152       = function(e) { listener.call (target, e || window.event); };
153 }
154
155 var MIM_key = new Array ();
156 MIM_key[0x09] = 'tab';
157 MIM_key[0x08] = 'backspace';
158 MIM_key[0x0D] = 'return';
159 MIM_key[0x1B] = 'escape';
160 MIM_key[0x20] = 'space';
161 MIM_key[0x21] = 'pageup';
162 MIM_key[0x22] = 'pagedown';
163 MIM_key[0x23] = 'end';
164 MIM_key[0x24] = 'home';
165 MIM_key[0x25] = 'left';
166 MIM_key[0x26] = 'up';
167 MIM_key[0x27] = 'right';
168 MIM_key[0x28] = 'down';
169 MIM_key[0x2D] = 'insert';
170 MIM_key[0x2E] = 'delete';
171 for (var i = 1; i <= 12; i++)
172   MIM_key[111 + i] = "f" + i;
173 MIM_key[0x90] = "numlock";
174 MIM_key[0xF0] = "capslock";
175
176 function MIM_decode_key (event)
177 {
178   var key = ((event.type == 'keydown' || event.keyCode) ? event.keyCode
179              : event.charCode ? event.charCode
180              : false);
181   if (! key)
182     return false;
183   if (event.type == 'keydown')
184     {
185       key = MIM_key[key];
186       if (! key)
187         return false;
188       if (event.shiftKey) key = "S-" + key ;
189       }
190   else
191     key = String.fromCharCode (key);
192   if (event.altKey) key = "A-" + key ;
193   if (event.ctrlKey) key = "C-" + key ;
194   return key;
195 }
196
197 function debug_print (event, ic)
198 {
199   if (! MIM_debug)
200     return;
201   var target = event.target;
202   var code = event.keyCode;
203   var ch = event.type == 'keydown' ? "-none-" : event.charCode;
204   var key = MIM_decode_key (event);
205
206   document.getElementById (event.type).innerHTML = "" + code + "/" + ch + " : " + key;
207   document.getElementById ('status').innerHTML = ic.im.status;
208   var keyseq = "";
209   for (var i = 0; i < ic.keyseq.length; i++)
210     keyseq += ic.keyseq[i];
211   document.getElementById ('keyseq').innerHTML = keyseq + ":" + ic.keyseq.length;
212   document.getElementById ('range').innerHTML = "" + ic.range[0] + "," + ic.range[1];;
213 }
214
215 function MIM_get_range (target, range)
216 {
217   if (target.selectionStart != null)
218     {
219       // for Mozilla
220       range[0] = target.selectionStart;
221       range[1] = target.selectionEnd;
222       return true;
223     }
224   if (document.selection != null)
225     {
226       target.focus();
227
228       var range = document.selection.createRange ();
229       var bookmark = range.getBookmark ();
230       var value = target.value;
231       var saved_value = value;
232       var marker = "_#_MARKER_#_";
233       while (value.indexOf (marker) != -1)
234         marker += "#_";
235       var parent = range.parentElement ();
236       if (parent == null || parent.type != "textarea")
237         {
238           range[0] = range[1] = 0;
239         }
240       else
241         {
242           range.text = marker + range.text + marker;
243           contents = this.element.value;
244           range[0] = contents.indexOf (marker);
245           contents = contents.replace(marker, "");
246           range[1] = contents.indexOf(marker);
247           target.value = originalContents;
248           range.moveToBookmark (bookmark);
249           range.select ();
250         }
251       return true;
252     }
253   return false;
254 }
255
256 function MIM_set_caret (target, pos)
257 {
258   if (target.selectionStart != null)
259     {
260       // Mozilla
261       target.focus ();
262       target.setSelectionRange (pos, pos);
263       return true;
264     }
265   if (target.createTextRange != null)
266     {
267       // IE
268       var range = target.createTextRange ();
269       range.move ('character', pos);
270       ranges.elect ();
271       return true;
272     }
273   // Unknown
274   target.focus ();
275   return false;
276 }
277
278 function MIM_ic (im, target)
279 {
280   this.im = im;
281   this.target = target;
282   this.key = false;
283   this.keyseq = new Array ();
284   this.range = new Array (-1, -1);
285   return this;
286 }
287
288 MIM_ic.prototype.reset = function ()
289 {
290   this.key = false;
291   while (this.keyseq.length > 0)
292     this.keyseq.pop ();
293   this.range[0] = this.range[1] = -1;
294 }
295
296 MIM_ic.prototype.check_caret = function ()
297 {
298   var from = this.range[0];
299   var to = this.range[1];
300
301   MIM_get_range (this.target, this.range);
302   if (from >= 0)
303     {
304       if (this.range[0] != this.range[1] || to != this.range[0])
305         this.reset ();
306       else
307         this.range[0] = from;
308     }
309 }
310
311 function MIM_insert (ic, insert)
312 {
313   var text = ic.target.value;
314   ic.target.value = (text.substring (0, ic.range[0])
315                      + insert
316                      + text.substring (ic.range[1]));
317   ic.range[1] = ic.range[0] + insert.length;
318   MIM_set_caret (ic.target, ic.range[1]);
319 }
320
321 function keyseq_string (keyseq)
322 {
323   var str = "";
324   for (var i = 0; i < keyseq.length; i++)
325     str += keyseq[i];
326   return str;
327 }
328
329 function MIM_handle_keyseq (event, ic)
330 {
331   var map = ic.im.lookup (ic.keyseq, 1000);
332   if (map instanceof Array)
333     {
334       MIM_insert (ic, map['_target_text']);
335       if (! ('_has_child' in map))
336         ic.reset ();
337       event.preventDefault ();
338       //document.getElementById ('text').value
339       //= keyseq_string (ic.keyseq) + " handled";
340     }
341   else if (map > 0)
342     {
343       MIM_insert (ic, ic.im.lookup (ic.keyseq, map)['_target_text']);
344       while (map > 0)
345         {
346           ic.keyseq.shift ();
347           map--;
348         }
349       ic.range[0] = ic.range[1];
350       if (ic.keyseq.length > 0)
351         MIM_handle_keyseq (event, ic);
352     }
353   else
354     {
355       ic.reset ();
356       //document.getElementById ('text').value
357       //= keyseq_string (ic.keyseq) + " unhandled";
358     }
359 }
360
361 function MIM_reset_ic (event)
362 {
363   var ic = event.target.mim_ic;
364   if (ic)
365     ic.reset ();
366 }
367
368 function MIM_keydown (event)
369 {
370   if (! (event.target.type == "text" || event.target.type == "textarea"))
371     return;
372
373   var ic = event.target.mim_ic;
374   if (! ic || ic.im != MIM_current_im)
375     {
376       ic = new MIM_ic (MIM_current_im, event.target);
377       event.target.mim_ic = ic;
378     }
379   MIM_add_event_listener (event.target, 'blur', MIM_reset_ic);
380   debug_print (event, ic);
381   if (ic.im.status < 0)
382     return;
383   ic.check_caret ();
384   ic.key = MIM_decode_key (event);
385 }
386
387 function MIM_keypress (event)
388 {
389   if (! (event.target.type == "text" || event.target.type == "textarea"))
390     return;
391
392   var ic = event.target.mim_ic;
393   var i;
394
395   debug_print (event, ic);
396   if (ic.im.status < 0)
397     return;
398   if (! ic.key)
399     ic.key = MIM_decode_key (event);
400   if (! ic.key)
401     {
402       ic.reset ();
403       return;
404     }
405   ic.keyseq.push (ic.key);
406   if (ic.im.status == 1) // Still loading.
407     return;
408   MIM_handle_keyseq (event, ic);
409   return;
410 }
411
412 function MIM_select_im (event)
413 {
414   var target = event.target.parentNode;
415   while (target.tagName != "SELECT")
416     target = target.parentNode;
417   var idx = 0;
418   var im = false;
419   for (var lang in MIM_list)
420     for (var name in MIM_list[lang])
421       if (idx++ == target.selectedIndex)
422         {
423           im = MIM_list[lang][name];
424           break;
425         }
426   document.getElementsByTagName ('body')[0].removeChild (target);
427   target.target.focus ();
428   if (im && im != MIM_current_im)
429     MIM_current_im = MIM_load (im);
430 }
431
432 function MIM_destroy_menu (event)
433 {
434   if (event.target.tagName == "SELECT")
435     document.getElementsByTagName ('body')[0].removeChild (event.target);
436 }
437
438 function MIM_select_menu (event)
439 {
440   var target = event.target;
441
442   if (! ((target.type == "text" || target.type == "textarea")
443          && event.which == 1 && event.ctrlKey))
444     return;
445
446   var sel = document.createElement ('select');
447   sel.onclick = MIM_select_im;
448   sel.onmouseout = MIM_destroy_menu;
449   sel.style.position='absolute';
450   sel.style.left = (event.clientX - 10) + "px";
451   sel.style.top = (event.clientY - 10) + "px";
452   sel.target = target;
453   var idx = 0;
454   for (var lang in MIM_list)
455     for (var name in MIM_list[lang])
456       {
457         var option = document.createElement ('option');
458         var imname = lang + "-" + name;
459         option.appendChild (document.createTextNode (imname));
460         option.value = imname;
461         sel.appendChild (option);
462         if (MIM_list[lang][name] == MIM_current_im)
463           sel.selectedIndex = idx;
464         idx++;
465       }
466   sel.size = idx;
467   document.getElementsByTagName ('body')[0].appendChild (sel);
468 }
469
470 MIM_current_im = MIM_register ('latn-post.js', 'latin', 'post');
471 MIM_register ('th-kesmanee.js', 'th', 'kesmanee');
472
473 function MIM_init ()
474 {
475   MIM_add_event_listener (window, 'keydown', MIM_keydown);
476   MIM_add_event_listener (window, 'keypress', MIM_keypress);
477   MIM_add_event_listener (window, 'mousedown', MIM_select_menu);
478   if (window.location == 'http://localhost/mim/index.html')
479     MIM_url = 'http://localhost/mim';
480   MIM_load (MIM_current_im);
481 }
482
483 function MIM_init_debug ()
484 {
485   MIM_debug = true;
486   MIM_init ();
487 }