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);
210 add: function(newwords) {
212 for(var type in newwords) {
214 if (this.keywords[type]) {
215 this.keywords[type].concat(newwords[type]);
217 this.keywords[type] = newwords[type];
227 get: function(t, s) {
229 if (!t || !s || !t.value) {
241 if (Wrapper.checkIntelli(t)) {
245 var a = t.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]{2,}/g) || [];
249 for (var i = 0; i < a.length; i++) {
262 for(var key in this.keywords) {
263 if (Wrapper.checkType(t, key)) {
264 words = words.concat(this.keywords[key]);
268 for(var i = 0; i < words.length; i++) {
270 if (words[i] != s && words[i].indexOf(s) == 0) {
271 matches.view.push(words[i].replace(/#\{c\}/g, ''));
273 matches.insert.push(words[i].slice(s.length));
277 return (matches.view.length != 0) ? matches : false;
283 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
285 * taggetに必要なHTMLを生成・操作する
293 // textarea周囲にHTMLを追加する
296 if ($(t).parents().is('.tagget_wrapper')) {
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');
307 if (this.isToolbar) {
309 wrapper.prepend(('<div class="tagget_toolbar"></div>')).children('div.tagget_toolbar');
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;</option>')
317 .append('<option value="raw">&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>')
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="全て置換" />')
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>')
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>')
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>')
362 // Draft Saved At 1:17とか表示
363 // 行、列を表示(line: , col:)
365 $('<p class="tagget_status"><span class="tagget_time"></span><span class="tagget_line"></span> </p>')
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');
379 getToolbar: function(t) {
381 return $(t).parents('.tagget_wrapper').children('.tagget_toolbar');
385 // wrap要素にeventを設定する。
386 // 既にHTMLがあった場合は、こちらだけを行う。
387 relate: function(t) {
389 var toolbar = this.getToolbar(t);
393 toolbar.find('.tagget_encode select').change(function() {
395 switch (this.value) {
398 Cursor.encodeSelection(t, Util.escapeHtml);
402 Cursor.encodeSelection(t, Util.unescapeHtml);
406 Cursor.encodeSelection(t, encodeURI);
410 Cursor.encodeSelection(t, encodeURIComponent);
414 Cursor.encodeSelection(t, decodeURI);
418 Cursor.encodeSelection(t, decodeURIComponent);
429 var inputs = toolbar.find('.tagget_replace input');
430 inputs.filter('[type=button]').click(function() {
434 var before = inputs.eq(0).val();
435 var after = inputs.eq(1).val();
436 // フラグはデフォルトでg(Replace All)
439 // TODO: 一応動くけどもっとみやすいコードに
441 // /before/gim形式で入力されたらフラグを抽出
443 if (before.match(/^\/.+\/([^\/]+)$/)) {
447 // 行頭の/、/以降の文字(フラグ、スラッシュ連続など形式外の入力)を除去
448 before = before.replace(/^\/|\/[^\/]+?$/g, '');
452 t.value = val.replace(new RegExp(before, flag), after);
462 return $(t).attr('class').match(/tagget_([0-9]+)/)[1];
466 isPopup: function(t) {
467 var popup = this.getPopup(t);
468 return popup.css('display') != 'none';
472 setPopup: function(display) {
473 var popup = this.getPopup(t);
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);
488 checkCookie: function(t) {
489 var cookie = $(t).parents('.tagget_wrapper')
490 .find('.tagget_cookie input').attr('checked');
495 checkIntelli: function(t) {
496 var intelli = $(t).parents('.tagget_wrapper')
497 .find('.tagget_intelli input').attr('checked');
502 absolutes: function(t) {
504 var body = $(document.body);
505 var id = this.getId(t);
508 var popup = $('<ul class="tagget_popup tagget_popup' + id + '"></ul>');
513 if (window.getComputedStyle) {
515 var dummy = $('<pre class="tagget_dummy tagget_dummy' + id + '"></pre>');
523 getDummy: function(t) {
525 var id = this.getId(t);
526 var dummy = $('.tagget_dummy' + id);
531 // textareaのstyleをdummyにコピー
532 adjust: function(t) {
534 var dummy = this.getDummy(t);
536 if (window.getComputedStyle) {
538 var org = getComputedStyle(t,'');
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'
548 for(var i = 0; i < props.length; i++){
550 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
551 return m1.toUpperCase();
554 dummy.css(capitalized, org.getPropertyValue(props[i]));
558 var offset = Util.getOffset(t);
566 dummy.width($t.width())
568 .scrollLeft($t.scrollLeft())
569 .scrollTop($t.scrollTop());
575 // TODO: 候補数制限 or 高さ設定してスクロール
576 showPopup: function(t) {
578 var popup = this.getPopup(t);
579 var suggests = Suggester.get(t, Cursor.getText(t)[0]);
584 if (popup.text() != suggests.view.join('')) {
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])
592 popup.children('li').removeClass('tagget_current');
593 $(this).addClass('tagget_current');
596 Cursor.insert(t, Util.unescapeHtml(popup.children('li.tagget_current').attr('title')));
600 li.addClass('tagget_current');
606 // TODO:入力分の削除をincertに現在の文字渡してやるようにした方がよさそう。
609 popup.children('li').each(function(i) {
610 $(this).attr('title', suggests.insert[i]);
614 var pos = Cursor.getPos(t);
631 getPopup: function(t) {
632 var id = this.getId(t);
633 var popup = $('.tagget_popup' + id);
640 choice: function(t, d) {
642 var popup = this.getPopup(t);
643 var lis = popup.children('li');
647 for(var i = 0; i < lis.length; i++) {
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');
661 for(var i = 0; i < lis.length; i++) {
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');
679 current: function(t) {
681 var popup = this.getPopup(t);
682 return popup.children('li.tagget_current').attr('title');
685 getStatus: function(t) {
686 return $(t).parents('.tagget_wrapper').find('.tagget_status');
689 setLine: function(t) {
691 var status = this.getStatus(t);
694 var s = Cursor.getText(t, /^[\s\S]*$/)[0] || '';
698 var thisLine = Cursor.getText(t, /.*$/)[0];
700 var lineNum = (s.match(/\n/g) || []).length + 1;
701 var cols = thisLine.length;
703 status.children('.tagget_line').html('line: ' + lineNum + ' col: ' + cols);
710 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
717 getText: function(t, r, after) {
719 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
724 r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
730 if (document.selection) {
732 // t.focus(); // focusなしでもいける
735 var range = document.selection.createRange();
738 var clone = range.duplicate();
740 // textarea内のテキスト全体を選択
741 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
742 clone.moveToElementText(t);
744 // cloneの選択範囲終点を、rangeの終点にあわせる
745 // [clone start] text1 [range start] text2 [range/clone end] text3
746 clone.setEndPoint('EndToEnd', range);
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;
759 else if ('selectionStart' in t) {
761 start = t.selectionStart;
762 end = t.selectionEnd;
769 text = t.value.slice(0, start).match(r);
771 text = t.value.slice(end).match(r);
779 getPos: function(t) {
783 if (document.selection) {
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;
793 } else if (window.getComputedStyle) {
795 var id = Wrapper.getId(t);
796 var dummy = $('.tagget_dummy' + id);
798 var span = dummy.children('span');
800 if(!span.is('span')) {
801 span = $('<span></span>');
806 dummy.text(t.value.slice(0, t.selectionEnd));
809 var offset = Util.getOffset(span.get(0));
811 x = offset.left - t.scrollLeft;
812 y = offset.top - t.scrollTop;
821 // textareaのカーソル位置に文字列挿入
822 insert: function(t, s) {
824 // カーソル移動位置(#{c})を取得後、削除
825 var cursor = s.indexOf('#{c}');
826 s = s.replace('#{c}', '');
828 // focusしないとIEでbodyに挿入されたりする
829 // Firefoxでもボタンで挿入後にfocusが戻らない
833 if (document.selection) {
836 var range = document.selection.createRange();
838 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
841 // カーソルがrange.textの最後になるので戻す
843 var back = s.length - (cursor != -1 ? cursor : s.length);
844 range.move('character', -back);
846 // 現在のカーソル位置を反映する(これやらないと水の泡)
851 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
852 else if ('selectionStart' in t) {
855 var top = t.scrollTop;
858 var start = t.selectionStart;
859 var end = t.selectionEnd;
861 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
862 t.value = t.value.slice(0, start) + s + t.value.slice(end);
865 var index = start + (cursor != -1 ? cursor : s.length);
866 t.setSelectionRange(index, index);
868 // 改行がたくさんある場合スクロールバーを下にずらす
869 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
870 top += parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10);
885 encodeSelection: function(t, func) {
890 if (document.selection) {
892 var range = document.selection.createRange();
893 range.text = func(range.text);
896 } else if ('selectionStart' in t) {
898 var top = t.scrollTop;
900 var start = t.selectionStart;
901 var end = t.selectionEnd;
903 var before = t.value.slice(start, end);
904 var after = func(before);
907 t.value = t.value.slice(0, start) +
908 after + t.value.slice(end);
910 t.setSelectionRange(end, end);
920 // カーソル位置より前にある開始タグをさかのぼって見ていく
921 // 閉じタグが何もなければ直近のタグを閉じる
922 // 閉じタグとマッチしないタグがあれば閉じる
923 // カーソル後に閉じタグがあるとかは無視
925 closeTag: function(t) {
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);
938 var i = sTags.length - 1;
940 for (; i >= 0 && eTags.length > 0; i--) {
943 // >と\s始まり(属性と閉じ括弧)を除去
944 // TODO:もうちょっと改善すれば改行された要素にも対応できる?
945 var sTag = sTags[i].replace(/^.*<|[\s>].*/g, '');
948 for (; j < eTags.length; j++) {
950 var eTag = eTags[j].replace(/^.*<\/|[\s>].*/g, '');
952 // マッチしたらその閉じタグを配列から消して除外
958 if (j < eTags.length) {
961 // マッチしなかったらその開始タグを閉じる
971 Cursor.insert(t, '</' + sTags[i].replace(/^.*<|[\s>].*/g, '') + '>');
981 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
988 escapeHtml: function(s, quot) {
989 s = s.replace(/&/g, '&')
990 .replace(/</g, '<')
991 .replace(/>/g, '>');
993 return !quot ? s : s.replace(/"/g, '"');
996 // &, <, >[, "]を戻す
997 unescapeHtml:function(s, quot) {
999 s = s.replace(/&/g, '&')
1000 .replace(/</g, '<')
1001 .replace(/>/g, '>');
1003 return !quot ? s : s.replace(/"/g, '"');
1006 // offset(要素の実xy座標)を簡易算出
1007 getOffset: function(elm) {
1011 if (elm.getBoundingClientRect) {
1013 var rect = elm.getBoundingClientRect();
1014 left = Math.round(scrollX + rect.left);
1015 top = Math.round(scrollY + rect.top);
1019 left = elm.offsetLeft;
1020 top = elm.offsetTop;
1021 var offsetParent = elm.offsetParent;
1023 while (offsetParent) {
1024 left += offsetParent.offsetLeft;
1025 top += offsetParent.offsetTop;
1026 offsetParent = offsetParent.offsetParent;
1038 fillZero: function(s) {
1039 return ('0' + s).slice(-2);
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());
1052 return y + '/' + m + '/' + d + '/' + ' ' + h + ':' + min + ':' + sec;
1058 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1064 load: function(key) {
1066 var cookie = $.cookie(key);
1075 var data = utf16to8(s);
1076 data = zip_deflate(data);
1077 data = base64encode(data);
1082 unzip: function(s) {
1084 var data = base64decode(s);
1085 data = zip_inflate(data);
1086 data = utf8to16(data);
1091 save: function(key, val) {
1093 var size = val.length;
1096 $.cookie(key, val, { 'expires': 1 });
1104 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1106 * textareaのEventに設定する関数群
1110 // keyupは発生タイミングが一番少ない
1112 keyup: function(e) {
1116 // 十字キー、Enterの時は補完を表示しない
1117 if(!(37 <= e.which && e.which <= 40) && !(e.which == 13)) {
1118 Wrapper.showPopup(t);
1122 else if(e.which == 37 || e.which == 39) {
1123 Wrapper.getPopup(t).hide();
1130 // press,upだとおかしくなるものもこっちで
1131 keydown: function(e) {
1136 // keydown以外だとうまくいかない
1138 Cursor.insert(t, '\t');
1142 // Shift+Enterで改行 or <br />入力
1143 if (e.shiftKey && e.which == 13) {
1146 if (Wrapper.checkType(t, 'html')) {
1149 Cursor.insert(t, n);
1156 // Ctrl+Enterで閉じタグ補完
1157 if (e.ctrlKey && e.which == 13) {
1159 if (Wrapper.checkType(t, 'html')) {
1169 // upだとおしっぱにできない、pressだとおかしくなる
1170 if (Wrapper.isPopup(t)) {
1181 Wrapper.choice(t, 1);
1190 Wrapper.choice(t, -1);
1195 Cursor.insert(t, Util.unescapeHtml(Wrapper.current(t)));
1196 Wrapper.getPopup(t).hide();
1204 if (e.which == 13) {
1206 var indent = Cursor.getText(t, /^[\t ]*/mg);
1207 Cursor.insert(t, '\n' + (indent ? indent[indent.length - 1] : ''));
1210 if (window.getComputedStyle) {
1211 $t.scrollTop($t.scrollTop() + parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10));
1225 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1227 var init = function(t, i) {
1230 while ($('textarea').is('.tagget_' + i)) {
1233 $(t).addClass('tagget_' + i);
1236 Wrapper.absolutes(t);
1240 // keyup(発生タイミングが一番少ない)で候補表示
1241 $(t).keyup(Event.keyup)
1244 .keydown(Event.keydown);
1247 var data = Cookie.load(Wrapper.getId(t));
1249 t.value = Cookie.unzip(data);
1256 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1259 * $('textarea.tagget').tagget()とかで呼び出し
1261 $.fn.tagget = function(conf) {
1266 toolbar: true, // ツールバー表示
1267 cookie: true // クッキーに保存
1270 // thisには$('textarea.tagget')が入ってくる
1271 this.each(function(i) {
1282 $(window).resize(function() {
1284 self.each(function() {
1285 Wrapper.adjust(this);
1291 var interval = 60000; // 60秒:1分
1292 setTimeout(function timer() {
1294 self.each(function() {
1296 if (Wrapper.checkCookie(this)) {
1298 Cookie.save(Wrapper.getId(this), Cookie.zip(this.value));
1300 var status = Wrapper.getStatus(this);
1301 status.children('.tagget_time').html('Draft Saved At ' + Util.now());
1305 setTimeout(timer, interval);