OSDN Git Service

ようやく再始動。文字化けに悩まされた・・・
[ligheditor/tagget.git] / jquery.tagget.js
1 /**
2  * tagget: Not WYSIWYG Editor (jQuery Plugin)
3  *
4  * http://tagget.org/
5  *
6  * Licensed under the MIT license.
7  * Copyright (c) 2009 tagget.org
8  *
9  * version 0.1.0
10  */
11  
12 (function($) {
13
14         // $('textarea.tagget').tagget()とかで呼び出し
15         $.fn.tagget = function(conf) {
16
17                 // 入力補完候補
18                 var keywords = {
19                 
20                         // htmlのキーワード       
21                         html: (function() {
22                 
23                                 var tags = [
24                                         '<a>#{cursor}</a>',
25                                         '<address>#{cursor}</address>',
26                                         '<![CDATA[ #{cursor} ]]>', 
27                                         '<img src="#{cursor}" />',
28                                         '<br />',
29                                         '<div>#{cursor}</div>\n',
30                                         '<span>#{cursor}</span>',
31
32                                         '<h1>#{cursor}</h1>\n',
33                                         '<h2>#{cursor}</h2>\n',
34                                         '<h3>#{cursor}</h3>\n',
35                                         '<h4>#{cursor}</h4>\n',
36                                         '<h5>#{cursor}</h5>\n',
37                                         '<h6>#{cursor}</h6>\n',
38
39                                         '<ul>\n<li>#{cursor}</li>\n</ul>\n',
40                                         '<ol>\n<li>#{cursor}</li>\n</ol>\n',
41                                         '<li>#{cursor}</li>\n',
42                                         '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n',
43                                         '<dt>#{cursor}</dt>\n',
44                                         '<dt>#{cursor}</dd>\n',
45
46                                         '<strong>#{cursor}</strong>',
47                                         '<em>#{cursor}</em>',
48
49                                         '<form>\n#{cursor}\n</form>',
50                                         '<input type="#{cursor}" />',
51
52                                         '<!--',
53                                         '-->',
54                                         '<!-- #{cursor} -->'
55                                 ];
56
57                                 var header = [
58                                         '<?xml version="1.0" encoding="#{cursor}"?>\n', 
59                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
60                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n',
61                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n',
62                                         '<!DOCTYPE html>', 
63                                         '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">\n',
64                                         '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n',
65                                         '<script type="text/javascript" src="#{cursor}"></script>\n',
66                                         '<script type="text/javascript">\n#{cursor}\n</script>\n',
67                                         '<style type="text/css">\n#{cursor}\n</style>\n',
68
69                                         '<meta http-equiv="Content-Type" content="text/html; charset=#{cursor}" />\n',
70                                         '<meta http-equiv="Content-Style-Type" content="text/css" />\n',
71                                         '<meta http-equiv="Content-Script-Type" content="text/javascript" />\n',
72                                         '<meta name="ROBOTS" content="#{cursor}" />\n',
73                                         '<meta name="description" content="#{cursor}" />\n',
74                                         '<meta name="keywords" content="#{cursor}" />\n',
75
76                                         '<head>#{cursor}</head>\n',
77                                         '<title>#{cursor}</title>\n',
78                                         '<body>#{cursor}</body>\n',
79
80                                         '<link rel="alternate" type="application/atom+xml" title="#{cursor}" href="" />\n',
81                                         '<link rel="alternate" type="application/rss+xml" title="#{cursor}" href="" />\n',
82                                         '<link rel="EditURI" type="application/rsd+xml" title="#{cursor}" href="" />\n'
83                                 ];
84
85                                 var attributes = [
86                                         'href="#{cursor}"',
87                                         'id="#{cursor}"',
88                                         'class="#{cursor}"',
89                                         'style="#{cursor}"',
90
91                                         'onclick="#{cursor}"',
92                                         'type="#{cursor}"',
93                                         'value="#{cursor}"',
94                                         'name="#{cursor}"',
95                                         'action="#{cursor}"',
96
97                                         'xml:lang="#{cursor}"',
98                                         'lang="#{cursor}',
99                                         'xmlns="#{cursor}"'
100                                 ];
101                                 
102                                 var values = [
103                                         'UTF-8',
104                                         'EUC-JP',
105                                         'Shift-JIS',
106                                         'ja',
107                                         'http://www.w3.org/1999/xhtml'
108                                 ];
109
110 //                              return tags.concat(header, attributes, values).sort();
111                                 return tags.concat(header, attributes, values);
112
113                         })(),
114                         
115                         // JavaScript
116                         js: (function() {
117                         
118                                 var objs = [
119                                         'function() { #{cursor} }',
120                                         'if (#{cursor}) { }'
121                                 ];
122                                 
123                                 var libs = [
124                                         'click(#{cursor});',
125                                         'html(#{cursor});'
126                                 ];
127                                 
128                                 return objs.concat(libs);               
129                         
130                         })(),
131                         
132                         // CSS
133                         css: (function() {
134                         
135                                 var props = [
136                                         'margin: ',
137                                         'margin-right: ',
138                                         'margin-left: ',
139                                         'margin-top: ',
140                                         'margin-bottom: ',
141                                         'padding: ',
142                                         'float: ',
143                                         'clear: '
144                                 ];
145                                                         
146                                 var values = [
147                                         'auto',
148                                         'center',
149                                         'right',
150                                         'left',
151                                         'both'
152                                 ];
153
154                                 return props.concat(values);            
155                         
156                         })()
157                 
158                 };              
159
160                 // 設定オブジェクト
161                 conf = $.extend({
162                         tags: true, // タグボタン表示
163                         edit: true, // 編集機能表示
164                         jqui: true, // jQuery UI CSS Framework仕様
165                         vars: true // 入力内容を利用した補完
166                 }, conf);
167
168                 // 追加の入力候補
169                 conf.keys = $.extend(conf.keys, keywords);
170
171                 // thisには$('textarea.tagget')が入ってくる
172
173                 this.each(function() {
174
175 //                              var tmp = this.value;
176                                 // 初期化中メッセージ
177 //                              this.value = 'Initializing tagget...';
178                         
179                                 init(this, conf);
180                                 
181                                 // 初期化完了
182 //                              this.value = tmp;
183                         });
184                         
185                 // This is jQuery!!
186                 return this;
187                 
188         }; // $.fn.tagget
189
190         // contextに依存しない関数
191         
192         // &, <, >(, ")を変換
193         var escapeHtml = function(s, quot) {
194                 s = s.replace(/&/g, '&amp;')
195                         .replace(/</g, '&lt;')
196                         .replace(/>/g, '&gt;');
197                 
198                 return !quot ? s : s.replace(/"/g, '&quot;');
199         };
200         
201         // &amp;, &lt;, &gt;(, &quot;)を戻す
202         var unescapeHtml = function(s, quot) {
203                                         
204                 s = s.replace(/&amp;/g, '&')
205                         .replace(/&lt;/g, '<')
206                         .replace(/&gt;/g, '>');
207                 
208                 return !quot ? s : s.replace(/&quot;/g, '"');
209         };
210
211         // offsetを簡易算出
212         var getOffset = function(elm) {
213
214                 var left, top;
215
216                 if (elm.getBoundingClientRect) {
217
218                         var rect = elm.getBoundingClientRect();
219                         left = Math.round(scrollX + rect.left);
220                         top = Math.round(scrollY + rect.top);
221                         
222                 } else {
223
224                         left = elm.offsetLeft;
225                         top  = elm.offsetTop;
226                         var offsetParent = elm.offsetParent;
227                         
228                         while (offsetParent) {
229                                 left += offsetParent.offsetLeft;
230                                 top  += offsetParent.offsetTop;             
231                                 offsetParent = offsetParent.offsetParent;
232                         }
233                         
234                 }
235
236                 return {
237                         left: left,
238                         top: top
239                 };
240                 
241         };
242
243
244         // tagget初期化
245         var init = function(textarea, conf) {
246         
247                 var t = $(textarea);
248
249                 // textarea or dummyを使う関数群
250                 
251                 // textareaのカーソル位置に文字列挿入
252                 var insert = function(s) {
253
254                         // カーソル移動位置(#{cursor})を取得後、削除
255                         var cursor = s.indexOf('#{cursor}');
256                         s = s.replace('#{cursor}', '');
257
258                         // focusしないとIEでbodyに挿入されたりする
259                         // Firefoxでもボタンで挿入後にfocusが戻らない
260                         textarea.focus(); 
261
262                         // for IE
263                         if (document.selection) {
264                                 
265                                 // 選択範囲を取得
266                                 var range = document.selection.createRange();
267
268                                 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
269                                 range.text = s;
270
271                                 // カーソルがrange.textの最後になるので戻す
272                                 // #{cursor}指定がなければ最後のまま
273                                 var back = s.length - (cursor != -1 ? cursor : s.length);
274                                 range.move('character', -back);
275
276                                 // 現在のカーソル位置を反映する(これやらないと水の泡)
277                                 range.select();
278                         }
279
280                         // Firefox
281                         // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
282                     else if ('selectionStart' in textarea) { 
283
284                                 // スクロールバーの位置を保存
285                                 var top = textarea.scrollTop;
286
287                                 // 選択範囲の開始・終了位置を取得
288                         var start = textarea.selectionStart;
289                         var end = textarea.selectionEnd;
290
291                                 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
292                         textarea.value = textarea.value.slice(0, start) + s + textarea.value.slice(end);
293
294                                 // カーソル移動位置に移動させる
295                                 var index = start + (cursor != -1 ? cursor : s.length);
296                         textarea.setSelectionRange(index, index);
297
298                                 // 改行がたくさんある場合スクロールバーを下にずらす
299                                 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
300                                         top += parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10);
301                                 }
302                                 
303                                 // スクロールバーを戻す
304                             textarea.scrollTop = top;
305                     }
306
307                         return this;
308                         
309                 };
310
311                 // 補完候補があるか?
312                 var check = function() {
313                 
314                         var matches = {
315                                 view: [],
316                                 insert: []
317                         };
318                 
319                         var text = getText()[0];
320
321                         if (text) { 
322
323                                 var words = [];
324                                 
325                                 if (conf.vars) {
326                                         var a = textarea.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]+/g) || [];
327
328                                         // 重複削除
329                                         var temp = [];                          
330                                         for (var i = 0; i < a.length; i++) {
331                                         
332                                                 var v = a[i];
333                                         
334                                                 if (!(v in temp)) {
335                                                         words.push(v);
336                                                         temp[v] = true;
337                                                 }
338                                         
339                                         }
340
341                                         
342                                 }
343
344                                 for(var key in conf.keys) {
345                                         if (t.hasClass('tagget_' + key) || t.hasClass(key)) {
346                                                 words = words.concat(conf.keys[key]);
347                                         }
348                                 }
349
350 // sortは重い原因になるので止め
351 //                              words = words.sort();
352                         
353                                 for(var i = 0; i < words.length; i++) {
354                                 
355                                         if (words[i] != text && words[i].indexOf(text) == 0) {
356                                                 matches.view.push(words[i].replace(/#\{cursor\}/g, ''));
357                                                 matches.insert.push(escapeHtml(words[i].slice(text.length)));
358                                         }
359                                 }
360                                 
361                         }
362                 
363                         return (matches.view.length != 0) ? matches : false;
364
365                 
366                 };
367
368                 // カーソル位置の文字を取得
369                 var getText = function(r, after) {
370
371 //                      if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
372                         if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
373
374                         var start, end;
375
376                         // IE
377                         if (document.selection) {
378
379 //                              textarea.focus(); // focusなしでもいける
380
381                                 // 選択範囲を取得
382                                 var range = document.selection.createRange();
383
384                                 // 選択範囲の複製を作成
385                                 var clone = range.duplicate();
386
387                                 // textarea内のテキスト全体を選択
388                                 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
389                                 clone.moveToElementText(textarea);
390
391                                 // cloneの選択範囲終点を、rangeの終点にあわせる
392                                 // [clone start] text1 [range start] text2 [range/clone end] text3
393                                 clone.setEndPoint('EndToEnd', range);
394
395                                 // 選択範囲始点を求める
396                                 // [clone start] text1 [range start] text2 [range/clone end] text3
397                                 // --------------------------------------------------------- clone.text.length == end
398                                 //                     ------------------------------------- range.text.length
399                                 // -------------------- clone.text.length - range.text.length = start
400                                 start = clone.text.length - range.text.length;
401                                 end = clone.text.length;
402
403                         }
404
405                         // Firefox
406                     else if ('selectionStart' in textarea) {
407
408                         start = textarea.selectionStart;
409                         end = textarea.selectionEnd;
410
411                     }
412
413                         var text;
414                         
415                         if (!after) {
416                                 text = textarea.value.slice(0, start).match(r);
417                         } else {
418                                 text = textarea.value.slice(end).match(r);
419                         }
420                         
421                         return text || [];
422                         
423                 };
424
425                 // カーソルの座標を取得
426                 var getPos = function() {
427
428                         var x, y;
429
430                         if (document.selection) {
431
432                                 var range = document.selection.createRange();
433                                 x = range.offsetLeft + 
434                                         (document.body.scrollLeft || document.documentElement.scrollLeft) - 
435                                                 document.documentElement.clientLeft;
436                                 y = range.offsetTop + 
437                                         (document.body.scrollTop || document.documentElement.scrollTop) - 
438                                                 document.documentElement.clientTop;
439                         
440                         } else if (window.getComputedStyle) {
441
442                                 var span = dummy.children('span');
443                                 
444                                 if(!span.is('span')) {
445                                         span = $('<span></span>');
446                                         span.html('|');
447                                 }
448                         
449                                 dummy.html('');
450                                 dummy.text(textarea.value.slice(0, textarea.selectionEnd));
451                                 dummy.append(span);
452
453                                 var offset = getOffset(span.get(0));
454
455                                 x = offset.left - textarea.scrollLeft;
456                                 y = offset.top - textarea.scrollTop;
457                     }
458
459                         return {
460                                 x: x,
461                                 y: y
462                         };
463                 };
464
465                 // wrap処理
466                 // textarea周囲にHTMLを追加する
467                 
468                 // 全体の枠を作ってその参照を取得
469                 // wrapの場合、普通にやると参照を取得できないのでparentsで取得
470                 var wrapper = t.wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
471                         .parents('div.tagget_wrapper');
472
473                 // 必要に応じてツールバーを追加
474                 var menu = (conf.tags || conf.edit) ?
475                         wrapper.prepend(('<div class="tagget_menu"></div>')).children('div.tagget_menu') : null;
476         
477                 // タグ挿入ボタン
478                 if (conf.tags) {
479                         var tags = $('<ul class="tagget_tags"></ul>')
480                                 .append('<li><a href="#">a</a></li>')
481                                 .append('<li><a href="#">p</a></li>')
482                                 .append('<li><a href="#">ul</a></li>')
483                                 .append('<li><a href="#">li</a></li>')
484                                 .append('<li><a href="#">div</a></li>')
485                                 .append('<li><a href="#">span</a></li>')
486                                 .append('<li><a href="#">pre</a></li>')
487                                 .append('<li><a href="#">code</a></li>')
488                                 .append('<li><a href="#">blockquote</a></li>')
489                                 .append('<li><a href="#">dl</a></li>')
490                                 .append('<li><a href="#">dt</a></li>')
491                                 .append('<li><a href="#">dd</a></li>')
492                                 .append('<li><a href="#">link</a></li>')
493                                 .append('<li><a href="#">script</a></li>')
494                                 .append('<li><a href="#">frameset</a></li>')
495                                 .append('<li><a href="#">frame</a></li>');
496                         menu.append(tags);
497                 }       
498
499                 // 編集機能
500                 if (conf.edit) {
501                         var edit = $('<div class="tagget_edit"></div>');
502                         
503                         // 選択範囲変換
504                         edit.append(
505                                 $('<p class="tagget_encode"></p>').append(
506                                         $('<select></selected>')
507                                                 .append('<option selected="selected" value="">選択範囲を変換</option>')
508                                                 .append('<option value="entity">&amp &lt; &gt; → &amp;amp; &amp;lt; &amp;gt;</option>')
509                                                 .append('<option value="raw">&amp;amp; &amp;lt; &amp;gt; → &amp &lt; &gt;</option>')
510                                                 .append('<option value="enc">encodeURI()</option>')
511                                                 .append('<option value="encc">encodeURIComponent()</option>')
512                                                 .append('<option value="dec">decodeURI()</option>')
513                                                 .append('<option value="decc">decodeURIComponent()</option>')
514                                 )
515                         );
516
517                         // 置換
518                         edit.append(
519                                 $('<p class="tagget_replace"></p>')
520                                         .append('<input type="text" value="置換前" />')
521                                         .append(' → ')
522                                         .append('<input type="text" value="置換後" />')
523                                         .append('<input type="button" value="置換" />')
524                         );
525
526                         menu.append(edit);
527                 }
528
529                 // jQuery UI CSS Frameworkのclass名を設定  
530                 if (conf.jqui) {
531                         menu.addClass('ui-helper-clearfix');
532                         wrapper.addClass('ui-widget-header ui-corner-all')
533                                 .children('div, p').addClass('ui-widget-header')
534                                         .find('li').addClass('ui-state-default ui-corner-all').hover(function(){ 
535                                                 $(this).addClass('ui-state-hover'); 
536                                         }, function(){ 
537                                                 $(this).removeClass('ui-state-hover'); 
538                                         });     
539                 }       
540
541                 var body = $(document.body);
542
543                 // suggestion用の要素作成
544                 var suggest = $('<ul class="tagget_suggest"></ul>');
545                 body.append(suggest);
546
547                 // Firefox用dummy生成
548                 // カーソル座標取得に使用
549                 if (window.getComputedStyle) {
550                         
551                         var dummy = $('<pre class="tagget_dummy"></pre>');
552
553                         // textareaのstyleをdummyにコピー
554                         var onResize = function() {
555                                 
556                                 var org = getComputedStyle(textarea,'');
557
558                                 var props = [
559                                         'width', 'height',
560                                         'padding-left', 'padding-right', 'padding-top', 'padding-bottom', 
561                                         'border-left-style', 'border-right-style','border-top-style','border-bottom-style', 
562                                         'border-left-width', 'border-right-width','border-top-width','border-bottom-width', 
563                                         'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
564                                 ];
565                         
566                             for(var i = 0; i < props.length; i++){
567                             
568                                 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
569                                                 return m1.toUpperCase();
570                                         });
571                             
572                                 dummy.css(capitalized, org.getPropertyValue(props[i]));
573
574                                 }
575
576                                 var offset = getOffset(textarea);
577
578                             dummy.css({
579                                 left: offset.left,
580                                 top: offset.top
581                             });
582                             
583                             dummy.width(t.width())
584                                 .height(t.height())
585                                         .scrollLeft(t.scrollLeft())
586                                 .scrollTop(t.scrollTop());
587
588                         };
589                         
590                         // resize時にはtextareaのサイズも変わるので
591                         $(window).resize(function() {
592                                 onResize();
593                         }).resize();
594
595                         body.append(dummy);
596
597                 }
598                 
599                 
600                 // イベント設定
601
602                 // タグ挿入ボタン
603                 if (conf.tags) {
604
605                         // clickイベント設定
606                         var onClick = function(elm, s) {
607
608                                 $(elm).click(function() {
609                                         insert(s);
610                                         return false;
611                                 });
612                                 
613                         };
614
615                         // タグボタンクリックで挿入
616                         tags.find('a').each(function() {
617
618                                 // htmlで判別してイベント設定
619                                 // title属性に設定するほうがいいか?
620                                 switch (this.innerHTML) {
621                                 
622                                         case 'a':
623                                                 onClick(this, '<a href="#{cursor}"></a>');
624                                                 break;
625                                 
626                                         case 'p':
627                                                 onClick(this, '<p>#{cursor}</p>\n');
628                                                 break;
629                                 
630                                         case 'ul':
631                                                 onClick(this, '<ul>\n<li>#{cursor}</li>\n</ul>\n');
632                                                 break;
633                                 
634                                         case 'li':
635                                                 onClick(this, '<li>#{cursor}</li>\n');
636                                                 break;
637
638                                         case 'dl':
639                                                 onClick(this, '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n');
640                                                 break;
641
642                                         case 'dt':
643                                                 onClick(this, '<dt>#{cursor}</dt>\n');
644                                                 break;
645
646                                         case 'dd':
647                                                 onClick(this, '<dd>#{cursor}</dd>\n');
648                                                 break;
649
650                                         case 'pre':
651                                                 onClick(this, '<pre>#{cursor}</pre>\n');
652                                                 break;
653
654                                         case 'code':
655                                                 onClick(this, '<code>#{cursor}</code>');
656                                                 break;
657
658                                         case 'blockquote':
659                                                 onClick(this, '<blockquote>#{cursor}</blockquote>\n');
660                                                 break;
661
662                                         case 'div':
663                                                 onClick(this, '<div>#{cursor}</div>\n');
664                                                 break;
665
666                                         case 'span':
667                                                 onClick(this, '<span>#{cursor}</span>\n');
668                                                 break;
669
670                                         case 'link':
671                                                 onClick(this, '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n');
672                                                 break;
673
674                                         case 'script':
675                                                 onClick(this, '<script type="text/javascript" src="#{cursor}"></script>\n');
676                                                 break;
677                                                 
678                                         case 'frameset':
679                                                 onClick(this, '<frameset>\n<frame src="#{cursor}" />\n</frameset>\n');
680                                                 break;
681                                                 
682                                         case 'frame':
683                                                 onClick(this, '<frame src="#{cursor}" />\n');
684                                                 break;
685                                                 
686                                 }
687                         
688                         });
689
690                 }
691
692                 // 編集機能設定   
693                 if (conf.edit) {
694                 
695                         // 選択範囲変換
696                         
697                         // 渡された関数で選択範囲を変換
698                         var onChange = function(func) {
699
700                                 textarea.focus(); 
701
702                                 if (document.selection) {
703                                 
704                                         var range = document.selection.createRange();
705                                         range.text = func(range.text);
706                                         range.select();
707                                         
708                                 } else if ('selectionStart' in textarea) { 
709
710                                         var top = textarea.scrollTop;
711
712                                 var start = textarea.selectionStart;
713                                 var end = textarea.selectionEnd;
714
715                                         textarea.value = textarea.value.slice(0, start) + 
716                                                 func(textarea.value.slice(start, end)) + 
717                                                         textarea.value.slice(end);
718
719                                 textarea.setSelectionRange(end, end);
720
721                                     textarea.scrollTop = top;
722                             }
723                         };
724                 
725                         // onchangeイベント設定
726                         edit.find('.tagget_encode select').change(function() {
727                         
728                                 switch (this.value) {
729                                 
730                                         case 'entity':
731                                                 onChange(escapeHtml);
732                                                 break;
733                                 
734                                         case 'raw':
735                                                 onChange(unescapeHtml);
736                                                 break;                          
737                                 
738                                         case 'enc':
739                                                 onChange(encodeURI);
740                                                 break;                          
741                                 
742                                         case 'encc':
743                                                 onChange(encodeURIComponent);
744                                                 break;  
745                                                                         
746                                         case 'dec':
747                                                 onChange(decodeURI);
748                                                 break;                          
749                                                                         
750                                         case 'decc':
751                                                 onChange(decodeURIComponent);
752                                                 break;                          
753                                 }
754                                 
755                                 this.value = '';
756                         
757                         });
758                         
759                         
760                         // 置換ボタンクリックで置換
761                         // 正規表現使用可
762                         var inputs = edit.find('.tagget_replace input');
763                         inputs.filter('[type=button]').click(function() {
764
765                                 var val = textarea.value;
766                                 
767                                 var before = inputs.eq(0).val();
768                                 var after = inputs.eq(1).val();
769                                 var flag = '';
770
771                                 if (before.match(/^\/.+\/([^\/]+)$/)) {
772                                         
773                                         flag = RegExp.$1;
774                                         before = before.replace(/^\/|\/[^\/]+?$/g, '');
775                                 }
776                                                                 
777                                 if (before) {
778                                         textarea.value = val.replace(new RegExp(before, flag), after);
779                                 }
780                                 
781                         });     
782                                 
783                 }
784
785                 // メイン機能
786                 // キー入力時のsuggestion設定
787                 
788                 // keyup(発生タイミングが一番少ない)で候補表示
789                 t.keyup(function(e) {
790                 
791                         var suggests = check();
792                         
793                         if (suggests) {
794                         
795                                 if (suggest.text() != suggests.view.join('')) {
796                         
797                                         suggest.html('');
798                         
799                                         for(var i = 0; i < suggests.view.length; i++) {
800                                                 var li = $('<li></li>').attr('title', suggests.insert[i])
801                                                         .append('<a href="#"></a>').text(suggests.view[i])
802                                                         .hover(function() {
803                                                                 suggest.children('li').removeClass('tagget_current');
804                                                                 $(this).addClass('tagget_current');
805                                                         })
806                                                         .click(function() {
807                                                                 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
808                                                                 suggest.hide();
809                                                         });
810                                                 if (i == 0) li.addClass('tagget_current');
811                                                 suggest.append(li);
812                                         }       
813
814                                 } else {
815
816                                         suggest.children('li').each(function(i) {
817                                                 $(this).attr('title', suggests.insert[i]);
818                                         });                     
819                                 }
820                                 
821                                 var pos = getPos();
822                                 
823                                 suggest.css({
824                                         left: pos.x,
825                                         top: pos.y
826                                 });
827
828                                 suggest.show();
829                                 
830                         } else {
831                                 suggest.hide();
832                         }       
833
834                         if (conf.bind) $(conf.bind).html(textarea.value);
835                 })
836                 
837                 // click時にも候補表示
838                 .click(function() {
839                         $(this).keyup();
840                 })
841                 
842                 .keydown(function(e) {
843                 
844                         // タブキャンセル
845                         // keydown以外だとうまくいかない
846                         if (e.which == 9) {
847                                 insert('\t');
848                                 return false;
849                         }
850
851                         // Shift+Enterで改行 or br入力
852                         // 補完却下も可能
853                         if (e.shiftKey && e.which == 13) {
854
855                                 var n = '\n'
856                                 
857                                 if ((t.hasClass('html') || t.hasClass('tagget_html')) && suggest.css('display') == 'none') {
858                                         n = '<br />\n';
859                                 }
860                                 
861                                 insert(n);
862                                 
863                                 return false;
864                         }
865
866                         // Ctrl+Enterで閉じタグ補完
867                         if (e.ctrlKey && e.which == 13) {
868
869                                 //                  <<-tag name-><----------attr-------------><->->
870                                 var sTag = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g);
871                                 var sTag2 = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g, true);
872                                 var eTag = getText(/<\s*\/\s*.+?\s*>/g);
873                                 var eTag2 = getText(/<\s*\/\s*.+?\s*>/g, true);
874 /*
875                                 console.log('s:'+sTag);
876                                 console.log('s2:'+sTag2);
877                                 console.log('e:'+eTag);
878                                 console.log('e2:'+eTag2);
879 */
880                                 // 整形式じゃなさそうなのはとりあえず無視
881                                 if (sTag.length + sTag2.length > eTag.length + eTag2.length) {
882                                 
883                                         for (var i = sTag.length - 1; i >= 0; i--) {
884                                         
885                                                 var s = sTag[i].replace(/^.*<|[\s>].*/g, '');
886                                                 if (!eTag || eTag.length == 0 || s != eTag.shift().replace(/^.*\/|>.*/g, '')) {
887                                                         insert('</' + s + '>');
888                                                         break;
889                                                 }
890                                         }
891
892                                 }
893
894                                 
895                                 // insert(n);
896                                 
897                                 return false;
898                         }
899
900                         // 十字キーで候補選択
901                         // upだとおしっぱにできない、pressだとおかしくなる
902                         if (suggest.css('display') != 'none') {
903
904
905                                 switch (e.which) {
906                                 
907                                         // up
908                                         case 38:
909                                                 var lis = suggest.children('li');
910                                                 
911                                                 for(var i = 0; i < lis.length; i++) {
912                                                 
913                                                         var li = lis.eq(i);
914                                                         
915                                                         if(li.hasClass('tagget_current')) {
916                                                                 li.removeClass('tagget_current');
917                                                                 i = (i == 0) ? lis.length - 1 : i - 1;
918                                                                 lis.eq(i).addClass('tagget_current');
919                                                                 break;
920                                                         }
921                                                 
922                                                 }
923                                                 return false;
924
925                                         // down
926                                         case 40:
927                                                 var lis = suggest.children('li');
928                                                 
929                                                 for(var i = 0; i < lis.length; i++) {
930                                                 
931                                                         var li = lis.eq(i);
932                                                         
933                                                         if(li.hasClass('tagget_current')) {
934                                                                 li.removeClass('tagget_current');
935                                                                 i = (i == lis.length - 1 ) ? 0 : i + 1;
936                                                                 lis.eq(i).addClass('tagget_current');
937                                                                 break;
938                                                         }
939                                                 
940                                                 }                       
941                                                 return false;
942
943                                         // Enterで補完
944                                         case 13:
945                                                 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
946                                                 suggest.hide();
947
948                                                 return false;
949                                 }
950
951                         } else {
952                         
953                                 if (e.which == 13) {
954                         
955                                         var indent = getText(/^[\t ]*/mg);
956                                         insert('\n' + (indent ? indent[indent.length - 1] : ''));
957
958                                         if (window.getComputedStyle) {
959                                                 t.scrollTop(t.scrollTop() + parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10));
960                                         }
961                                         
962                                         return false;
963                         
964                                 }                       
965                         
966                         }
967
968                 });
969         
970         }; // init
971
972 })(jQuery);