*** empty log message ***
[m17n/m17n-lib-js.git] / mim.js
1 // -* coding: utf-8; -*
2
3 var MIM = {};
4
5 // URL of the input method server.
6 MIM.server = "http://www.m17n.org/common/mim-js",
7 // Boolean flag to tell if MIM is active or not.
8 MIM.enabled = true;
9 // Boolean flag to tell if MIM is running in debug mode or not.
10 MIM.debug = false;
11 // List of registered input methods.
12 MIM.list = new Array ();
13 // Currently selected input method.
14 MIM.current_im = false;
15
16 MIM.im = function (lang, name, filename)
17 {
18   this.status = 0; /* 0: not-yet-loaded, 1:loading, 2:loaded, -1:error */
19   this.url = MIM.server + "/" + filename;
20   this.lang = lang;
21   this.name = name;
22   this.keymap = false;
23   this.body = null;
24
25   function add_keystring (map, keystring, str)
26   {
27     var i, c;
28     var newmap;
29     var intermediate_string = "";
30
31     for (i = 0; i < keystring.length; i++)
32     {
33       c = keystring.charAt (i);
34       if (c in map)
35         {
36           map = map[c];
37           if ('_target_text' in map)
38             intermediate_string = map['_target_text'];
39           else
40             intermediate_string += c;
41         }
42       else
43         {
44           newmap = new Array ();
45           map[c] = newmap;
46           map['_has_child'] = true;
47           map = newmap;
48           intermediate_string += c;
49           map['_target_text'] = intermediate_string;
50         }
51     }
52     map['_target_text'] = str;
53   }
54
55   this.lookup = function (keyseq, limit)
56   {
57     var map = this.keymap;
58
59     if (limit > keyseq.length)
60       limit = keyseq.length;
61     for (var i = 0; i < limit; i++)
62       {
63         var c = keyseq[i];
64         if (! (c in map))
65           return i;
66         map = map[c];
67       }
68     return map;
69   }
70
71   this.load_map = function (mapdef)
72   {
73     this.keymap = new Array ();
74     for (var keystring in mapdef)
75       add_keystring (this.keymap, keystring, mapdef[keystring]);
76   }
77
78   this.load_map_node = function ()
79   {
80     this.keymap = new Array ();
81     var maps = this.body.getElementsByTagName ('map');
82     var map = maps[0];
83     var rules = map.getElementsByTagName ('rule');
84     for (var i = 0; i < rules.length; i++)
85       {
86         var rule = rules[i];
87         var keyseq_elm = MIM.first_element (rule);
88         var keystring = keyseq_elm.attributes[0].nodeValue;
89         var insert_elm = MIM.next_element (rule);
90         var str = insert_elm.attributes[0].nodeValue;
91         add_keystring (this.keymap, keystring, str);
92       }
93   }
94 };
95
96 MIM.error_return = function (msg, ret)
97 {
98   alert (msg);
99   return ret;
100 }
101
102 MIM.first_element = function (node)
103 {
104   node.mim_index = 0;
105   return MIM.next_element (node);
106 }
107
108 MIM.next_element = function (node)
109 {
110   var element = node.childNodes[node.mim_index++];
111   while (element && element.nodeType != 1)
112     element = node.childNodes[node.mim_index++];
113   return element;
114 }
115
116 MIM.check_map = function (im, map)
117 {
118   var rules = map.getElementsByTagName ('rule');
119   var len = rules.length;
120   
121   for (var i = 0; i < len; i++)
122     {
123       var rule = rules[i];
124       var elm = MIM.first_element (rule);
125
126       if (!elm || elm.nodeName != 'keyseq')
127         return false;
128       while ((elm = MIM.next_element (rule)))
129         if (elm.nodeName != 'insert')
130           return false;
131     }
132   return true;
133 }
134
135 MIM.check_state = function (im, state)
136 {
137   var branches = state.getElementsByTagName ('branch');
138   var len = branches.length;
139
140   for (var i = 0; i < len; i++)
141     {
142       var branch = branches[i];
143       var elm = MIM.first_element (branch);
144
145       if (elm)
146         return false;
147     }
148   return true;
149 }
150
151 MIM.parse = function (im)
152 {
153   var maps = im.body.getElementsByTagName ('map');
154   var states = im.body.getElementsByTagName ('state');
155   var str = "";
156   var i;
157
158   if (! maps || maps.length == 0)
159     MIM.error_return ('No map', false);
160   if (! states)
161     MIM.error_return ('No state', false);
162   for (i = 0; i < maps.length; i++)
163     if (! MIM.check_map (im, maps[i]))
164       MIM.error_return ('Unsupported directive in map', false);
165   for (var i = 0; i < states.length; i++)
166     if (! MIM.check_state (im, states[i]))
167       MIM.error_return ('Unsupported directive in state', false);
168   im.load_map_node ();
169   return true;
170 }
171
172 MIM.register = function (lang, name, url)
173 {
174   var im = new MIM.im (lang, name, url);
175   if (! (lang in MIM.list))
176     MIM.list[lang] = new Array ();
177   MIM.list[lang][name] = im;
178   return im;
179 };
180
181 MIM.find = function (lang, name)
182 {
183   if (! (lang in MIM.list))
184     return false;
185   if (! (name in MIM.list[lang]))
186     return false;
187   return MIM.list[lang][name];
188 };
189
190 MIM.load_async = function (im)
191 {
192   var obj = (window.XMLHttpRequest ? new XMLHttpRequest ()
193              : window.ActiveXObject ? new ActiveXObject ("Msxml2.XMLHTTP")
194              : null);
195
196   if (! obj)
197     alert ("XMLHttpRequest not supported");
198   obj.open ('GET', im.url, true);
199   im.status = 1; /* loading */
200   obj.onreadystatechange = function () { 
201     if (obj.readyState == 4)
202       {
203         try {
204           eval (obj.responseText);
205           im.status = 2; /* loaded */
206         } catch (e) {
207           alert ("load error:" + e.message + " at " + e.lineNumber
208                  + " " + obj.responseText);
209           im.status = -1; /* load fail */
210         }
211       }
212   };
213   obj.send (null);
214   return im;
215 };
216
217 MIM.load_sync = function (im)
218 {
219   var obj = (window.XMLHttpRequest ? new XMLHttpRequest ()
220              : window.ActiveXObject ? new ActiveXObject ("Msxml2.XMLHTTP")
221              : null);
222
223   if (! obj)
224     alert ("XMLHttpRequest not supported");
225   if (true)
226     {
227       obj.open ('GET', im.url, false);
228       obj.send ("");  
229       eval (obj.responseText);
230       if (! im.keymap)
231         alert ("obj.responseText");
232       return im;
233     }
234   else
235     {
236       obj.open ('GET', 'latn-post.mimx', false);
237       obj.overrideMimeType ('text/xml');
238       obj.send ("");
239       im.body = obj.responseXML;
240       document.AnXml = im.body;
241       if (MIM.parse (im))
242         return im;
243       alert (im.parse_error);
244     }
245   return false;
246 };
247
248 MIM.load = function (im)
249 {
250   var s = document.createElement ('script');
251
252   s.charset = 'UTF-8';
253   s.src = im.url;
254   document.body.appendChild (s);
255   document.body.removeChild (s);
256   if (! im.keymap)
257     alert ("load fail");
258   im.status = 2;
259   return im;
260 };
261
262 MIM.add_event_listener
263   = (window.addEventListener
264      ? function (target, type, listener) {
265        target.addEventListener (type, listener, false);
266      }
267      : window.attachEvent
268      ? function (target, type, listener) {
269        target.attachEvent ('on' + type,
270                            function() {
271                              listener.call (target, window.event);
272                            });
273      }
274      : function (target, type, listener) {
275        target['on' + type]
276          = function (e) { listener.call (target, e || window.event); };
277      });
278
279 (function () {
280   var keys = new Array ();
281   keys[0x09] = 'tab';
282   keys[0x08] = 'backspace';
283   keys[0x0D] = 'return';
284   keys[0x1B] = 'escape';
285   keys[0x20] = 'space';
286   keys[0x21] = 'pageup';
287   keys[0x22] = 'pagedown';
288   keys[0x23] = 'end';
289   keys[0x24] = 'home';
290   keys[0x25] = 'left';
291   keys[0x26] = 'up';
292   keys[0x27] = 'right';
293   keys[0x28] = 'down';
294   keys[0x2D] = 'insert';
295   keys[0x2E] = 'delete';
296   for (var i = 1; i <= 12; i++)
297     keys[111 + i] = "f" + i;
298   keys[0x90] = "numlock";
299   keys[0xF0] = "capslock";
300   MIM.special_key = keys;
301 }) ();
302
303 MIM.decode_key = function (event)
304 {
305   var key = ((event.type == 'keydown' || event.keyCode) ? event.keyCode
306              : event.charCode ? event.charCode
307              : false);
308   if (! key)
309     return false;
310   if (event.type == 'keydown')
311     {
312       key = MIM.special_key[key];
313       if (! key)
314         return false;
315       if (event.shiftKey) key = "S-" + key ;
316       }
317   else
318     key = String.fromCharCode (key);
319   if (event.altKey) key = "A-" + key ;
320   if (event.ctrlKey) key = "C-" + key ;
321   return key;
322 };
323
324 MIM.debug_print = function (event, ic)
325 {
326   if (! MIM.debug)
327     return;
328   if (! MIM.debug_nodes)
329     {
330       MIM.debug_nodes = new Array ();
331       MIM.debug_nodes['keydown'] = document.getElementById ('keydown');
332       MIM.debug_nodes['keypress'] = document.getElementById ('keypress');
333       MIM.debug_nodes['status0'] = document.getElementById ('status0');
334       MIM.debug_nodes['status1'] = document.getElementById ('status1');
335       MIM.debug_nodes['keyseq0'] = document.getElementById ('keyseq0');
336       MIM.debug_nodes['keyseq1'] = document.getElementById ('keyseq1');
337       MIM.debug_nodes['range0'] = document.getElementById ('range0');
338       MIM.debug_nodes['range1'] = document.getElementById ('range1');
339     }
340   var target = event.target;
341   var code = event.keyCode;
342   var ch = event.type == 'keydown' ? 0 : event.charCode;
343   var key = MIM.decode_key (event);
344   var keyseq = "";
345   var index;
346
347   MIM.debug_nodes[event.type].innerHTML = "" + code + "/" + ch + " : " + key;
348   index = (event.type == 'keydown' ? '0' : '1');
349   MIM.debug_nodes['status' + index].innerHTML = ic.im.status;
350   for (var i = 0; i < ic.keyseq.length; i++)
351     keyseq += ic.keyseq[i];
352   MIM.debug_nodes['keyseq' + index].innerHTML
353     = keyseq + ":" + ic.keyseq.length;
354   MIM.debug_nodes['range' + index].innerHTML
355     = "" + ic.range[0] + ":" + ic.range[1];
356 };
357
358 MIM.get_range = function (target, range)
359 {
360   if (target.selectionStart != null) // for Mozilla
361     {
362       range[0] = target.selectionStart;
363       range[1] = target.selectionEnd;
364     }
365   else                          // for IE
366     {
367       var r = document.selection.createRange ();
368       var rr = r.duplicate ();
369
370       rr.moveToElementText (target);
371       rr.setEndPoint ('EndToEnd', range);
372       range[0] = rr.text.length - r.text.length;
373       range[1] = rr.text.length;
374     }
375 }
376
377 MIM.set_caret = function (target, pos)
378 {
379   if (target.selectionStart != null) // Mozilla
380     {
381       target.focus ();
382       target.setSelectionRange (pos, pos);
383     }
384   else                          // IE
385     {
386       var range = target.createTextRange ();
387       range.move ('character', pos);
388       range.select ();
389     }
390 };
391
392 MIM.ic = function (im, target)
393 {
394   this.im = im;
395   this.target = target;
396   this.key = false;
397   this.keyseq = new Array ();
398   this.range = new Array (0, 0);
399   return this;
400 };
401
402 MIM.ic.prototype.reset = function ()
403 {
404   this.key = false;
405   while (this.keyseq.length > 0)
406     this.keyseq.pop ();
407 };
408
409 MIM.ic.prototype.check_range = function ()
410 {
411   var from = this.range[0], to = this.range[1];
412
413   MIM.get_range (this.target, this.range);
414   if (this.range[0] != this.range[1] || to != this.range[1])
415     this.reset ();
416   else
417     this.range[0] = from;
418 };
419
420 MIM.insert = function (ic, insert)
421 {
422   var text = ic.target.value;
423   ic.target.value = (text.substring (0, ic.range[0])
424                      + insert
425                      + text.substring (ic.range[1]));
426   ic.range[1] = ic.range[0] + insert.length;
427   MIM.set_caret (ic.target, ic.range[1]);
428 };
429
430 function keyseq_string (keyseq)
431 {
432   var str = "";
433   for (var i = 0; i < keyseq.length; i++)
434     str += keyseq[i];
435   return str;
436 }
437
438 MIM.handle_keyseq = function (event, ic)
439 {
440   var map = ic.im.lookup (ic.keyseq, 1000);
441   if (map instanceof Array)
442     {
443       MIM.insert (ic, map['_target_text']);
444       if (! ('_has_child' in map))
445         {
446           ic.reset ();
447           ic.range[0] = ic.range[1];
448         }
449       event.preventDefault ();
450       //document.getElementById ('text').value
451       //= keyseq_string (ic.keyseq) + " handled";
452     }
453   else if (map > 0)
454     {
455       MIM.insert (ic, ic.im.lookup (ic.keyseq, map)['_target_text']);
456       while (map > 0)
457         {
458           ic.keyseq.shift ();
459           map--;
460         }
461       ic.range[0] = ic.range[1];
462       if (ic.keyseq.length > 0)
463         MIM.handle_keyseq (event, ic);
464     }
465   else
466     {
467       ic.reset ();
468       //document.getElementById ('text').value
469       //= keyseq_string (ic.keyseq) + " unhandled";
470     }
471 };
472
473 MIM.reset_ic = function (event)
474 {
475   var ic = event.target.mim_ic;
476   if (ic)
477     ic.reset ();
478 };
479
480 MIM.keydown = function (event)
481 {
482   var target = event.target;
483   if (! (target.type == "text" || target.type == "textarea"))
484     return;
485
486   var ic = target.mim_ic;
487   if (! ic || ic.im != MIM.current_im)
488     {
489       ic = new MIM.ic (MIM.current_im, target);
490       target.mim_ic = ic;
491       MIM.add_event_listener (target, 'blur', MIM.reset_ic);
492     }
493   if (ic.im.status < 0)
494     return;
495   ic.check_range ();
496   MIM.debug_print (event, ic);
497   ic.key = MIM.decode_key (event);
498 };
499
500 MIM.keypress = function (event)
501 {
502   if (! (event.target.type == "text" || event.target.type == "textarea"))
503     return;
504
505   var ic = event.target.mim_ic;
506   var i;
507
508   try {
509     if (ic.im.status < 0)
510       return;
511     if (! ic.key)
512       ic.key = MIM.decode_key (event);
513     if (! ic.key)
514       {
515         ic.reset ();
516         return;
517       }
518     ic.keyseq.push (ic.key);
519     if (ic.im.status == 1) // Still loading.
520       return;
521     MIM.handle_keyseq (event, ic);
522   } finally {
523     MIM.debug_print (event, ic);
524   }
525   return;
526 };
527
528 MIM.select_im = function (event)
529 {
530   var target = event.target.parentNode;
531   while (target.tagName != "SELECT")
532     target = target.parentNode;
533   var idx = 0;
534   var im = false;
535   for (var lang in MIM.list)
536     for (var name in MIM.list[lang])
537       if (idx++ == target.selectedIndex)
538         {
539           im = MIM.list[lang][name];
540           break;
541         }
542   document.getElementsByTagName ('body')[0].removeChild (target);
543   target.target.focus ();
544   if (im && im != MIM.current_im)
545     MIM.current_im = MIM.load_sync (im);
546 };
547
548 MIM.destroy_menu = function (event)
549 {
550   if (event.target.tagName == "SELECT")
551     document.getElementsByTagName ('body')[0].removeChild (event.target);
552 };
553
554 MIM.select_menu = function (event)
555 {
556   var target = event.target;
557
558   if (! ((target.type == "text" || target.type == "textarea")
559          && event.which == 1 && event.ctrlKey))
560     return;
561
562   var sel = document.createElement ('select');
563   sel.onclick = MIM.select_im;
564   sel.onmouseout = MIM.destroy_menu;
565   sel.style.position='absolute';
566   sel.style.left = (event.clientX - 10) + "px";
567   sel.style.top = (event.clientY - 10) + "px";
568   sel.target = target;
569   var idx = 0;
570   for (var lang in MIM.list)
571     for (var name in MIM.list[lang])
572       {
573         var option = document.createElement ('option');
574         var imname = lang + "-" + name;
575         option.appendChild (document.createTextNode (imname));
576         option.value = imname;
577         sel.appendChild (option);
578         if (MIM.list[lang][name] == MIM.current_im)
579           sel.selectedIndex = idx;
580         idx++;
581       }
582   sel.size = idx;
583   document.getElementsByTagName ('body')[0].appendChild (sel);
584 };
585
586 MIM.textinput = function (event)
587 {
588   var str = ''
589   var changed = false;
590   for (var i = 0; i < event.data.length; i++)
591     {
592       if (event.data.charAt (i) == 'あ')
593         {
594           str += 'ア'; changed = true;
595         }
596       else
597         str += event.data.charAt (i);
598     }
599   alert (str);
600   if (changed)
601     {
602       var e = document.createEvent ('TextEvent');
603       e.initTextEvent ('textInput', event.canBuggle, event.cancelable, event.view,
604                        str, event.inputMode);
605       document.getElementById ('text').value = str;
606       event.target.dispatchEvent (e);
607     }
608 }
609
610 MIM.init = function ()
611 {
612   MIM.add_event_listener (window, 'keydown', MIM.keydown);
613   MIM.add_event_listener (window, 'keypress', MIM.keypress);
614   MIM.add_event_listener (window, 'mousedown', MIM.select_menu);
615   MIM.add_event_listener (window, 'textInput', MIM.textinput);
616   if (window.location == 'http://localhost/mim/index.html')
617     MIM.server = 'http://localhost/mim';
618   MIM.current_im = MIM.register ('latin', 'post', 'latn-post.js');
619   MIM.register ('th', 'kesmanee', 'th-kesmanee.js');
620   MIM.load_sync (MIM.current_im);
621 };
622
623 MIM.init_debug = function ()
624 {
625   MIM.debug = true;
626   MIM.init ();
627 };