2 * tagget: Not WYSIWYG Editor (jQuery Plugin)
3 * Simple interface and good suggestions.
7 * Licensed under the MIT license.
8 * Copyright (c) 2009 tagget.org
15 // $('textarea.tagget').tagget()とかで呼び出し
16 $.fn.tagget = function(conf) {
26 '<address>#{cursor}</address>',
27 '<![CDATA[ #{cursor} ]]>',
28 '<img src="#{cursor}" />',
30 '<div>#{cursor}</div>\n',
31 '<span>#{cursor}</span>',
33 '<h1>#{cursor}</h1>\n',
34 '<h2>#{cursor}</h2>\n',
35 '<h3>#{cursor}</h3>\n',
36 '<h4>#{cursor}</h4>\n',
37 '<h5>#{cursor}</h5>\n',
38 '<h6>#{cursor}</h6>\n',
40 '<ul>\n<li>#{cursor}</li>\n</ul>\n',
41 '<ol>\n<li>#{cursor}</li>\n</ol>\n',
42 '<li>#{cursor}</li>\n',
43 '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n',
44 '<dt>#{cursor}</dt>\n',
45 '<dt>#{cursor}</dd>\n',
47 '<strong>#{cursor}</strong>',
50 '<form>\n#{cursor}\n</form>',
51 '<input type="#{cursor}" />',
59 '<?xml version="1.0" encoding="#{cursor}"?>\n',
60 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
61 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n',
62 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n',
64 '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">\n',
65 '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n',
66 '<script type="text/javascript" src="#{cursor}"></script>\n',
67 '<script type="text/javascript">\n#{cursor}\n</script>\n',
68 '<style type="text/css">\n#{cursor}\n</style>\n',
70 '<meta http-equiv="Content-Type" content="text/html; charset=#{cursor}" />\n',
71 '<meta http-equiv="Content-Style-Type" content="text/css" />\n',
72 '<meta http-equiv="Content-Script-Type" content="text/javascript" />\n',
73 '<meta name="ROBOTS" content="#{cursor}" />\n',
74 '<meta name="description" content="#{cursor}" />\n',
75 '<meta name="keywords" content="#{cursor}" />\n',
77 '<head>#{cursor}</head>\n',
78 '<title>#{cursor}</title>\n',
79 '<body>#{cursor}</body>\n',
81 '<link rel="alternate" type="application/atom+xml" title="#{cursor}" href="" />\n',
82 '<link rel="alternate" type="application/rss+xml" title="#{cursor}" href="" />\n',
83 '<link rel="EditURI" type="application/rsd+xml" title="#{cursor}" href="" />\n'
92 'onclick="#{cursor}"',
98 'xml:lang="#{cursor}"',
108 'http://www.w3.org/1999/xhtml'
111 // return tags.concat(header, attributes, values).sort();
112 return tags.concat(header, attributes, values);
120 'function() { #{cursor} }',
129 return objs.concat(libs);
155 return props.concat(values);
163 tags: true, // タグボタン表示
164 edit: true, // 編集機能表示
165 jqui: true, // jQuery UI CSS Framework仕様
166 vars: true // 入力内容を利用した補完
170 conf.keys = $.extend(conf.keys, keywords);
172 // thisには$('textarea.tagget')が入ってくる
174 this.each(function() {
176 // var tmp = this.value;
178 // this.value = 'Initializing tagget...';
194 var escapeHtml = function(s, quot) {
195 s = s.replace(/&/g, '&')
196 .replace(/</g, '<')
197 .replace(/>/g, '>');
199 return !quot ? s : s.replace(/"/g, '"');
202 // &, <, >(, ")を戻す
203 var unescapeHtml = function(s, quot) {
205 s = s.replace(/&/g, '&')
206 .replace(/</g, '<')
207 .replace(/>/g, '>');
209 return !quot ? s : s.replace(/"/g, '"');
213 var getOffset = function(elm) {
217 if (elm.getBoundingClientRect) {
219 var rect = elm.getBoundingClientRect();
220 left = Math.round(scrollX + rect.left);
221 top = Math.round(scrollY + rect.top);
225 left = elm.offsetLeft;
227 var offsetParent = elm.offsetParent;
229 while (offsetParent) {
230 left += offsetParent.offsetLeft;
231 top += offsetParent.offsetTop;
232 offsetParent = offsetParent.offsetParent;
246 var init = function(textarea, conf) {
250 // textarea or dummyを使う関数群
252 // textareaのカーソル位置に文字列挿入
253 var insert = function(s) {
255 // カーソル移動位置(#{cursor})を取得後、削除
256 var cursor = s.indexOf('#{cursor}');
257 s = s.replace('#{cursor}', '');
259 // focusしないとIEでbodyに挿入されたりする
260 // Firefoxでもボタンで挿入後にfocusが戻らない
264 if (document.selection) {
267 var range = document.selection.createRange();
269 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
272 // カーソルがrange.textの最後になるので戻す
273 // #{cursor}指定がなければ最後のまま
274 var back = s.length - (cursor != -1 ? cursor : s.length);
275 range.move('character', -back);
277 // 現在のカーソル位置を反映する(これやらないと水の泡)
282 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
283 else if ('selectionStart' in textarea) {
286 var top = textarea.scrollTop;
289 var start = textarea.selectionStart;
290 var end = textarea.selectionEnd;
292 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
293 textarea.value = textarea.value.slice(0, start) + s + textarea.value.slice(end);
296 var index = start + (cursor != -1 ? cursor : s.length);
297 textarea.setSelectionRange(index, index);
299 // 改行がたくさんある場合スクロールバーを下にずらす
300 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
301 top += parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10);
305 textarea.scrollTop = top;
313 var check = function() {
320 var text = getText()[0];
327 var a = textarea.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]+/g) || [];
331 for (var i = 0; i < a.length; i++) {
345 for(var key in conf.keys) {
346 if (t.hasClass('tagget_' + key) || t.hasClass(key)) {
347 words = words.concat(conf.keys[key]);
352 // words = words.sort();
354 for(var i = 0; i < words.length; i++) {
356 if (words[i] != text && words[i].indexOf(text) == 0) {
357 matches.view.push(words[i].replace(/#\{cursor\}/g, ''));
358 matches.insert.push(escapeHtml(words[i].slice(text.length)));
364 return (matches.view.length != 0) ? matches : false;
370 var getText = function(r, after) {
372 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
373 if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
378 if (document.selection) {
380 // textarea.focus(); // focusなしでもいける
383 var range = document.selection.createRange();
386 var clone = range.duplicate();
388 // textarea内のテキスト全体を選択
389 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
390 clone.moveToElementText(textarea);
392 // cloneの選択範囲終点を、rangeの終点にあわせる
393 // [clone start] text1 [range start] text2 [range/clone end] text3
394 clone.setEndPoint('EndToEnd', range);
397 // [clone start] text1 [range start] text2 [range/clone end] text3
398 // --------------------------------------------------------- clone.text.length == end
399 // ------------------------------------- range.text.length
400 // -------------------- clone.text.length - range.text.length = start
401 start = clone.text.length - range.text.length;
402 end = clone.text.length;
407 else if ('selectionStart' in textarea) {
409 start = textarea.selectionStart;
410 end = textarea.selectionEnd;
417 text = textarea.value.slice(0, start).match(r);
419 text = textarea.value.slice(end).match(r);
427 var getPos = function() {
431 if (document.selection) {
433 var range = document.selection.createRange();
434 x = range.offsetLeft +
435 (document.body.scrollLeft || document.documentElement.scrollLeft) -
436 document.documentElement.clientLeft;
437 y = range.offsetTop +
438 (document.body.scrollTop || document.documentElement.scrollTop) -
439 document.documentElement.clientTop;
441 } else if (window.getComputedStyle) {
443 var span = dummy.children('span');
445 if(!span.is('span')) {
446 span = $('<span></span>');
451 dummy.text(textarea.value.slice(0, textarea.selectionEnd));
454 var offset = getOffset(span.get(0));
456 x = offset.left - textarea.scrollLeft;
457 y = offset.top - textarea.scrollTop;
467 // textarea周囲にHTMLを追加する
470 // wrapの場合、普通にやると参照を取得できないのでparentsで取得
471 var wrapper = t.wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
472 .parents('div.tagget_wrapper');
475 var menu = (conf.tags || conf.edit) ?
476 wrapper.prepend(('<div class="tagget_menu"></div>')).children('div.tagget_menu') : null;
480 var tags = $('<ul class="tagget_tags"></ul>')
481 .append('<li><a href="#">a</a></li>')
482 .append('<li><a href="#">p</a></li>')
483 .append('<li><a href="#">ul</a></li>')
484 .append('<li><a href="#">li</a></li>')
485 .append('<li><a href="#">div</a></li>')
486 .append('<li><a href="#">span</a></li>')
487 .append('<li><a href="#">pre</a></li>')
488 .append('<li><a href="#">code</a></li>')
489 .append('<li><a href="#">blockquote</a></li>')
490 .append('<li><a href="#">dl</a></li>')
491 .append('<li><a href="#">dt</a></li>')
492 .append('<li><a href="#">dd</a></li>')
493 .append('<li><a href="#">link</a></li>')
494 .append('<li><a href="#">script</a></li>')
495 .append('<li><a href="#">frameset</a></li>')
496 .append('<li><a href="#">frame</a></li>');
502 var edit = $('<div class="tagget_edit"></div>');
506 $('<p class="tagget_encode"></p>').append(
507 $('<select></selected>')
508 .append('<option selected="selected" value="">選択範囲を変換</option>')
509 .append('<option value="entity">& < > → &amp; &lt; &gt;</option>')
510 .append('<option value="raw">&amp; &lt; &gt; → & < ></option>')
511 .append('<option value="enc">encodeURI()</option>')
512 .append('<option value="encc">encodeURIComponent()</option>')
513 .append('<option value="dec">decodeURI()</option>')
514 .append('<option value="decc">decodeURIComponent()</option>')
520 $('<p class="tagget_replace"></p>')
521 .append('<input type="text" value="置換前" />')
523 .append('<input type="text" value="置換後" />')
524 .append('<input type="button" value="置換" />')
530 // jQuery UI CSS Frameworkのclass名を設定
532 menu.addClass('ui-helper-clearfix');
533 wrapper.addClass('ui-widget-header ui-corner-all')
534 .children('div, p').addClass('ui-widget-header')
535 .find('li').addClass('ui-state-default ui-corner-all').hover(function(){
536 $(this).addClass('ui-state-hover');
538 $(this).removeClass('ui-state-hover');
542 var body = $(document.body);
545 var suggest = $('<ul class="tagget_suggest"></ul>');
546 body.append(suggest);
550 if (window.getComputedStyle) {
552 var dummy = $('<pre class="tagget_dummy"></pre>');
554 // textareaのstyleをdummyにコピー
555 var onResize = function() {
557 var org = getComputedStyle(textarea,'');
561 'padding-left', 'padding-right', 'padding-top', 'padding-bottom',
562 'border-left-style', 'border-right-style','border-top-style','border-bottom-style',
563 'border-left-width', 'border-right-width','border-top-width','border-bottom-width',
564 'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
567 for(var i = 0; i < props.length; i++){
569 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
570 return m1.toUpperCase();
573 dummy.css(capitalized, org.getPropertyValue(props[i]));
577 var offset = getOffset(textarea);
584 dummy.width(t.width())
586 .scrollLeft(t.scrollLeft())
587 .scrollTop(t.scrollTop());
591 // resize時にはtextareaのサイズも変わるので
592 $(window).resize(function() {
607 var onClick = function(elm, s) {
609 $(elm).click(function() {
617 tags.find('a').each(function() {
620 // title属性に設定するほうがいいか?
621 switch (this.innerHTML) {
624 onClick(this, '<a href="#{cursor}"></a>');
628 onClick(this, '<p>#{cursor}</p>\n');
632 onClick(this, '<ul>\n<li>#{cursor}</li>\n</ul>\n');
636 onClick(this, '<li>#{cursor}</li>\n');
640 onClick(this, '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n');
644 onClick(this, '<dt>#{cursor}</dt>\n');
648 onClick(this, '<dd>#{cursor}</dd>\n');
652 onClick(this, '<pre>#{cursor}</pre>\n');
656 onClick(this, '<code>#{cursor}</code>');
660 onClick(this, '<blockquote>#{cursor}</blockquote>\n');
664 onClick(this, '<div>#{cursor}</div>\n');
668 onClick(this, '<span>#{cursor}</span>\n');
672 onClick(this, '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n');
676 onClick(this, '<script type="text/javascript" src="#{cursor}"></script>\n');
680 onClick(this, '<frameset>\n<frame src="#{cursor}" />\n</frameset>\n');
684 onClick(this, '<frame src="#{cursor}" />\n');
699 var onChange = function(func) {
703 if (document.selection) {
705 var range = document.selection.createRange();
706 range.text = func(range.text);
709 } else if ('selectionStart' in textarea) {
711 var top = textarea.scrollTop;
713 var start = textarea.selectionStart;
714 var end = textarea.selectionEnd;
716 textarea.value = textarea.value.slice(0, start) +
717 func(textarea.value.slice(start, end)) +
718 textarea.value.slice(end);
720 textarea.setSelectionRange(end, end);
722 textarea.scrollTop = top;
727 edit.find('.tagget_encode select').change(function() {
729 switch (this.value) {
732 onChange(escapeHtml);
736 onChange(unescapeHtml);
744 onChange(encodeURIComponent);
752 onChange(decodeURIComponent);
763 var inputs = edit.find('.tagget_replace input');
764 inputs.filter('[type=button]').click(function() {
766 var val = textarea.value;
768 var before = inputs.eq(0).val();
769 var after = inputs.eq(1).val();
772 if (before.match(/^\/.+\/([^\/]+)$/)) {
775 before = before.replace(/^\/|\/[^\/]+?$/g, '');
779 textarea.value = val.replace(new RegExp(before, flag), after);
787 // キー入力時のsuggestion設定
789 // keyup(発生タイミングが一番少ない)で候補表示
790 t.keyup(function(e) {
792 var suggests = check();
796 if (suggest.text() != suggests.view.join('')) {
800 for(var i = 0; i < suggests.view.length; i++) {
801 var li = $('<li></li>').attr('title', suggests.insert[i])
802 .append('<a href="#"></a>').text(suggests.view[i])
804 suggest.children('li').removeClass('tagget_current');
805 $(this).addClass('tagget_current');
808 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
811 if (i == 0) li.addClass('tagget_current');
817 suggest.children('li').each(function(i) {
818 $(this).attr('title', suggests.insert[i]);
835 if (conf.bind) $(conf.bind).html(textarea.value);
843 .keydown(function(e) {
846 // keydown以外だとうまくいかない
852 // Shift+Enterで改行 or br入力
854 if (e.shiftKey && e.which == 13) {
858 if ((t.hasClass('html') || t.hasClass('tagget_html')) && suggest.css('display') == 'none') {
868 if (e.ctrlKey && e.which == 13) {
870 // <<-tag name-><----------attr-------------><->->
871 var sTag = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g);
872 var sTag2 = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g, true);
873 var eTag = getText(/<\s*\/\s*.+?\s*>/g);
874 var eTag2 = getText(/<\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 insert('</' + s + '>');
902 // upだとおしっぱにできない、pressだとおかしくなる
903 if (suggest.css('display') != 'none') {
910 var lis = suggest.children('li');
912 for(var i = 0; i < lis.length; i++) {
916 if(li.hasClass('tagget_current')) {
917 li.removeClass('tagget_current');
918 i = (i == 0) ? lis.length - 1 : i - 1;
919 lis.eq(i).addClass('tagget_current');
928 var lis = suggest.children('li');
930 for(var i = 0; i < lis.length; i++) {
934 if(li.hasClass('tagget_current')) {
935 li.removeClass('tagget_current');
936 i = (i == lis.length - 1 ) ? 0 : i + 1;
937 lis.eq(i).addClass('tagget_current');
946 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
956 var indent = getText(/^[\t ]*/mg);
957 insert('\n' + (indent ? indent[indent.length - 1] : ''));
959 if (window.getComputedStyle) {
960 t.scrollTop(t.scrollTop() + parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10));