2 * tagget: The Simple HTML Editor (jQuery Plugin)
6 * Licensed under the MIT license.
7 * Copyright (c) 2009 tagget.org
13 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
17 * 入力補完候補を保持、取得するオブジェクト
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',
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',
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',
46 '<head>#{c}</head>\n',
47 '<title>#{c}</title>\n',
48 '<body>#{c}</body>\n',
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'
58 '<address>#{c}</address>',
73 '<ul>\n<li>#{c}</li>\n</ul>\n',
74 '<ol>\n<li>#{c}</li>\n</ol>\n',
76 '<dl>\n<dt>#{c}</dt>\n<dd></dd>\n</dl>\n',
80 '<table>\n#{c}\n</table>\n',
85 '<strong>#{c}</strong>',
88 '<form>\n#{c}\n</form>',
89 '<fieldset>#{c}</fieldset>',
90 '<input type="#{c}" />',
113 'onmouseover="#{c}"',
138 'http://www.w3.org/1999/xhtml'
141 return header.concat(body, attributes, values);
148 'function() { #{c} }',
157 return objs.concat(libs);
183 'background-color: ',
184 'background-repeat: ',
198 return props.concat(values);
212 return elm.concat(attr);
224 return prop.concat(val);
235 add: function(newwords) {
237 for(var type in newwords) {
239 if (this.keywords[type]) {
240 this.keywords[type].concat(newwords[type]);
242 this.keywords[type] = newwords[type];
252 get: function(t, s) {
254 if (!t || !s || !t.value) {
266 if (Wrapper.checkIntelli(t)) {
270 var a = t.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]{2,}/g) || [];
274 for (var i = 0; i < a.length; i++) {
287 for(var key in this.keywords) {
288 if (Wrapper.checkType(t, key)) {
289 words = words.concat(this.keywords[key]);
293 for(var i = 0; i < words.length; i++) {
295 if (words[i] != s && words[i].indexOf(s) == 0) {
296 matches.view.push(words[i].replace(/#\{c\}/g, ''));
298 matches.insert.push(words[i].slice(s.length));
302 return (matches.view.length != 0) ? matches : false;
308 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
310 * taggetに必要なHTMLを生成・操作する
318 // textarea周囲にHTMLを追加する
321 if ($(t).parents().is('.tagget_wrapper')) {
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');
332 if (this.isToolbar) {
334 wrapper.prepend(('<div class="tagget_toolbar"></div>')).children('div.tagget_toolbar');
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;</option>')
342 .append('<option value="raw">&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>')
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="全て置換" />')
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>')
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>')
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>')
387 // Draft Saved At 1:17とか表示
388 // 行、列を表示(line: , col:)
390 $('<p class="tagget_status"><span class="tagget_time"></span><span class="tagget_line"></span> </p>')
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');
404 getToolbar: function(t) {
406 return $(t).parents('.tagget_wrapper').children('.tagget_toolbar');
410 // wrap要素にeventを設定する。
411 // 既にHTMLがあった場合は、こちらだけを行う。
412 relate: function(t) {
414 var toolbar = this.getToolbar(t);
418 toolbar.find('.tagget_encode select').change(function() {
420 switch (this.value) {
423 Cursor.encodeSelection(t, Util.escapeHtml);
427 Cursor.encodeSelection(t, Util.unescapeHtml);
431 Cursor.encodeSelection(t, encodeURI);
435 Cursor.encodeSelection(t, encodeURIComponent);
439 Cursor.encodeSelection(t, decodeURI);
443 Cursor.encodeSelection(t, decodeURIComponent);
454 var inputs = toolbar.find('.tagget_replace input');
455 inputs.filter('[type=button]').click(function() {
459 var before = inputs.eq(0).val();
460 var after = inputs.eq(1).val();
461 // フラグはデフォルトでg(Replace All)
464 // TODO: 一応動くけどもっとみやすいコードに
466 // /before/gim形式で入力されたらフラグを抽出
468 if (before.match(/^\/.+\/([^\/]+)$/)) {
472 // 行頭の/、/以降の文字(フラグ、スラッシュ連続など形式外の入力)を除去
473 before = before.replace(/^\/|\/[^\/]+?$/g, '');
477 t.value = val.replace(new RegExp(before, flag), after);
487 return $(t).attr('class').match(/tagget_([0-9]+)/)[1];
491 isPopup: function(t) {
492 var popup = this.getPopup(t);
493 return popup.css('display') != 'none';
497 setPopup: function(display) {
498 var popup = this.getPopup(t);
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);
513 checkCookie: function(t) {
514 var cookie = $(t).parents('.tagget_wrapper')
515 .find('.tagget_cookie input').attr('checked');
520 checkIntelli: function(t) {
521 var intelli = $(t).parents('.tagget_wrapper')
522 .find('.tagget_intelli input').attr('checked');
527 absolutes: function(t) {
529 var body = $(document.body);
530 var id = this.getId(t);
533 var popup = $('<ul class="tagget_popup tagget_popup' + id + '"></ul>');
538 if (window.getComputedStyle) {
540 var dummy = $('<pre class="tagget_dummy tagget_dummy' + id + '"></pre>');
548 getDummy: function(t) {
550 var id = this.getId(t);
551 var dummy = $('.tagget_dummy' + id);
556 // textareaのstyleをdummyにコピー
557 adjust: function(t) {
559 var dummy = this.getDummy(t);
561 if (window.getComputedStyle) {
563 var org = getComputedStyle(t,'');
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'
573 for(var i = 0; i < props.length; i++){
575 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
576 return m1.toUpperCase();
579 dummy.css(capitalized, org.getPropertyValue(props[i]));
583 var offset = Util.getOffset(t);
591 dummy.width($t.width())
593 .scrollLeft($t.scrollLeft())
594 .scrollTop($t.scrollTop());
600 // TODO: 候補数制限 or 高さ設定してスクロール
601 showPopup: function(t) {
603 var popup = this.getPopup(t);
604 var suggests = Suggester.get(t, Cursor.getText(t)[0]);
609 if (popup.text() != suggests.view.join('')) {
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])
617 popup.children('li').removeClass('tagget_current');
618 $(this).addClass('tagget_current');
621 Cursor.insert(t, Util.unescapeHtml(popup.children('li.tagget_current').attr('title')));
625 li.addClass('tagget_current');
631 // TODO:入力分の削除をincertに現在の文字渡してやるようにした方がよさそう。
634 popup.children('li').each(function(i) {
635 $(this).attr('title', suggests.insert[i]);
639 var pos = Cursor.getPos(t);
656 getPopup: function(t) {
657 var id = this.getId(t);
658 var popup = $('.tagget_popup' + id);
665 choice: function(t, d) {
667 var popup = this.getPopup(t);
668 var lis = popup.children('li');
672 for(var i = 0; i < lis.length; i++) {
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');
686 for(var i = 0; i < lis.length; i++) {
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');
704 current: function(t) {
706 var popup = this.getPopup(t);
707 return popup.children('li.tagget_current').attr('title');
710 getStatus: function(t) {
711 return $(t).parents('.tagget_wrapper').find('.tagget_status');
714 setLine: function(t) {
716 var status = this.getStatus(t);
719 var s = Cursor.getText(t, /^[\s\S]*$/)[0] || '';
723 var thisLine = Cursor.getText(t, /.*$/)[0];
725 var lineNum = (s.match(/\n/g) || []).length + 1;
726 var cols = thisLine.length;
728 status.children('.tagget_line').html('line: ' + lineNum + ' col: ' + cols);
735 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
742 getText: function(t, r, after) {
744 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
749 r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
755 if (document.selection && !window.opera) {
757 // t.focus(); // focusなしでもいける
760 var range = document.selection.createRange();
763 var clone = range.duplicate();
765 // textarea内のテキスト全体を選択
766 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
767 clone.moveToElementText(t);
769 // cloneの選択範囲終点を、rangeの終点にあわせる
770 // [clone start] text1 [range start] text2 [range/clone end] text3
771 clone.setEndPoint('EndToEnd', range);
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;
784 else if ('selectionStart' in t) {
786 start = t.selectionStart;
787 end = t.selectionEnd;
794 text = t.value.slice(0, start).match(r);
796 text = t.value.slice(end).match(r);
804 getPos: function(t) {
808 if (document.selection && !window.opera) {
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;
818 } else if (window.getComputedStyle) {
820 var id = Wrapper.getId(t);
821 var dummy = $('.tagget_dummy' + id);
823 var span = dummy.children('span');
825 if(!span.is('span')) {
826 span = $('<span></span>');
831 dummy.text(t.value.slice(0, t.selectionEnd));
834 var offset = Util.getOffset(span.get(0));
836 x = offset.left - t.scrollLeft;
837 y = offset.top - t.scrollTop;
846 // textareaのカーソル位置に文字列挿入
847 insert: function(t, s) {
849 // カーソル移動位置(#{c})を取得後、削除
850 var cursor = s.indexOf('#{c}');
851 s = s.replace('#{c}', '');
853 // focusしないとIEでbodyに挿入されたりする
854 // Firefoxでもボタンで挿入後にfocusが戻らない
858 if (document.selection && !window.opera) {
861 var range = document.selection.createRange();
863 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
866 // カーソルがrange.textの最後になるので戻す
868 var back = s.length - (cursor != -1 ? cursor : s.length);
869 range.move('character', -back);
871 // 現在のカーソル位置を反映する(これやらないと水の泡)
876 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
877 else if ('selectionStart' in t) {
880 var top = t.scrollTop;
883 var start = t.selectionStart;
884 var end = t.selectionEnd;
886 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
887 t.value = t.value.slice(0, start) + s + t.value.slice(end);
890 var index = start + (cursor != -1 ? cursor : s.length);
891 t.setSelectionRange(index, index);
893 // 改行がたくさんある場合スクロールバーを下にずらす
894 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
895 top += parseInt(getComputedStyle(t, '').getPropertyValue('font-size'), 10) + 3;
910 encodeSelection: function(t, func) {
915 if (document.selection && !window.opera) {
917 var range = document.selection.createRange();
918 range.text = func(range.text);
921 } else if ('selectionStart' in t) {
923 var top = t.scrollTop;
925 var start = t.selectionStart;
926 var end = t.selectionEnd;
928 var before = t.value.slice(start, end);
929 var after = func(before);
932 t.value = t.value.slice(0, start) +
933 after + t.value.slice(end);
935 t.setSelectionRange(end, end);
945 // カーソル位置より前にある開始タグをさかのぼって見ていく
946 // 閉じタグが何もなければ直近のタグを閉じる
947 // 閉じタグとマッチしないタグがあれば閉じる
948 // カーソル後に閉じタグがあるとかは無視
950 closeTag: function(t) {
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);
963 var i = sTags.length - 1;
965 for (; i >= 0 && eTags.length > 0; i--) {
968 // >と\s始まり(属性と閉じ括弧)を除去
969 // TODO:もうちょっと改善すれば改行された要素にも対応できる?
970 var sTag = sTags[i].replace(/^.*<|[\s>].*/g, '');
973 for (; j < eTags.length; j++) {
975 var eTag = eTags[j].replace(/^.*<\/|[\s>].*/g, '');
977 // マッチしたらその閉じタグを配列から消して除外
983 if (j < eTags.length) {
986 // マッチしなかったらその開始タグを閉じる
996 Cursor.insert(t, '</' + sTags[i].replace(/^.*<|[\s>].*/g, '') + '>');
1006 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1013 escapeHtml: function(s, quot) {
1014 s = s.replace(/&/g, '&')
1015 .replace(/</g, '<')
1016 .replace(/>/g, '>');
1018 return !quot ? s : s.replace(/"/g, '"');
1021 // &, <, >[, "]を戻す
1022 unescapeHtml:function(s, quot) {
1024 s = s.replace(/&/g, '&')
1025 .replace(/</g, '<')
1026 .replace(/>/g, '>');
1028 return !quot ? s : s.replace(/"/g, '"');
1031 // offset(要素の実xy座標)を簡易算出
1032 getOffset: function(elm) {
1036 if (elm.getBoundingClientRect) {
1038 var rect = elm.getBoundingClientRect();
1039 left = Math.round(scrollX + rect.left);
1040 top = Math.round(scrollY + rect.top);
1044 left = elm.offsetLeft;
1045 top = elm.offsetTop;
1046 var offsetParent = elm.offsetParent;
1048 while (offsetParent) {
1049 left += offsetParent.offsetLeft;
1050 top += offsetParent.offsetTop;
1051 offsetParent = offsetParent.offsetParent;
1063 fillZero: function(s) {
1064 return ('0' + s).slice(-2);
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());
1077 return y + '/' + m + '/' + d + '/' + ' ' + h + ':' + min + ':' + sec;
1083 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1089 load: function(key) {
1091 var cookie = $.cookie(key);
1100 var data = utf16to8(s);
1101 data = zip_deflate(data);
1102 data = base64encode(data);
1107 unzip: function(s) {
1109 var data = base64decode(s);
1110 data = zip_inflate(data);
1111 data = utf8to16(data);
1116 save: function(key, val) {
1118 var size = val.length;
1121 $.cookie(key, val, { 'expires': 1 });
1129 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1131 * textareaのEventに設定する関数群
1135 // keyupは発生タイミングが一番少ない
1137 keyup: function(e) {
1141 // 十字キー、Enterの時は補完を表示しない
1142 if(!(37 <= e.which && e.which <= 40) && !(e.which == 13)) {
1143 Wrapper.showPopup(t);
1147 else if(e.which == 37 || e.which == 39) {
1148 Wrapper.getPopup(t).hide();
1155 // press,upだとおかしくなるものもこっちで
1156 keydown: function(e) {
1161 // keydown以外だとうまくいかない
1163 Cursor.insert(t, '\t');
1167 // Shift+Enterで改行 or <br />入力
1168 if (e.shiftKey && e.which == 13) {
1171 if (Wrapper.checkType(t, 'html')) {
1174 Cursor.insert(t, n);
1181 // Ctrl+Enterで閉じタグ補完
1182 if (e.ctrlKey && e.which == 13) {
1184 if (Wrapper.checkType(t, 'html')) {
1194 // upだとおしっぱにできない、pressだとおかしくなる
1195 if (Wrapper.isPopup(t)) {
1206 Wrapper.choice(t, 1);
1215 Wrapper.choice(t, -1);
1220 Cursor.insert(t, Util.unescapeHtml(Wrapper.current(t)));
1221 Wrapper.getPopup(t).hide();
1229 if (e.which == 13) {
1231 var indent = Cursor.getText(t, /^[\t ]*/mg);
1232 Cursor.insert(t, '\n' + (indent ? indent[indent.length - 1] : ''));
1234 // TODO:もっとちゃんとスクロールの高さ直す
1236 if (window.getComputedStyle) {
1237 $t.scrollTop($t.scrollTop() + parseInt(getComputedStyle(t, '').getPropertyValue('font-size'), 10) + 3);
1251 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1253 var init = function(t, i) {
1256 while ($('textarea').is('.tagget_' + i)) {
1259 $(t).addClass('tagget_' + i);
1262 Wrapper.absolutes(t);
1266 // keyup(発生タイミングが一番少ない)で候補表示
1267 $(t).keyup(Event.keyup)
1270 .keydown(Event.keydown);
1273 var data = Cookie.load(Wrapper.getId(t));
1275 t.value = Cookie.unzip(data);
1282 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1285 * $('textarea.tagget').tagget()とかで呼び出し
1287 $.fn.tagget = function(conf) {
1292 toolbar: true, // ツールバー表示
1293 cookie: true // クッキーに保存
1296 // thisには$('textarea.tagget')が入ってくる
1297 this.each(function(i) {
1308 $(window).resize(function() {
1310 self.each(function() {
1311 Wrapper.adjust(this);
1317 var interval = 60000; // 60秒:1分
1318 setTimeout(function timer() {
1320 self.each(function() {
1322 if (Wrapper.checkCookie(this)) {
1324 Cookie.save(Wrapper.getId(this), Cookie.zip(this.value));
1326 var status = Wrapper.getStatus(this);
1327 status.children('.tagget_time').html('Draft Saved At ' + Util.now());
1331 setTimeout(timer, interval);