OSDN Git Service

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