2 * tagget: The Simple HTML Editor (jQuery Plugin)
6 * Licensed under the MIT license.
7 * Copyright (c) 2009 tagget.org
13 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
17 * 入力補完候補を保持、取得するオブジェクト
31 '<?xml version="1.0" encoding="#{c}"?>\n',
32 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
33 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n',
34 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n',
36 '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">\n',
37 '<link rel="stylesheet" type="text/css" href="#{c}" />\n',
38 '<script type="text/javascript" src="#{c}"></script>\n',
39 '<script type="text/javascript">\n#{c}\n</script>\n',
40 '<style type="text/css">\n#{c}\n</style>\n',
42 '<meta http-equiv="Content-Type" content="text/html; charset=#{c}" />\n',
43 '<meta http-equiv="Content-Style-Type" content="text/css" />\n',
44 '<meta http-equiv="Content-Script-Type" content="text/javascript" />\n',
45 '<meta name="ROBOTS" content="#{c}" />\n',
46 '<meta name="description" content="#{c}" />\n',
47 '<meta name="keywords" content="#{c}" />\n',
49 '<head>#{c}</head>\n',
50 '<title>#{c}</title>\n',
51 '<body>#{c}</body>\n',
53 '<link rel="alternate" type="application/atom+xml" title="#{c}" href="" />\n',
54 '<link rel="alternate" type="application/rss+xml" title="#{c}" href="" />\n',
55 '<link rel="EditURI" type="application/rsd+xml" title="#{c}" href="" />\n'
61 '<address>#{c}</address>',
75 '<ul>\n<li>#{c}</li>\n</ul>\n',
76 '<ol>\n<li>#{c}</li>\n</ol>\n',
78 '<dl>\n<dt>#{c}</dt>\n<dd></dd>\n</dl>\n',
82 '<strong>#{c}</strong>',
85 '<form>\n#{c}\n</form>',
86 '<input type="#{c}" />',
102 'onmouseover="#{c}"',
127 'http://www.w3.org/1999/xhtml'
130 return header.concat(body, attributes, values);
137 'function() { #{c} }',
146 return objs.concat(libs);
171 return props.concat(values);
183 add: function(newwords) {
185 for(var type in newwords) {
187 if (this.keywords[type]) {
188 this.keywords[type].concat(newwords[type]);
190 this.keywords[type] = newwords[type];
200 get: function(t, s) {
202 if (!t || !s || !t.value) {
214 if (Wrapper.checkIntelli(t)) {
218 var a = t.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]{2,}/g) || [];
222 for (var i = 0; i < a.length; i++) {
235 for(var key in this.keywords) {
236 if (Wrapper.checkType(t, key)) {
237 words = words.concat(this.keywords[key]);
241 for(var i = 0; i < words.length; i++) {
243 if (words[i] != s && words[i].indexOf(s) == 0) {
244 matches.view.push(words[i].replace(/#\{c\}/g, ''));
246 matches.insert.push(words[i].slice(s.length));
250 return (matches.view.length != 0) ? matches : false;
256 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
258 * taggetに必要なHTMLを生成・操作する
266 // textarea周囲にHTMLを追加する
269 if ($(t).parents().is('.tagget_wrapper')) {
275 // wrapの戻り値はwrapされた要素なので、parentsでwrapした要素を取得
276 var wrapper = $(t).wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
277 .parents('div.tagget_wrapper');
280 if (this.isToolbar) {
282 wrapper.prepend(('<div class="tagget_toolbar"></div>')).children('div.tagget_toolbar');
286 $('<p class="tagget_encode"></p>').append(
287 $('<select title="選択範囲を変換"></select>')
288 .append('<option selected="selected" value="">Encode Selection</option>')
289 .append('<option value="entity">& < > → &amp; &lt; &gt;</option>')
290 .append('<option value="raw">&amp; &lt; &gt; → & < ></option>')
291 .append('<option value="enc">encodeURI()</option>')
292 .append('<option value="encc">encodeURIComponent()</option>')
293 .append('<option value="dec">decodeURI()</option>')
294 .append('<option value="decc">decodeURIComponent()</option>')
300 $('<p class="tagget_replace"></p>')
301 .append('<input type="text" value="Before" title="置換前" />')
302 .append('<img src="img/v_arrow010102.gif" />')
303 .append('<input type="text" value="After" title="置換後" />')
304 .append('<input type="button" value="Replace All" title="全て置換" />')
309 $('<p class="tagget_type"></p>').append(
310 $('<select title="選択範囲を変換"></select>')
311 .append('<option selected="selected" value="html|css|js">HTML,CSS,JS</option>')
312 .append('<option value="html|css">HTML,CSS</option>')
313 .append('<option value="html">HTML</option>')
314 .append('<option value="css">CSS</option>')
315 .append('<option value="js">JS</option>')
316 .append('<option value="css|js">CSS,JS</option>')
317 .append('<option value="html|js">HTML,JS</option>')
323 $('<p class="tagget_cookie"></p>')
324 .append('<input type="checkbox" checked="checked" title="Cookieに下書きを保存" id="tagget_check_cookie" /><label for="tagget_check_cookie" title="Cookieに下書きを保存">Cookie</label>')
330 $('<p class="tagget_intelli"></p>')
331 .append('<input type="checkbox" checked="checked" title="入力内容から補完" id="tagget_check_intelli" /><label for="tagget_check_intelli" title="入力内容から補完">intelligent</label>')
335 // Draft Saved At 1:17とか表示
336 // 行、列を表示(line: , col:)
338 $('<p class="tagget_status"><span class="tagget_time"></span><span class="tagget_line"></span> </p>')
341 // jQuery UI CSS Frameworkのclass名を設定
342 toolbar.addClass('ui-helper-clearfix');
343 wrapper.addClass('ui-widget-header ui-corner-all')
344 .children('div, p').addClass('ui-widget-header');
353 // wrap要素にeventを設定する。
354 // 既にHTMLがあった場合は、こちらだけを行う。
355 relate: function(t) {
357 var toolbar = $(t).parents('.tagget_wrapper').children('.tagget_toolbar');
361 toolbar.find('.tagget_encode select').change(function() {
363 switch (this.value) {
366 Cursor.encodeSelection(t, Util.escapeHtml);
370 Cursor.encodeSelection(t, Util.unescapeHtml);
374 Cursor.encodeSelection(t, encodeURI);
378 Cursor.encodeSelection(t, encodeURIComponent);
382 Cursor.encodeSelection(t, decodeURI);
386 Cursor.encodeSelection(t, decodeURIComponent);
397 var inputs = toolbar.find('.tagget_replace input');
398 inputs.filter('[type=button]').click(function() {
402 var before = inputs.eq(0).val();
403 var after = inputs.eq(1).val();
404 // フラグはデフォルトでg(Replace All)
407 // TODO: 一応動くけどもっとみやすいコードに
409 // /before/gim形式で入力されたらフラグを抽出
411 if (before.match(/^\/.+\/([^\/]+)$/)) {
415 // 行頭の/、/以降の文字(フラグ、スラッシュ連続など形式外の入力)を除去
416 before = before.replace(/^\/|\/[^\/]+?$/g, '');
420 t.value = val.replace(new RegExp(before, flag), after);
430 return $(t).attr('class').match(/tagget_([0-9]+)/)[1];
434 isPopup: function(t) {
435 var popup = this.getPopup(t);
436 return popup.css('display') != 'none';
440 setPopup: function(display) {
441 var popup = this.getPopup(t);
449 checkType: function(t, type) {
450 var currentType = $(t).parents('.tagget_wrapper')
451 .find('.tagget_type select').val();
452 return (new RegExp(currentType)).test(type);
455 checkIntelli: function(t) {
456 var intelli = $(t).parents('.tagget_wrapper')
457 .find('.tagget_intelli input').attr('checked');
462 absolutes: function(t) {
464 var body = $(document.body);
465 var id = this.getId(t);
468 var popup = $('<ul class="tagget_popup tagget_popup' + id + '"></ul>');
473 if (window.getComputedStyle) {
475 var dummy = $('<pre class="tagget_dummy tagget_dummy' + id + '"></pre>');
483 getDummy: function(t) {
485 var id = this.getId(t);
486 var dummy = $('.tagget_dummy' + id);
491 // textareaのstyleをdummyにコピー
492 adjust: function(t) {
494 var dummy = this.getDummy(t);
496 if (window.getComputedStyle) {
498 var org = getComputedStyle(t,'');
502 'padding-left', 'padding-right', 'padding-top', 'padding-bottom',
503 'border-left-style', 'border-right-style','border-top-style','border-bottom-style',
504 'border-left-width', 'border-right-width','border-top-width','border-bottom-width',
505 'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
508 for(var i = 0; i < props.length; i++){
510 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
511 return m1.toUpperCase();
514 dummy.css(capitalized, org.getPropertyValue(props[i]));
518 var offset = Util.getOffset(t);
526 dummy.width($t.width())
528 .scrollLeft($t.scrollLeft())
529 .scrollTop($t.scrollTop());
535 // TODO: 候補数制限 or 高さ設定してスクロール
536 showPopup: function(t) {
538 var popup = this.getPopup(t);
539 var suggests = Suggester.get(t, Cursor.getText(t)[0]);
544 if (popup.text() != suggests.view.join('')) {
548 for(var i = 0; i < suggests.view.length; i++) {
549 var li = $('<li></li>').attr('title', suggests.insert[i])
550 .append('<a href="#"></a>').text(suggests.view[i])
552 popup.children('li').removeClass('tagget_current');
553 $(this).addClass('tagget_current');
556 Cursor.insert(t, Util.unescapeHtml(popup.children('li.tagget_current').attr('title')));
560 li.addClass('tagget_current');
566 // TODO:入力分の削除をincertに現在の文字渡してやるようにした方がよさそう。
569 popup.children('li').each(function(i) {
570 $(this).attr('title', suggests.insert[i]);
574 var pos = Cursor.getPos(t);
591 getPopup: function(t) {
592 var id = this.getId(t);
593 var popup = $('.tagget_popup' + id);
600 choice: function(t, d) {
602 var popup = this.getPopup(t);
603 var lis = popup.children('li');
607 for(var i = 0; i < lis.length; i++) {
611 if(li.hasClass('tagget_current')) {
612 li.removeClass('tagget_current');
613 i = (i == 0) ? lis.length - 1 : i - 1;
614 lis.eq(i).addClass('tagget_current');
621 for(var i = 0; i < lis.length; i++) {
625 if(li.hasClass('tagget_current')) {
626 li.removeClass('tagget_current');
627 i = (i == lis.length - 1 ) ? 0 : i + 1;
628 lis.eq(i).addClass('tagget_current');
639 current: function(t) {
641 var popup = this.getPopup(t);
642 return popup.children('li.tagget_current').attr('title');
645 getStatus: function(t) {
646 return $(t).parents('.tagget_wrapper').find('.tagget_status');
649 setLine: function(t) {
651 var status = this.getStatus(t);
654 var s = Cursor.getText(t, /^[\s\S]*$/)[0] || '';
658 var thisLine = Cursor.getText(t, /.*$/)[0];
660 var lineNum = (s.match(/\n/g) || []).length + 1;
661 var cols = thisLine.length;
663 status.children('.tagget_line').html('line: ' + lineNum + ' col: ' + cols);
670 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
677 getText: function(t, r, after) {
679 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
684 r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
690 if (document.selection) {
692 // t.focus(); // focusなしでもいける
695 var range = document.selection.createRange();
698 var clone = range.duplicate();
700 // textarea内のテキスト全体を選択
701 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
702 clone.moveToElementText(t);
704 // cloneの選択範囲終点を、rangeの終点にあわせる
705 // [clone start] text1 [range start] text2 [range/clone end] text3
706 clone.setEndPoint('EndToEnd', range);
709 // [clone start] text1 [range start] text2 [range/clone end] text3
710 // --------------------------------------------------------- clone.text.length == end
711 // ------------------------------------- range.text.length
712 // -------------------- clone.text.length - range.text.length = start
713 start = clone.text.length - range.text.length;
714 end = clone.text.length;
719 else if ('selectionStart' in t) {
721 start = t.selectionStart;
722 end = t.selectionEnd;
729 text = t.value.slice(0, start).match(r);
731 text = t.value.slice(end).match(r);
739 getPos: function(t) {
743 if (document.selection) {
745 var range = document.selection.createRange();
746 x = range.offsetLeft +
747 (document.body.scrollLeft || document.documentElement.scrollLeft) -
748 document.documentElement.clientLeft;
749 y = range.offsetTop +
750 (document.body.scrollTop || document.documentElement.scrollTop) -
751 document.documentElement.clientTop;
753 } else if (window.getComputedStyle) {
755 var id = Wrapper.getId(t);
756 var dummy = $('.tagget_dummy' + id);
758 var span = dummy.children('span');
760 if(!span.is('span')) {
761 span = $('<span></span>');
766 dummy.text(t.value.slice(0, t.selectionEnd));
769 var offset = Util.getOffset(span.get(0));
771 x = offset.left - t.scrollLeft;
772 y = offset.top - t.scrollTop;
781 // textareaのカーソル位置に文字列挿入
782 insert: function(t, s) {
784 // カーソル移動位置(#{c})を取得後、削除
785 var cursor = s.indexOf('#{c}');
786 s = s.replace('#{c}', '');
788 // focusしないとIEでbodyに挿入されたりする
789 // Firefoxでもボタンで挿入後にfocusが戻らない
793 if (document.selection) {
796 var range = document.selection.createRange();
798 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
801 // カーソルがrange.textの最後になるので戻す
803 var back = s.length - (cursor != -1 ? cursor : s.length);
804 range.move('character', -back);
806 // 現在のカーソル位置を反映する(これやらないと水の泡)
811 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
812 else if ('selectionStart' in t) {
815 var top = t.scrollTop;
818 var start = t.selectionStart;
819 var end = t.selectionEnd;
821 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
822 t.value = t.value.slice(0, start) + s + t.value.slice(end);
825 var index = start + (cursor != -1 ? cursor : s.length);
826 t.setSelectionRange(index, index);
828 // 改行がたくさんある場合スクロールバーを下にずらす
829 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
830 top += parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10);
845 encodeSelection: function(t, func) {
850 if (document.selection) {
852 var range = document.selection.createRange();
853 range.text = func(range.text);
856 } else if ('selectionStart' in t) {
858 var top = t.scrollTop;
860 var start = t.selectionStart;
861 var end = t.selectionEnd;
863 var before = t.value.slice(start, end);
864 var after = func(before);
867 t.value = t.value.slice(0, start) +
868 after + t.value.slice(end);
870 t.setSelectionRange(end, end);
880 // カーソル位置より前にある開始タグをさかのぼって見ていく
881 // 閉じタグが何もなければ直近のタグを閉じる
882 // 閉じタグとマッチしないタグがあれば閉じる
883 // カーソル後に閉じタグがあるとかは無視
885 closeTag: function(t) {
890 var sTags = Cursor.getText(t, /^[\s\S]*$/)[0]
891 .replace(/<[!\?\/][\s\S]*?>/g, '') // <!?/を除去
892 .replace(/<[^>]*?\/>/g, '') // 空要素/>を除去
893 .match(/<[^>]+?>/g); // タグ抽出
894 var eTags = Cursor.getText(t, /<\/[^?]+?>/g);
898 var i = sTags.length - 1;
900 for (; i >= 0 && eTags.length > 0; i--) {
903 // >と\s始まり(属性と閉じ括弧)を除去
904 // TODO:もうちょっと改善すれば改行された要素にも対応できる?
905 var sTag = sTags[i].replace(/^.*<|[\s>].*/g, '');
908 for (; j < eTags.length; j++) {
910 var eTag = eTags[j].replace(/^.*<\/|[\s>].*/g, '');
912 // マッチしたらその閉じタグを配列から消して除外
918 if (j < eTags.length) {
921 // マッチしなかったらその開始タグを閉じる
931 Cursor.insert(t, '</' + sTags[i].replace(/^.*<|[\s>].*/g, '') + '>');
941 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
948 escapeHtml: function(s, quot) {
949 s = s.replace(/&/g, '&')
950 .replace(/</g, '<')
951 .replace(/>/g, '>');
953 return !quot ? s : s.replace(/"/g, '"');
956 // &, <, >[, "]を戻す
957 unescapeHtml:function(s, quot) {
959 s = s.replace(/&/g, '&')
960 .replace(/</g, '<')
961 .replace(/>/g, '>');
963 return !quot ? s : s.replace(/"/g, '"');
966 // offset(要素の実xy座標)を簡易算出
967 getOffset: function(elm) {
971 if (elm.getBoundingClientRect) {
973 var rect = elm.getBoundingClientRect();
974 left = Math.round(scrollX + rect.left);
975 top = Math.round(scrollY + rect.top);
979 left = elm.offsetLeft;
981 var offsetParent = elm.offsetParent;
983 while (offsetParent) {
984 left += offsetParent.offsetLeft;
985 top += offsetParent.offsetTop;
986 offsetParent = offsetParent.offsetParent;
999 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1005 load: function(key) {
1007 var cookie = $.cookie(key);
1016 var data = utf16to8(s);
1017 data = zip_deflate(data);
1018 data = base64encode(data);
1023 unzip: function(s) {
1025 var data = base64decode(s);
1026 data = zip_inflate(data);
1027 data = utf8to16(data);
1032 save: function(key, val) {
1034 var size = val.length;
1037 $.cookie(key, val, { 'expires': 1 });
1046 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1048 var init = function(t, i) {
1051 while ($('textarea').is('.tagget_' + i)) {
1054 $(t).addClass('tagget_' + i);
1057 Wrapper.absolutes(t);
1059 // setTimeout(function() {
1065 // keyup(発生タイミングが一番少ない)で候補表示
1066 $(t).keyup(function(e) {
1068 // 十字キー、Enterの時は補完を表示しない
1069 if(!(37 <= e.which && e.which <= 40) && !(e.which == 13)) {
1070 Wrapper.showPopup(t);
1071 } else if(e.which == 37 || e.which == 39) {
1072 Wrapper.getPopup(t).hide();
1079 .keydown(function(e) {
1082 // keydown以外だとうまくいかない
1084 Cursor.insert(t, '\t');
1088 // Shift+Enterで改行 or <br />入力
1090 if (e.shiftKey && e.which == 13) {
1093 if (!Wrapper.isPopup(t) && Wrapper.checkType(t, 'html')) {
1096 Cursor.insert(t, n);
1101 // Ctrl+Enterで閉じタグ補完
1102 if (e.ctrlKey && e.which == 13) {
1110 // upだとおしっぱにできない、pressだとおかしくなる
1111 if (Wrapper.isPopup(t)) {
1122 Wrapper.choice(t, 1);
1131 Wrapper.choice(t, -1);
1136 Cursor.insert(t, Util.unescapeHtml(Wrapper.current(t)));
1137 Wrapper.getPopup(t).hide();
1145 if (e.which == 13) {
1147 var indent = Cursor.getText(t, /^[\t ]*/mg);
1148 Cursor.insert(t, '\n' + (indent ? indent[indent.length - 1] : ''));
1151 if (window.getComputedStyle) {
1152 $t.scrollTop($t.scrollTop() + parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10));
1164 var data = Cookie.load(Wrapper.getId(t));
1166 t.value = Cookie.unzip(data);
1173 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1176 * $('textarea.tagget').tagget()とかで呼び出し
1178 $.fn.tagget = function(conf) {
1183 toolbar: true, // ツールバー表示
1184 cookie: true // クッキーに保存
1187 // thisには$('textarea.tagget')が入ってくる
1188 this.each(function(i) {
1199 $(window).resize(function() {
1201 self.each(function() {
1202 Wrapper.adjust(this);
1208 var interval = 60000; // 60秒:1分
1209 setTimeout(function timer() {
1211 self.each(function() {
1213 Cookie.save(Wrapper.getId(this), Cookie.zip(this.value));
1215 var fillZero = function(s) {
1216 return ('0' + s).slice(-2);
1218 var status = Wrapper.getStatus(this);
1219 var date = new Date();
1220 var y = date.getFullYear();
1221 var m = fillZero(date.getMonth() + 1);
1222 var d = fillZero(date.getDate());
1223 var h = fillZero(date.getHours());
1224 var min = fillZero(date.getMinutes());
1225 var sec = fillZero(date.getSeconds());
1226 status.children('.tagget_time')
1227 .html('Draft Saved At ' +
1228 y + '/' + m + '/' + d + '/' + ' ' + h + ':' + min + ':' + sec);
1230 setTimeout(timer, interval);