OSDN Git Service

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