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