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)) {
216 var a = t.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]+/g) || [];
220 for (var i = 0; i < a.length; i++) {
233 for(var key in this.keywords) {
234 if (Wrapper.checkType(t, key)) {
235 words = words.concat(this.keywords[key]);
239 for(var i = 0; i < words.length; i++) {
241 if (words[i] != s && words[i].indexOf(s) == 0) {
242 matches.view.push(words[i].replace(/#\{c\}/g, ''));
244 matches.insert.push(words[i].slice(s.length));
248 return (matches.view.length != 0) ? matches : false;
254 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
256 * taggetに必要なHTMLを生成・操作する
264 // textarea周囲にHTMLを追加する
267 if ($(t).parents().is('.tagget_wrapper')) {
273 // wrapの戻り値はwrapされた要素なので、parentsでwrapした要素を取得
274 var wrapper = $(t).wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
275 .parents('div.tagget_wrapper');
278 if (this.isToolbar) {
280 wrapper.prepend(('<div class="tagget_toolbar"></div>')).children('div.tagget_toolbar');
284 $('<p class="tagget_encode"></p>').append(
285 $('<select title="選択範囲を変換"></select>')
286 .append('<option selected="selected" value="">Encode Selection</option>')
287 .append('<option value="entity">& < > → &amp; &lt; &gt;</option>')
288 .append('<option value="raw">&amp; &lt; &gt; → & < ></option>')
289 .append('<option value="enc">encodeURI()</option>')
290 .append('<option value="encc">encodeURIComponent()</option>')
291 .append('<option value="dec">decodeURI()</option>')
292 .append('<option value="decc">decodeURIComponent()</option>')
298 $('<p class="tagget_replace"></p>')
299 .append('<input type="text" value="Before" title="置換前" />')
300 .append('<img src="img/v_arrow010102.gif" />')
301 .append('<input type="text" value="After" title="置換後" />')
302 .append('<input type="button" value="Replace All" title="全て置換" />')
307 $('<p class="tagget_type"></p>').append(
308 $('<select title="選択範囲を変換"></select>')
309 .append('<option selected="selected" value="html|css|js">HTML,CSS,JS</option>')
310 .append('<option value="html|css">HTML,CSS</option>')
311 .append('<option value="html">HTML</option>')
312 .append('<option value="css">CSS</option>')
313 .append('<option value="js">JS</option>')
314 .append('<option value="css|js">CSS,JS</option>')
315 .append('<option value="html|js">HTML,JS</option>')
321 $('<p class="tagget_cookie"></p>')
322 .append('<input type="checkbox" checked="checked" title="Cookieに下書きを保存" id="tagget_check_cookie" /><label for="tagget_check_cookie" title="Cookieに下書きを保存">Cookie</label>')
328 $('<p class="tagget_intelli"></p>')
329 .append('<input type="checkbox" checked="checked" title="入力内容から補完" id="tagget_check_intelli" /><label for="tagget_check_intelli" title="入力内容から補完">intelligent</label>')
333 // Draft Saved At 1:17とか表示
334 // 行、列を表示(line: , col:)
336 $('<p class="tagget_status"><span class="tagget_time"></span><span class="tagget_line"></span> </p>')
339 // jQuery UI CSS Frameworkのclass名を設定
340 toolbar.addClass('ui-helper-clearfix');
341 wrapper.addClass('ui-widget-header ui-corner-all')
342 .children('div, p').addClass('ui-widget-header');
351 // wrap要素にeventを設定する。
352 // 既にHTMLがあった場合は、こちらだけを行う。
353 relate: function(t) {
355 var toolbar = $(t).parents('.tagget_wrapper').children('.tagget_toolbar');
359 toolbar.find('.tagget_encode select').change(function() {
361 switch (this.value) {
364 Cursor.encodeSelection(t, Util.escapeHtml);
368 Cursor.encodeSelection(t, Util.unescapeHtml);
372 Cursor.encodeSelection(t, encodeURI);
376 Cursor.encodeSelection(t, encodeURIComponent);
380 Cursor.encodeSelection(t, decodeURI);
384 Cursor.encodeSelection(t, decodeURIComponent);
395 var inputs = toolbar.find('.tagget_replace input');
396 inputs.filter('[type=button]').click(function() {
400 var before = inputs.eq(0).val();
401 var after = inputs.eq(1).val();
404 if (before.match(/^\/.+\/([^\/]+)$/)) {
407 before = before.replace(/^\/|\/[^\/]+?$/g, '');
411 t.value = val.replace(new RegExp(before, flag), after);
421 return $(t).attr('class').match(/tagget_([0-9]+)/)[1];
425 isPopup: function(t) {
426 var popup = this.getPopup(t);
427 return popup.css('display') != 'none';
431 setPopup: function(display) {
432 var popup = this.getPopup(t);
440 checkType: function(t, type) {
441 var currentType = $(t).parents('.tagget_wrapper')
442 .find('.tagget_type select').val();
443 return (new RegExp(currentType)).test(type);
446 checkIntelli: function(t) {
447 var intelli = $(t).parents('.tagget_wrapper')
448 .find('.tagget_intelli input').attr('checked');
453 absolutes: function(t) {
455 var body = $(document.body);
456 var id = this.getId(t);
459 var popup = $('<ul class="tagget_popup tagget_popup' + id + '"></ul>');
464 if (window.getComputedStyle) {
466 var dummy = $('<pre class="tagget_dummy tagget_dummy' + id + '"></pre>');
474 getDummy: function(t) {
476 var id = this.getId(t);
477 var dummy = $('.tagget_dummy' + id);
482 // textareaのstyleをdummyにコピー
483 adjust: function(t) {
485 var dummy = this.getDummy(t);
487 if (window.getComputedStyle) {
489 var org = getComputedStyle(t,'');
493 'padding-left', 'padding-right', 'padding-top', 'padding-bottom',
494 'border-left-style', 'border-right-style','border-top-style','border-bottom-style',
495 'border-left-width', 'border-right-width','border-top-width','border-bottom-width',
496 'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
499 for(var i = 0; i < props.length; i++){
501 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
502 return m1.toUpperCase();
505 dummy.css(capitalized, org.getPropertyValue(props[i]));
509 var offset = Util.getOffset(t);
517 dummy.width($t.width())
519 .scrollLeft($t.scrollLeft())
520 .scrollTop($t.scrollTop());
526 showPopup: function(t) {
528 var popup = this.getPopup(t);
529 var suggests = Suggester.get(t, Cursor.getText(t)[0]);
534 if (popup.text() != suggests.view.join('')) {
538 for(var i = 0; i < suggests.view.length; i++) {
539 var li = $('<li></li>').attr('title', suggests.insert[i])
540 .append('<a href="#"></a>').text(suggests.view[i])
542 popup.children('li').removeClass('tagget_current');
543 $(this).addClass('tagget_current');
546 Cursor.insert(t, Util.unescapeHtml(popup.children('li.tagget_current').attr('title')));
550 li.addClass('tagget_current');
556 // TODO:入力分の削除をincertに現在の文字渡してやるようにした方がよさそう。
559 popup.children('li').each(function(i) {
560 $(this).attr('title', suggests.insert[i]);
564 var pos = Cursor.getPos(t);
581 getPopup: function(t) {
582 var id = this.getId(t);
583 var popup = $('.tagget_popup' + id);
590 choice: function(t, d) {
592 var popup = this.getPopup(t);
593 var lis = popup.children('li');
597 for(var i = 0; i < lis.length; i++) {
601 if(li.hasClass('tagget_current')) {
602 li.removeClass('tagget_current');
603 i = (i == 0) ? lis.length - 1 : i - 1;
604 lis.eq(i).addClass('tagget_current');
611 for(var i = 0; i < lis.length; i++) {
615 if(li.hasClass('tagget_current')) {
616 li.removeClass('tagget_current');
617 i = (i == lis.length - 1 ) ? 0 : i + 1;
618 lis.eq(i).addClass('tagget_current');
629 current: function(t) {
631 var popup = this.getPopup(t);
632 return popup.children('li.tagget_current').attr('title');
635 getStatus: function(t) {
636 return $(t).parents('.tagget_wrapper').find('.tagget_status');
639 setLine: function(t) {
641 var status = this.getStatus(t);
644 var s = Cursor.getText(t, /^[\s\S]*$/)[0] || '';
648 var thisLine = Cursor.getText(t, /.*$/)[0];
650 var lineNum = (s.match(/\n/g) || []).length + 1;
651 var cols = thisLine.length;
653 status.children('.tagget_line').html('line: ' + lineNum + ' col: ' + cols);
660 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
667 getText: function(t, r, after) {
669 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
671 r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
677 if (document.selection) {
679 // t.focus(); // focusなしでもいける
682 var range = document.selection.createRange();
685 var clone = range.duplicate();
687 // textarea内のテキスト全体を選択
688 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
689 clone.moveToElementText(t);
691 // cloneの選択範囲終点を、rangeの終点にあわせる
692 // [clone start] text1 [range start] text2 [range/clone end] text3
693 clone.setEndPoint('EndToEnd', range);
696 // [clone start] text1 [range start] text2 [range/clone end] text3
697 // --------------------------------------------------------- clone.text.length == end
698 // ------------------------------------- range.text.length
699 // -------------------- clone.text.length - range.text.length = start
700 start = clone.text.length - range.text.length;
701 end = clone.text.length;
706 else if ('selectionStart' in t) {
708 start = t.selectionStart;
709 end = t.selectionEnd;
716 text = t.value.slice(0, start).match(r);
718 text = t.value.slice(end).match(r);
726 getPos: function(t) {
730 if (document.selection) {
732 var range = document.selection.createRange();
733 x = range.offsetLeft +
734 (document.body.scrollLeft || document.documentElement.scrollLeft) -
735 document.documentElement.clientLeft;
736 y = range.offsetTop +
737 (document.body.scrollTop || document.documentElement.scrollTop) -
738 document.documentElement.clientTop;
740 } else if (window.getComputedStyle) {
742 var id = Wrapper.getId(t);
743 var dummy = $('.tagget_dummy' + id);
745 var span = dummy.children('span');
747 if(!span.is('span')) {
748 span = $('<span></span>');
753 dummy.text(t.value.slice(0, t.selectionEnd));
756 var offset = Util.getOffset(span.get(0));
758 x = offset.left - t.scrollLeft;
759 y = offset.top - t.scrollTop;
768 // textareaのカーソル位置に文字列挿入
769 insert: function(t, s) {
771 // カーソル移動位置(#{c})を取得後、削除
772 var cursor = s.indexOf('#{c}');
773 s = s.replace('#{c}', '');
775 // focusしないとIEでbodyに挿入されたりする
776 // Firefoxでもボタンで挿入後にfocusが戻らない
780 if (document.selection) {
783 var range = document.selection.createRange();
785 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
788 // カーソルがrange.textの最後になるので戻す
790 var back = s.length - (cursor != -1 ? cursor : s.length);
791 range.move('character', -back);
793 // 現在のカーソル位置を反映する(これやらないと水の泡)
798 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
799 else if ('selectionStart' in t) {
802 var top = t.scrollTop;
805 var start = t.selectionStart;
806 var end = t.selectionEnd;
808 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
809 t.value = t.value.slice(0, start) + s + t.value.slice(end);
812 var index = start + (cursor != -1 ? cursor : s.length);
813 t.setSelectionRange(index, index);
815 // 改行がたくさんある場合スクロールバーを下にずらす
816 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
817 top += parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10);
832 encodeSelection: function(t, func) {
837 if (document.selection) {
839 var range = document.selection.createRange();
840 range.text = func(range.text);
843 } else if ('selectionStart' in t) {
845 var top = t.scrollTop;
847 var start = t.selectionStart;
848 var end = t.selectionEnd;
850 var before = t.value.slice(start, end);
851 var after = func(before);
854 t.value = t.value.slice(0, start) +
855 after + t.value.slice(end);
857 t.setSelectionRange(end, end);
868 closeTag: function(t) {
870 // <<-tag name-><----------attr-------------><->->
871 var sTag = Cursor.getText(t, /<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g);
872 var sTag2 = Cursor.getText(t, /<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g, true);
873 var eTag = Cursor.getText(t, /<\s*\/\s*.+?\s*>/g);
874 var eTag2 = Cursor.getText(t, /<\s*\/\s*.+?\s*>/g, true);
876 console.log('s:'+sTag);
877 console.log('s2:'+sTag2);
878 console.log('e:'+eTag);
879 console.log('e2:'+eTag2);
881 // 整形式じゃなさそうなのはとりあえず無視
882 if (sTag.length + sTag2.length > eTag.length + eTag2.length) {
884 for (var i = sTag.length - 1; i >= 0; i--) {
886 var s = sTag[i].replace(/^.*<|[\s>].*/g, '');
887 if (!eTag || eTag.length == 0 || s != eTag.shift().replace(/^.*\/|>.*/g, '')) {
888 Cursor.insert(t, '</' + s + '>');
900 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
907 escapeHtml: function(s, quot) {
908 s = s.replace(/&/g, '&')
909 .replace(/</g, '<')
910 .replace(/>/g, '>');
912 return !quot ? s : s.replace(/"/g, '"');
915 // &, <, >[, "]を戻す
916 unescapeHtml:function(s, quot) {
918 s = s.replace(/&/g, '&')
919 .replace(/</g, '<')
920 .replace(/>/g, '>');
922 return !quot ? s : s.replace(/"/g, '"');
925 // offset(要素の実xy座標)を簡易算出
926 getOffset: function(elm) {
930 if (elm.getBoundingClientRect) {
932 var rect = elm.getBoundingClientRect();
933 left = Math.round(scrollX + rect.left);
934 top = Math.round(scrollY + rect.top);
938 left = elm.offsetLeft;
940 var offsetParent = elm.offsetParent;
942 while (offsetParent) {
943 left += offsetParent.offsetLeft;
944 top += offsetParent.offsetTop;
945 offsetParent = offsetParent.offsetParent;
958 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
964 load: function(key) {
966 var cookie = $.cookie(key);
975 var data = utf16to8(s);
976 data = zip_deflate(data);
977 data = base64encode(data);
984 var data = base64decode(s);
985 data = zip_inflate(data);
986 data = utf8to16(data);
991 save: function(key, val) {
993 var size = val.length;
996 $.cookie(key, val, { 'expires': 1 });
1005 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1007 var init = function(t, i) {
1010 while ($('textarea').is('.tagget_' + i)) {
1013 $(t).addClass('tagget_' + i);
1016 Wrapper.absolutes(t);
1018 // setTimeout(function() {
1024 // keyup(発生タイミングが一番少ない)で候補表示
1025 $(t).keyup(function(e) {
1027 if(!(37 <= e.which && e.which <= 40)) {
1028 Wrapper.showPopup(t);
1029 } else if(e.which == 37 || e.which == 39) {
1030 Wrapper.getPopup(t).hide();
1037 .keydown(function(e) {
1040 // keydown以外だとうまくいかない
1042 Cursor.insert(t, '\t');
1046 // Shift+Enterで改行 or <br />入力
1048 if (e.shiftKey && e.which == 13) {
1051 if (!Wrapper.isPopup(t) && Wrapper.checkType(t, 'html')) {
1054 Cursor.insert(t, n);
1059 // Ctrl+Enterで閉じタグ補完
1060 if (e.ctrlKey && e.which == 13) {
1068 // upだとおしっぱにできない、pressだとおかしくなる
1069 if (Wrapper.isPopup(t)) {
1080 Wrapper.choice(t, 1);
1089 Wrapper.choice(t, -1);
1094 Cursor.insert(t, Util.unescapeHtml(Wrapper.current(t)));
1095 Wrapper.getPopup(t).hide();
1103 if (e.which == 13) {
1105 var indent = Cursor.getText(t, /^[\t ]*/mg);
1106 Cursor.insert(t, '\n' + (indent ? indent[indent.length - 1] : ''));
1109 if (window.getComputedStyle) {
1110 $t.scrollTop($t.scrollTop() + parseInt(getComputedStyle(t, '').getPropertyValue('line-height'), 10));
1122 t.value = Cookie.unzip(Cookie.load(Wrapper.getId(t)));
1128 /* ---------- ---------- ---------- ---------- ---------- ---------- ---------- ---------- */
1131 * $('textarea.tagget').tagget()とかで呼び出し
1133 $.fn.tagget = function(conf) {
1138 toolbar: true, // ツールバー表示
1139 cookie: true // クッキーに保存
1142 // thisには$('textarea.tagget')が入ってくる
1143 this.each(function(i) {
1154 $(window).resize(function() {
1156 self.each(function() {
1157 Wrapper.adjust(this);
1163 var interval = 60000; // 60秒:1分
1164 setTimeout(function timer() {
1166 self.each(function() {
1168 Cookie.save(Wrapper.getId(this), Cookie.zip(this.value));
1170 var fillZero = function(s) {
1171 return ('0' + s).slice(-2);
1173 var status = Wrapper.getStatus(this);
1174 var date = new Date();
1175 var y = date.getFullYear();
1176 var m = fillZero(date.getMonth() + 1);
1177 var d = fillZero(date.getDate());
1178 var h = fillZero(date.getHours());
1179 var min = fillZero(date.getMinutes());
1180 var sec = fillZero(date.getSeconds());
1181 status.children('.tagget_time')
1182 .html('Draft Saved At ' +
1183 y + '/' + m + '/' + d + '/' + ' ' + h + ':' + min + ':' + sec);
1185 setTimeout(timer, interval);