OSDN Git Service

候補が出すぎなのでいつかは対策しないと
[ligheditor/tagget.git] / jquery.tagget.js
1 /**
2  * tagget: The Simple HTML 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.1
10  */
11 (function($) {
12
13     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
14
15         /**
16          *
17          * 入力補完候補を保持、取得するオブジェクト
18          *
19          */
20         var Suggester = {
21         
22                 // 入力データを利用した補完を行うか
23                 byInputs: true,
24
25                 keywords: {     
26                 
27                         html: (function() {
28
29                                 // <head>内の要素           
30                                 var header = [
31                                         '<?xml version="1.0" encoding="#{c}"?>\n', 
32                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
33                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n',
34                                         '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n',
35                                         '<!DOCTYPE html>', 
36                                         '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">\n',
37                                         '<link rel="stylesheet" type="text/css" href="#{c}" />\n',
38                                         '<script type="text/javascript" src="#{c}"></script>\n',
39                                         '<script type="text/javascript">\n#{c}\n</script>\n',
40                                         '<style type="text/css">\n#{c}\n</style>\n',
41
42                                         '<meta http-equiv="Content-Type" content="text/html; charset=#{c}" />\n',
43                                         '<meta http-equiv="Content-Style-Type" content="text/css" />\n',
44                                         '<meta http-equiv="Content-Script-Type" content="text/javascript" />\n',
45                                         '<meta name="ROBOTS" content="#{c}" />\n',
46                                         '<meta name="description" content="#{c}" />\n',
47                                         '<meta name="keywords" content="#{c}" />\n',
48
49                                         '<head>#{c}</head>\n',
50                                         '<title>#{c}</title>\n',
51                                         '<body>#{c}</body>\n',
52
53                                         '<link rel="alternate" type="application/atom+xml" title="#{c}" href="" />\n',
54                                         '<link rel="alternate" type="application/rss+xml" title="#{c}" href="" />\n',
55                                         '<link rel="EditURI" type="application/rsd+xml" title="#{c}" href="" />\n'
56                                 ];
57
58                                 // <body>内の要素           
59                                 var body = [
60                                         '<a>#{c}</a>',
61                                         '<address>#{c}</address>',
62                                         '<![CDATA[ #{c} ]]>', 
63                                         '<img src="#{c}" />',
64                                         '<br />',
65                                         '<div>#{c}</div>\n',
66                                         '<span>#{c}</span>',
67
68                                         '<h1>#{c}</h1>\n',
69                                         '<h2>#{c}</h2>\n',
70                                         '<h3>#{c}</h3>\n',
71                                         '<h4>#{c}</h4>\n',
72                                         '<h5>#{c}</h5>\n',
73                                         '<h6>#{c}</h6>\n',
74
75                                         '<ul>\n<li>#{c}</li>\n</ul>\n',
76                                         '<ol>\n<li>#{c}</li>\n</ol>\n',
77                                         '<li>#{c}</li>\n',
78                                         '<dl>\n<dt>#{c}</dt>\n<dd></dd>\n</dl>\n',
79                                         '<dt>#{c}</dt>\n',
80                                         '<dt>#{c}</dd>\n',
81
82                                         '<strong>#{c}</strong>',
83                                         '<em>#{c}</em>',
84
85                                         '<form>\n#{c}\n</form>',
86                                         '<input type="#{c}" />',
87
88                                         '<!--',
89                                         '-->',
90                                         '<!-- #{c} -->'
91                                 ];
92
93                                 // 属性               
94                                 var attributes = [
95                                         'href="#{c}"',
96                                         'id="#{c}"',
97                                         'class="#{c}"',
98                                         'style="#{c}"',
99
100                                         'onload="#{c}"',
101                                         'onclick="#{c}"',
102                                         'onmouseover="#{c}"',
103                                         'onmouseout="#{c}"',
104                                         'ondblclick="#{c}"',
105                                         'onsubmit="#{c}"',
106                                         
107                                         'type="#{c}"',
108                                         'value="#{c}"',
109                                         'name="#{c}"',
110                                         'action="#{c}"',
111
112                                         'xml:lang="#{c}"',
113                                         'lang="#{c}',
114                                         'xmlns="#{c}"'
115                                 ];
116                                 
117                                 // 属性値    
118                                 var values = [
119                                         'UTF-8',
120                                         'EUC-JP',
121                                         'Shift-JIS',
122                                         'text',
123                                         'button',
124                                         'submit',
125                                         'reset',
126                                         'ja',
127                                         'http://www.w3.org/1999/xhtml'
128                                 ];
129
130                                 return header.concat(body, attributes, values);
131
132                         })(),
133                         
134                         js: (function() {
135                         
136                                 var objs = [
137                                         'function() { #{c} }',
138                                         'if (#{c}) { }'
139                                 ];
140                                 
141                                 var libs = [
142                                         'click(#{c});',
143                                         'html(#{c});'
144                                 ];
145                                 
146                                 return objs.concat(libs);               
147                         
148                         })(),
149                         
150                         css: (function() {
151                         
152                                 var props = [
153                                         'margin: ',
154                                         'margin-right: ',
155                                         'margin-left: ',
156                                         'margin-top: ',
157                                         'margin-bottom: ',
158                                         'padding: ',
159                                         'float: ',
160                                         'clear: '
161                                 ];
162                                                         
163                                 var values = [
164                                         'auto',
165                                         'center',
166                                         'right',
167                                         'left',
168                                         'both'
169                                 ];
170
171                                 return props.concat(values);            
172                         
173                         })()
174                         
175                         
176                 }, // keywords
177         
178         
179                 /**
180                  * キーワードを追加
181                  * keywordsと同じ形式で渡す。
182                  */
183                 add: function(newwords) {
184         
185                         for(var type in newwords) {
186         
187                                 if (this.keywords[type]) {
188                                         this.keywords[type].concat(newwords[type]);
189                                 } else {
190                                         this.keywords[type] = newwords[type];
191                                 }
192         
193                         }
194         
195                 }, //add
196                 
197                 /**
198                  * 補完候補を取得
199                  */
200                 get: function(t, s) {
201                 
202                         if (!t || !s || !t.value) {
203                                 return false;
204                         }
205                         
206                         var matches = {
207                                 view: [],
208                                 insert: []
209                         };
210                 
211                         var words = [];
212
213                         // 入力内容で補完
214                         if (Wrapper.checkIntelli(t)) {
215
216                                 // 変数名とかを抽出
217                                 // 記号じゃない連続文字列
218                                 var a = t.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]{2,}/g) || [];
219
220                                 // 重複削除
221                                 var temp = [];                          
222                                 for (var i = 0; i < a.length; i++) {
223                                 
224                                         var v = a[i];
225                                 
226                                         if (!(v in temp)) {
227                                                 words.push(v);
228                                                 temp[v] = true;
229                                         }
230                                 
231                                 }
232                                 
233                         }
234
235                         for(var key in this.keywords) {
236                                 if (Wrapper.checkType(t, key)) {
237                                         words = words.concat(this.keywords[key]);
238                                 }
239                         }
240
241                         for(var i = 0; i < words.length; i++) {
242                         
243                                 if (words[i] != s && words[i].indexOf(s) == 0) {
244                                         matches.view.push(words[i].replace(/#\{c\}/g, ''));
245                                         // 既に入力されている部分を除いて挿入
246                                         matches.insert.push(words[i].slice(s.length));
247                                 }
248                         }
249                                 
250                         return (matches.view.length != 0) ? matches : false;
251                 
252                 }
253         
254         };
255
256     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
257         /**
258          * taggetに必要なHTMLを生成・操作する
259          */
260         Wrapper = {
261         
262                 // ツールバーを表示するか
263                 isToolbar: true,
264         
265                 // wrap処理
266                 // textarea周囲にHTMLを追加する
267                 wrap: function(t) {
268                 
269                         if ($(t).parents().is('.tagget_wrapper')) {
270                                 this.relate(t);
271                                 return true;
272                         }
273                         
274                         // 全体の枠を作ってその参照を取得
275                         // wrapの戻り値はwrapされた要素なので、parentsでwrapした要素を取得
276                         var wrapper = $(t).wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
277                                 .parents('div.tagget_wrapper');
278
279                         // 必要に応じてツールバーを追加
280                         if (this.isToolbar) {
281                                 var toolbar = 
282                                         wrapper.prepend(('<div class="tagget_toolbar"></div>')).children('div.tagget_toolbar');
283
284                                 // 選択範囲変換
285                                 toolbar.append(
286                                         $('<p class="tagget_encode"></p>').append(
287                                                 $('<select title="選択範囲を変換"></select>')
288                                                         .append('<option selected="selected" value="">Encode Selection</option>')
289                                                         .append('<option value="entity">&amp &lt; &gt; → &amp;amp; &amp;lt; &amp;gt;</option>')
290                                                         .append('<option value="raw">&amp;amp; &amp;lt; &amp;gt; → &amp &lt; &gt;</option>')
291                                                         .append('<option value="enc">encodeURI()</option>')
292                                                         .append('<option value="encc">encodeURIComponent()</option>')
293                                                         .append('<option value="dec">decodeURI()</option>')
294                                                         .append('<option value="decc">decodeURIComponent()</option>')
295                                         )
296                                 );
297
298                                 // 置換
299                                 toolbar.append(
300                                         $('<p class="tagget_replace"></p>')
301                                                 .append('<input type="text" value="Before" title="置換前" />')
302                                                 .append('<img src="img/v_arrow010102.gif" />')
303                                                 .append('<input type="text" value="After" title="置換後" />')
304                                                 .append('<input type="button" value="Replace All" title="全て置換" />')
305                                 );
306
307                                 // ファイルタイプ
308                                 toolbar.append(
309                                         $('<p class="tagget_type"></p>').append(
310                                                 $('<select title="選択範囲を変換"></select>')
311                                                         .append('<option selected="selected" value="html|css|js">HTML,CSS,JS</option>')
312                                                         .append('<option value="html|css">HTML,CSS</option>')
313                                                         .append('<option value="html">HTML</option>')
314                                                         .append('<option value="css">CSS</option>')
315                                                         .append('<option value="js">JS</option>')
316                                                         .append('<option value="css|js">CSS,JS</option>')
317                                                         .append('<option value="html|js">HTML,JS</option>')
318                                         )
319                                 );
320
321                                 // Cookie
322                                 toolbar.append(
323                                         $('<p class="tagget_cookie"></p>')
324                                                 .append('<input type="checkbox" checked="checked" title="Cookieに下書きを保存" id="tagget_check_cookie" /><label for="tagget_check_cookie" title="Cookieに下書きを保存">Cookie</label>')
325                                 );
326
327                                 // intelligent
328                                 // 入力内容を使用した補完
329                                 toolbar.append(
330                                         $('<p class="tagget_intelli"></p>')
331                                                 .append('<input type="checkbox" checked="checked" title="入力内容から補完" id="tagget_check_intelli" /><label for="tagget_check_intelli" title="入力内容から補完">intelligent</label>')
332                                 );
333
334                                 // Status
335                                 // Draft Saved At 1:17とか表示
336                                 // 行、列を表示(line: , col:)
337                                 wrapper.append(
338                                         $('<p class="tagget_status"><span class="tagget_time"></span><span class="tagget_line"></span>&nbsp;</p>')
339                                 );
340
341                                 // jQuery UI CSS Frameworkのclass名を設定  
342                                 toolbar.addClass('ui-helper-clearfix');
343                                 wrapper.addClass('ui-widget-header ui-corner-all')
344                                         .children('div, p').addClass('ui-widget-header');
345
346                         }
347                 
348                         this.relate(t);
349                 
350                 },
351
352
353                 // wrap要素にeventを設定する。
354                 // 既にHTMLがあった場合は、こちらだけを行う。
355                 relate: function(t) {
356                 
357                         var toolbar = $(t).parents('.tagget_wrapper').children('.tagget_toolbar');
358
359                         // 選択範囲変換
360                         // onchangeイベント設定
361                         toolbar.find('.tagget_encode select').change(function() {
362                         
363                                 switch (this.value) {
364                                 
365                                         case 'entity':
366                                                 Cursor.encodeSelection(t, Util.escapeHtml);
367                                                 break;
368                                 
369                                         case 'raw':
370                                                 Cursor.encodeSelection(t, Util.unescapeHtml);
371                                                 break;                          
372                                 
373                                         case 'enc':
374                                                 Cursor.encodeSelection(t, encodeURI);
375                                                 break;                          
376                                 
377                                         case 'encc':
378                                                 Cursor.encodeSelection(t, encodeURIComponent);
379                                                 break;  
380                                                                         
381                                         case 'dec':
382                                                 Cursor.encodeSelection(t, decodeURI);
383                                                 break;                          
384                                                                         
385                                         case 'decc':
386                                                 Cursor.encodeSelection(t, decodeURIComponent);
387                                                 break;                          
388                                 }
389                                 
390                                 this.value = '';
391                         
392                         });
393                         
394                         
395                         // 置換ボタンクリックで置換
396                         // 正規表現使用可
397                         var inputs = toolbar.find('.tagget_replace input');
398                         inputs.filter('[type=button]').click(function() {
399
400                                 var val = t.value;
401                                 
402                                 var before = inputs.eq(0).val();
403                                 var after = inputs.eq(1).val();
404                                 // フラグはデフォルトでg(Replace All)
405                                 var flag = 'g';
406
407                                 // TODO: 一応動くけどもっとみやすいコードに
408
409                                 // /before/gim形式で入力されたらフラグを抽出
410                                 // それ以外は全体を正規表現として扱う
411                                 if (before.match(/^\/.+\/([^\/]+)$/)) {
412                                         
413                                         // フラグ上書き
414                                         flag = RegExp.$1;
415                                         // 行頭の/、/以降の文字(フラグ、スラッシュ連続など形式外の入力)を除去
416                                         before = before.replace(/^\/|\/[^\/]+?$/g, '');
417                                 }
418                                                                 
419                                 if (before) {
420                                         t.value = val.replace(new RegExp(before, flag), after);
421                                 }
422                                 
423                         });     
424
425                 
426                 },
427
428                 // textareaからID取得
429                 getId: function(t) {
430                         return $(t).attr('class').match(/tagget_([0-9]+)/)[1];
431                 },
432                 
433                 // Popup表示状態
434                 isPopup: function(t) {
435                         var popup = this.getPopup(t);
436                         return popup.css('display') != 'none';
437                 },
438
439                 // Popup表示設定
440                 setPopup: function(display) {
441                         var popup = this.getPopup(t);
442                         if (display) {
443                                 popup.show();
444                         } else {
445                                 popup.hide();
446                         }
447                 },
448                                 
449                 checkType: function(t, type) {
450                         var currentType = $(t).parents('.tagget_wrapper')
451                                 .find('.tagget_type select').val();
452                         return (new RegExp(currentType)).test(type);
453                 },
454
455                 checkIntelli: function(t) {
456                         var intelli = $(t).parents('.tagget_wrapper')
457                                 .find('.tagget_intelli input').attr('checked');
458                         return intelli;
459                 },
460                 
461                 // Popup,Dummyを生成
462                 absolutes: function(t) {
463                 
464                         var body = $(document.body);
465                         var id = this.getId(t);
466                 
467                         // suggestion用の要素作成
468                         var popup = $('<ul class="tagget_popup tagget_popup' + id + '"></ul>');
469                         body.append(popup);
470
471                         // Firefox用dummy生成
472                         // カーソル座標取得に使用
473                         if (window.getComputedStyle) {
474                                 
475                                 var dummy = $('<pre class="tagget_dummy tagget_dummy' + id + '"></pre>');
476                                 body.append(dummy);
477
478                         }
479
480                 },
481                 
482                 // Dummyを取得
483                 getDummy: function(t) {
484
485                         var id = this.getId(t);
486                         var dummy = $('.tagget_dummy' + id);
487                         return dummy;
488                         
489                 },
490                 
491                 // textareaのstyleをdummyにコピー
492                 adjust: function(t) {
493
494                         var dummy = this.getDummy(t);
495
496                         if (window.getComputedStyle) {
497                                 
498                                 var org = getComputedStyle(t,'');
499
500                                 var props = [
501                                         'width', 'height',
502                                         'padding-left', 'padding-right', 'padding-top', 'padding-bottom', 
503                                         'border-left-style', 'border-right-style','border-top-style','border-bottom-style', 
504                                         'border-left-width', 'border-right-width','border-top-width','border-bottom-width', 
505                                         'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
506                                 ];
507                         
508                             for(var i = 0; i < props.length; i++){
509                             
510                                 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
511                                                 return m1.toUpperCase();
512                                         });
513                             
514                                 dummy.css(capitalized, org.getPropertyValue(props[i]));
515
516                                 }
517
518                                 var offset = Util.getOffset(t);
519
520                             dummy.css({
521                                 left: offset.left,
522                                 top: offset.top
523                             });
524                             
525                             var $t = $(t);
526                             dummy.width($t.width())
527                                 .height($t.height())
528                                         .scrollLeft($t.scrollLeft())
529                                 .scrollTop($t.scrollTop());
530
531                         }
532                 },
533                 
534                 // suggestionを表示
535                 // TODO: 候補数制限 or 高さ設定してスクロール
536                 showPopup: function(t) {
537         
538                         var popup = this.getPopup(t);
539                         var suggests = Suggester.get(t, Cursor.getText(t)[0]);
540                         
541                         if (suggests) {
542                         
543                                 // 変化があった場合のみ再構成
544                                 if (popup.text() != suggests.view.join('')) {
545                         
546                                         popup.html('');
547                         
548                                         for(var i = 0; i < suggests.view.length; i++) {
549                                                 var li = $('<li></li>').attr('title', suggests.insert[i])
550                                                         .append('<a href="#"></a>').text(suggests.view[i])
551                                                         .hover(function() {
552                                                                 popup.children('li').removeClass('tagget_current');
553                                                                 $(this).addClass('tagget_current');
554                                                         })
555                                                         .click(function() {
556                                                                 Cursor.insert(t, Util.unescapeHtml(popup.children('li.tagget_current').attr('title')));
557                                                                 popup.hide();
558                                                         });
559                                                 if (i == 0) {
560                                                         li.addClass('tagget_current');
561                                                 }
562                                                 popup.append(li);
563                                         }       
564
565                                 // 変化がなければ属性のみ変更
566                                 // TODO:入力分の削除をincertに現在の文字渡してやるようにした方がよさそう。
567                                 } else {
568
569                                         popup.children('li').each(function(i) {
570                                                 $(this).attr('title', suggests.insert[i]);
571                                         });                     
572                                 }
573                                 
574                                 var pos = Cursor.getPos(t);
575                                 
576                                 popup.css({
577                                         left: pos.x,
578                                         top: pos.y
579                                 });
580
581                                 popup.show();
582                                 
583                         } else {
584                         
585                                 popup.hide();
586                                 
587                         }       
588                 },
589                 
590                 // popupを取得
591                 getPopup: function(t) {
592                         var id = this.getId(t);
593                         var popup = $('.tagget_popup' + id);
594                         return popup;
595                 },
596                 
597                 
598                 // suggest選択
599                 // TODO:もっとちゃんと書く
600                 choice: function(t, d) {
601
602                         var popup = this.getPopup(t);
603                         var lis = popup.children('li');
604                 
605                         if(d == 1) {
606                                 
607                                 for(var i = 0; i < lis.length; i++) {
608                                 
609                                         var li = lis.eq(i);
610                                         
611                                         if(li.hasClass('tagget_current')) {
612                                                 li.removeClass('tagget_current');
613                                                 i = (i == 0) ? lis.length - 1 : i - 1;
614                                                 lis.eq(i).addClass('tagget_current');
615                                                 break;
616                                         }
617                                 
618                                 }
619                         } else {
620                                 
621                                 for(var i = 0; i < lis.length; i++) {
622                                 
623                                         var li = lis.eq(i);
624                                         
625                                         if(li.hasClass('tagget_current')) {
626                                                 li.removeClass('tagget_current');
627                                                 i = (i == lis.length - 1 ) ? 0 : i + 1;
628                                                 lis.eq(i).addClass('tagget_current');
629                                                 break;
630                                         }
631                                 
632                                 }                       
633                         
634                         }
635
636                 },
637                 
638                 // 選択中のsuggestionを取得
639                 current: function(t) {
640                 
641                         var popup = this.getPopup(t);
642                         return popup.children('li.tagget_current').attr('title');
643                 },
644                 
645                 getStatus: function(t) {
646                         return $(t).parents('.tagget_wrapper').find('.tagget_status');
647                 },
648                 
649                 setLine: function(t) {
650                 
651                         var status = this.getStatus(t);
652                         
653                         //カーソル前の全文字列
654                         var s = Cursor.getText(t, /^[\s\S]*$/)[0] || '';
655                         
656                         //現在の行の行頭までの文字列
657                         //.は改行にはマッチしないので楽
658                         var thisLine = Cursor.getText(t, /.*$/)[0];
659                         
660                         var lineNum = (s.match(/\n/g) || []).length + 1;
661                         var cols = thisLine.length;
662
663                         status.children('.tagget_line').html('line: ' + lineNum + ' col: ' + cols);
664                 
665                 }
666                 
667         
668         };
669
670     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
671         /**
672          * textarea内のテキストを操作
673          */
674         Cursor = {
675
676                 // カーソル位置の文字を取得
677                 getText: function(t, r, after) {
678
679 //                      if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
680                         if (!r) {
681                                 // 補完対象
682                                 // 変な記号で始まらない文字列
683                                 // <で始まる文字列(タグ)
684                                 r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
685                         }
686                         
687                         var start, end;
688
689                         // IE
690                         if (document.selection) {
691
692 //                              t.focus(); // focusなしでもいける
693
694                                 // 選択範囲を取得
695                                 var range = document.selection.createRange();
696
697                                 // 選択範囲の複製を作成
698                                 var clone = range.duplicate();
699
700                                 // textarea内のテキスト全体を選択
701                                 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
702                                 clone.moveToElementText(t);
703
704                                 // cloneの選択範囲終点を、rangeの終点にあわせる
705                                 // [clone start] text1 [range start] text2 [range/clone end] text3
706                                 clone.setEndPoint('EndToEnd', range);
707
708                                 // 選択範囲始点を求める
709                                 // [clone start] text1 [range start] text2 [range/clone end] text3
710                                 // --------------------------------------------------------- clone.text.length == end
711                                 //                     ------------------------------------- range.text.length
712                                 // -------------------- clone.text.length - range.text.length = start
713                                 start = clone.text.length - range.text.length;
714                                 end = clone.text.length;
715
716                         }
717
718                         // Firefox
719                     else if ('selectionStart' in t) {
720
721                         start = t.selectionStart;
722                         end = t.selectionEnd;
723
724                     }
725
726                         var text;
727                         
728                         if (!after) {
729                                 text = t.value.slice(0, start).match(r);
730                         } else {
731                                 text = t.value.slice(end).match(r);
732                         }
733                         
734                         return text || [];
735                         
736                 },
737
738                 // カーソルの座標を取得
739                 getPos: function(t) {
740
741                         var x, y;
742
743                         if (document.selection) {
744                         
745                                 var range = document.selection.createRange();
746                                 x = range.offsetLeft + 
747                                         (document.body.scrollLeft || document.documentElement.scrollLeft) - 
748                                                 document.documentElement.clientLeft;
749                                 y = range.offsetTop + 
750                                         (document.body.scrollTop || document.documentElement.scrollTop) - 
751                                                 document.documentElement.clientTop;
752                         
753                         } else if (window.getComputedStyle) {
754
755                                 var id = Wrapper.getId(t);
756                                 var dummy = $('.tagget_dummy' + id);
757
758                                 var span = dummy.children('span');
759                                 
760                                 if(!span.is('span')) {
761                                         span = $('<span></span>');
762                                         span.html('|');
763                                 }
764                 
765                                 dummy.html('');
766                                 dummy.text(t.value.slice(0, t.selectionEnd));
767                                 dummy.append(span);
768
769                                 var offset = Util.getOffset(span.get(0));
770
771                                 x = offset.left - t.scrollLeft;
772                                 y = offset.top - t.scrollTop;
773                     }
774
775                         return {
776                                 x: x,
777                                 y: y
778                         };
779                 },
780                 
781                 // textareaのカーソル位置に文字列挿入
782                 insert: function(t, s) {
783
784                         // カーソル移動位置(#{c})を取得後、削除
785                         var cursor = s.indexOf('#{c}');
786                         s = s.replace('#{c}', '');
787
788                         // focusしないとIEでbodyに挿入されたりする
789                         // Firefoxでもボタンで挿入後にfocusが戻らない
790                         t.focus(); 
791
792                         // for IE
793                         if (document.selection) {
794                                 
795                                 // 選択範囲を取得
796                                 var range = document.selection.createRange();
797
798                                 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
799                                 range.text = s;
800
801                                 // カーソルがrange.textの最後になるので戻す
802                                 // #{c}指定がなければ最後のまま
803                                 var back = s.length - (cursor != -1 ? cursor : s.length);
804                                 range.move('character', -back);
805
806                                 // 現在のカーソル位置を反映する(これやらないと水の泡)
807                                 range.select();
808                         }
809
810                         // Firefox
811                         // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
812                     else if ('selectionStart' in t) { 
813
814                                 // スクロールバーの位置を保存
815                                 var top = t.scrollTop;
816
817                                 // 選択範囲の開始・終了位置を取得
818                         var start = t.selectionStart;
819                         var end = t.selectionEnd;
820
821                                 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
822                         t.value = t.value.slice(0, start) + s + t.value.slice(end);
823
824                                 // カーソル移動位置に移動させる
825                                 var index = start + (cursor != -1 ? cursor : s.length);
826                         t.setSelectionRange(index, index);
827
828                                 // 改行がたくさんある場合スクロールバーを下にずらす
829                                 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
830                                         top += parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10);
831                                 }
832                                 
833                                 // スクロールバーを戻す
834                             t.scrollTop = top;
835                     }
836
837                         return this;
838                         
839                 },
840         
841                 /**
842                  * 渡された関数で選択範囲を変換
843                  */
844                 // TODO:変換後も選択した状態に
845                 encodeSelection: function(t, func) {
846                 
847                         t.focus(); 
848
849                         // IE
850                         if (document.selection) {
851                         
852                                 var range = document.selection.createRange();
853                                 range.text = func(range.text);
854                                 range.select();
855                                 
856                         } else if ('selectionStart' in t) { 
857
858                                 var top = t.scrollTop;
859
860                         var start = t.selectionStart;
861                         var end = t.selectionEnd;
862
863                                 var before = t.value.slice(start, end);
864                                 var after = func(before);
865                                 
866
867                                 t.value = t.value.slice(0, start) + 
868                                         after + t.value.slice(end);
869
870                         t.setSelectionRange(end, end);
871
872                             t.scrollTop = top;
873                     }
874                     
875                 },
876                 
877                 /**
878                  * 閉じタグ補完
879                  */
880                 // カーソル位置より前にある開始タグをさかのぼって見ていく
881                 // 閉じタグが何もなければ直近のタグを閉じる
882                 // 閉じタグとマッチしないタグがあれば閉じる
883                 // カーソル後に閉じタグがあるとかは無視
884                 // 整形式じゃないのも無視
885                  closeTag: function(t) {
886
887                                 // カーソルより前の開始タグ一覧
888                                 // <! <? </ 空要素/>は除く
889                                 // TODO:できれば一文でやりたい
890                                 var sTags = Cursor.getText(t, /^[\s\S]*$/)[0]
891                                         .replace(/<[!\?\/][\s\S]*?>/g, '')      // <!?/を除去
892                                         .replace(/<[^>]*?\/>/g, '')             // 空要素/>を除去
893                                         .match(/<[^>]+?>/g);                            // タグ抽出
894                                 var eTags = Cursor.getText(t, /<\/[^?]+?>/g);
895                                 
896                                 if (sTags) {
897                                 
898                                         var i = sTags.length - 1;
899                                                 
900                                         for (; i >= 0 && eTags.length > 0; i--) {
901                                         
902                                                         // <までとタグ名以降を除去
903                                                         // >と\s始まり(属性と閉じ括弧)を除去
904                                                         // TODO:もうちょっと改善すれば改行された要素にも対応できる?
905                                                         var sTag = sTags[i].replace(/^.*<|[\s>].*/g, '');
906
907                                                         var j = 0;
908                                                         for (; j < eTags.length; j++) {
909
910                                                                 var eTag = eTags[j].replace(/^.*<\/|[\s>].*/g, '');
911
912                                                                 // マッチしたらその閉じタグを配列から消して除外
913                                                                 if (sTag == eTag) {
914                                                                         break;
915                                                                 }
916                                                         }
917
918                                                         if (j < eTags.length) {
919                                                                 eTags.splice(j, 1);
920                                                                 
921                                                         // マッチしなかったらその開始タグを閉じる
922                                                         } else {
923                                                                 break;
924                                                         }
925
926                                         }
927
928                                         // 現在の開始タグを閉じる
929                                         // 全てマッチしたら何もしない
930                                         if (i >= 0) {
931                                                 Cursor.insert(t, '</' + sTags[i].replace(/^.*<|[\s>].*/g, '') + '>');
932                                         }
933
934                                 }
935
936                 }
937         
938         };
939
940
941     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
942         /*
943          * contextに依存しない関数群
944          */
945         Util = {
946         
947                 // &, <, >[, "]を変換
948                 escapeHtml: function(s, quot) {
949                         s = s.replace(/&/g, '&amp;')
950                                 .replace(/</g, '&lt;')
951                                 .replace(/>/g, '&gt;');
952                         
953                         return !quot ? s : s.replace(/"/g, '&quot;');
954                 },
955                 
956                 // &amp;, &lt;, &gt;[, &quot;]を戻す
957                 unescapeHtml:function(s, quot) {
958                                                 
959                         s = s.replace(/&amp;/g, '&')
960                                 .replace(/&lt;/g, '<')
961                                 .replace(/&gt;/g, '>');
962                         
963                         return !quot ? s : s.replace(/&quot;/g, '"');
964                 },
965
966                 // offset(要素の実xy座標)を簡易算出
967                 getOffset: function(elm) {
968
969                         var left, top;
970
971                         if (elm.getBoundingClientRect) {
972
973                                 var rect = elm.getBoundingClientRect();
974                                 left = Math.round(scrollX + rect.left);
975                                 top = Math.round(scrollY + rect.top);
976                                 
977                         } else {
978
979                                 left = elm.offsetLeft;
980                                 top  = elm.offsetTop;
981                                 var offsetParent = elm.offsetParent;
982                                 
983                                 while (offsetParent) {
984                                         left += offsetParent.offsetLeft;
985                                         top  += offsetParent.offsetTop;             
986                                         offsetParent = offsetParent.offsetParent;
987                                 }
988                                 
989                         }
990
991                         return {
992                                 left: left,
993                                 top: top
994                         };
995                         
996                 }
997         };
998
999     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1000         /**
1001          * Cookieに下書き保存
1002          */
1003         var Cookie = {
1004         
1005                 load: function(key) {
1006                 
1007                         var cookie = $.cookie(key);
1008                         if (cookie) {
1009                                 return cookie;
1010                         }
1011                 
1012                 },
1013                 
1014                 zip: function(s) {
1015
1016                 var data = utf16to8(s);
1017                 data = zip_deflate(data);
1018                 data = base64encode(data);
1019                         return data;
1020                 
1021                 },
1022
1023                 unzip: function(s) {
1024
1025                     var data = base64decode(s);
1026                     data = zip_inflate(data);
1027                     data = utf8to16(data);
1028                         return data;
1029                 
1030                 },
1031                 
1032                 save: function(key, val) {
1033
1034                         var size = val.length;
1035                 
1036                         if(size <= 4000) {
1037                                 $.cookie(key, val, { 'expires': 1 });
1038                         }
1039                 
1040                 }
1041         
1042         
1043         };
1044
1045
1046     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1047         // tagget初期化
1048         var init = function(t, i) {
1049         
1050                 // 被らないコードを振る
1051                 while ($('textarea').is('.tagget_' + i)) {
1052                         i = '0' + i;
1053                 }
1054                 $(t).addClass('tagget_' + i);
1055
1056                 Wrapper.wrap(t);
1057                 Wrapper.absolutes(t);
1058
1059 //              setTimeout(function() {
1060 //              
1061 //              });
1062
1063                 // イベント設定
1064
1065                 // keyup(発生タイミングが一番少ない)で候補表示
1066                 $(t).keyup(function(e) {
1067                 
1068                         // 十字キー、Enterの時は補完を表示しない
1069                         if(!(37 <= e.which && e.which <= 40) && !(e.which == 13)) {
1070                                 Wrapper.showPopup(t);
1071                         } else if(e.which == 37 || e.which == 39) {
1072                                 Wrapper.getPopup(t).hide();
1073                         }
1074                         
1075                         Wrapper.setLine(t);
1076                 })
1077
1078                 // 入力キーに応じた処理               
1079                 .keydown(function(e) {
1080                 
1081                         // タブキャンセル
1082                         // keydown以外だとうまくいかない
1083                         if (e.which == 9) {
1084                                 Cursor.insert(t, '\t');
1085                                 return false;
1086                         }
1087
1088                         // Shift+Enterで改行 or <br />入力
1089                         // 補完却下も可能
1090                         if (e.shiftKey && e.which == 13) {
1091
1092                                 var n = '\n';
1093                                 if (!Wrapper.isPopup(t) && Wrapper.checkType(t, 'html')) {
1094                                         n = '<br />';
1095                                 }
1096                                 Cursor.insert(t, n);
1097                                 
1098                                 return false;
1099                         }
1100
1101                         // Ctrl+Enterで閉じタグ補完
1102                         if (e.ctrlKey && e.which == 13) {
1103
1104                                 Cursor.closeTag(t);
1105                         
1106                                 return false;
1107                         }
1108
1109                         // 十字キーで候補選択
1110                         // upだとおしっぱにできない、pressだとおかしくなる
1111                         if (Wrapper.isPopup(t)) {
1112
1113
1114                                 switch (e.which) {
1115                                 
1116                                         // right
1117 //                                      case 37:
1118
1119                                         // up
1120                                         case 38:
1121
1122                                                 Wrapper.choice(t, 1);
1123                                                 return false;
1124
1125                                         // left
1126 //                                      case 39:
1127
1128                                         // down
1129                                         case 40:
1130
1131                                                 Wrapper.choice(t, -1);
1132                                                 return false;
1133
1134                                         // Enterで補完
1135                                         case 13:
1136                                                 Cursor.insert(t, Util.unescapeHtml(Wrapper.current(t)));
1137                                                 Wrapper.getPopup(t).hide();
1138
1139                                                 return false;
1140                                 }
1141
1142                         } else {
1143                 
1144                                 // インデントを合わせる       
1145                                 if (e.which == 13) {
1146
1147                                         var indent = Cursor.getText(t, /^[\t ]*/mg);
1148                                         Cursor.insert(t, '\n' + (indent ? indent[indent.length - 1] : ''));
1149
1150                                         var $t = $(t);
1151                                         if (window.getComputedStyle) {
1152                                                 $t.scrollTop($t.scrollTop() + parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10));
1153                                         }
1154                                         
1155                                         return false;
1156                         
1157                                 }                       
1158                         
1159                         }
1160
1161                 });
1162
1163                 // 最初に1回だけ呼び出し。
1164                 var data = Cookie.load(Wrapper.getId(t));
1165                 if (data) {
1166                         t.value = Cookie.unzip(data);
1167                 }
1168                 Wrapper.setLine(t);
1169         
1170         }; // init
1171         
1172
1173     /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1174         /**
1175          * jqueryにプラグイン追加
1176          * $('textarea.tagget').tagget()とかで呼び出し
1177          */
1178         $.fn.tagget = function(conf) {
1179
1180
1181                 // 設定オブジェクト
1182                 conf = $.extend({
1183                         toolbar: true, // ツールバー表示
1184                         cookie: true  // クッキーに保存
1185                 }, conf);
1186
1187                 // thisには$('textarea.tagget')が入ってくる
1188                 this.each(function(i) {
1189
1190                                 // textareaを初期化
1191                                 init(this, i);
1192
1193                 });
1194
1195                 // window全体のイベント設定
1196                 
1197                 var self = this;                
1198                 // リサイズ時にDummyを調整
1199                 $(window).resize(function() {
1200
1201                         self.each(function() {
1202                                 Wrapper.adjust(this);
1203                         });
1204
1205                 }).resize();
1206                 
1207                 // Cookie保存設定
1208                 var interval = 60000; // 60秒:1分
1209                 setTimeout(function timer() {
1210
1211                         self.each(function() {
1212
1213                                 Cookie.save(Wrapper.getId(this), Cookie.zip(this.value));
1214
1215                                 var fillZero = function(s) {
1216                                     return ('0' + s).slice(-2); 
1217                                 };
1218                                 var status = Wrapper.getStatus(this);   
1219                                 var date = new Date();
1220                                 var y = date.getFullYear();
1221                                 var m = fillZero(date.getMonth() + 1);
1222                                 var d = fillZero(date.getDate());
1223                                 var h = fillZero(date.getHours());
1224                                 var min = fillZero(date.getMinutes());
1225                                 var sec = fillZero(date.getSeconds());
1226                                 status.children('.tagget_time')
1227                                         .html('Draft Saved At ' + 
1228                                                 y + '/' + m + '/' + d + '/' + ' ' + h + ':' + min + ':' + sec);
1229                                 
1230                                 setTimeout(timer, interval);
1231                         });             
1232                 }, interval);
1233                         
1234                         
1235                 // This is jQuery!!
1236                 return this;
1237                 
1238         }; // $.fn.tagget
1239
1240
1241 })(jQuery);