OSDN Git Service

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