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