2 * tagget: Not WYSIWYG Editor (jQuery Plugin)
6 * Licensed under the MIT license.
7 * Copyright (c) 2009 tagget.org
14 // $('textarea.tagget').tagget()とかで呼び出し
15 $.fn.tagget = function(conf) {
25 '<address>#{cursor}</address>',
26 '<![CDATA[ #{cursor} ]]>',
27 '<img src="#{cursor}" />',
29 '<div>#{cursor}</div>\n',
30 '<span>#{cursor}</span>',
32 '<h1>#{cursor}</h1>\n',
33 '<h2>#{cursor}</h2>\n',
34 '<h3>#{cursor}</h3>\n',
35 '<h4>#{cursor}</h4>\n',
36 '<h5>#{cursor}</h5>\n',
37 '<h6>#{cursor}</h6>\n',
39 '<ul>\n<li>#{cursor}</li>\n</ul>\n',
40 '<ol>\n<li>#{cursor}</li>\n</ol>\n',
41 '<li>#{cursor}</li>\n',
42 '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n',
43 '<dt>#{cursor}</dt>\n',
44 '<dt>#{cursor}</dd>\n',
46 '<strong>#{cursor}</strong>',
49 '<form>\n#{cursor}\n</form>',
50 '<input type="#{cursor}" />',
58 '<?xml version="1.0" encoding="#{cursor}"?>\n',
59 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">\n',
60 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">\n',
61 '<!DOCTYPE HTML PUBLIC "-//W3C//DTD XHTML 1.0 Frameset//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-frameset.dtd">\n',
63 '<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="ja" lang="ja">\n',
64 '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n',
65 '<script type="text/javascript" src="#{cursor}"></script>\n',
66 '<script type="text/javascript">\n#{cursor}\n</script>\n',
67 '<style type="text/css">\n#{cursor}\n</style>\n',
69 '<meta http-equiv="Content-Type" content="text/html; charset=#{cursor}" />\n',
70 '<meta http-equiv="Content-Style-Type" content="text/css" />\n',
71 '<meta http-equiv="Content-Script-Type" content="text/javascript" />\n',
72 '<meta name="ROBOTS" content="#{cursor}" />\n',
73 '<meta name="description" content="#{cursor}" />\n',
74 '<meta name="keywords" content="#{cursor}" />\n',
76 '<head>#{cursor}</head>\n',
77 '<title>#{cursor}</title>\n',
78 '<body>#{cursor}</body>\n',
80 '<link rel="alternate" type="application/atom+xml" title="#{cursor}" href="" />\n',
81 '<link rel="alternate" type="application/rss+xml" title="#{cursor}" href="" />\n',
82 '<link rel="EditURI" type="application/rsd+xml" title="#{cursor}" href="" />\n'
91 'onclick="#{cursor}"',
97 'xml:lang="#{cursor}"',
107 'http://www.w3.org/1999/xhtml'
110 // return tags.concat(header, attributes, values).sort();
111 return tags.concat(header, attributes, values);
119 'function() { #{cursor} }',
128 return objs.concat(libs);
154 return props.concat(values);
162 tags: true, // タグボタン表示
163 edit: true, // 編集機能表示
164 jqui: true, // jQuery UI CSS Framework仕様
165 vars: true // 入力内容を利用した補完
169 conf.keys = $.extend(conf.keys, keywords);
171 // thisには$('textarea.tagget')が入ってくる
173 this.each(function() {
175 // var tmp = this.value;
177 // this.value = 'Initializing tagget...';
193 var escapeHtml = function(s, quot) {
194 s = s.replace(/&/g, '&')
195 .replace(/</g, '<')
196 .replace(/>/g, '>');
198 return !quot ? s : s.replace(/"/g, '"');
201 // &, <, >(, ")を戻す
202 var unescapeHtml = function(s, quot) {
204 s = s.replace(/&/g, '&')
205 .replace(/</g, '<')
206 .replace(/>/g, '>');
208 return !quot ? s : s.replace(/"/g, '"');
212 var getOffset = function(elm) {
216 if (elm.getBoundingClientRect) {
218 var rect = elm.getBoundingClientRect();
219 left = Math.round(scrollX + rect.left);
220 top = Math.round(scrollY + rect.top);
224 left = elm.offsetLeft;
226 var offsetParent = elm.offsetParent;
228 while (offsetParent) {
229 left += offsetParent.offsetLeft;
230 top += offsetParent.offsetTop;
231 offsetParent = offsetParent.offsetParent;
245 var init = function(textarea, conf) {
249 // textarea or dummyを使う関数群
251 // textareaのカーソル位置に文字列挿入
252 var insert = function(s) {
254 // カーソル移動位置(#{cursor})を取得後、削除
255 var cursor = s.indexOf('#{cursor}');
256 s = s.replace('#{cursor}', '');
258 // focusしないとIEでbodyに挿入されたりする
259 // Firefoxでもボタンで挿入後にfocusが戻らない
263 if (document.selection) {
266 var range = document.selection.createRange();
268 // 選択中のテキスト引数sで置き換え(現在のカーソル位置にsを挿入)
271 // カーソルがrange.textの最後になるので戻す
272 // #{cursor}指定がなければ最後のまま
273 var back = s.length - (cursor != -1 ? cursor : s.length);
274 range.move('character', -back);
276 // 現在のカーソル位置を反映する(これやらないと水の泡)
281 // inかundefinedあたりで判定しないとselectionStartが0の時ミスる
282 else if ('selectionStart' in textarea) {
285 var top = textarea.scrollTop;
288 var start = textarea.selectionStart;
289 var end = textarea.selectionEnd;
291 // 開始位置と終了位置の間(現在のカーソル位置)にsを挿入
292 textarea.value = textarea.value.slice(0, start) + s + textarea.value.slice(end);
295 var index = start + (cursor != -1 ? cursor : s.length);
296 textarea.setSelectionRange(index, index);
298 // 改行がたくさんある場合スクロールバーを下にずらす
299 if (/\n/g.test(s) && s.match(/\n/g).length > 2) {
300 top += parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10);
304 textarea.scrollTop = top;
312 var check = function() {
319 var text = getText()[0];
326 var a = textarea.value.match(/[^<>\s '"#\=:;{}\(\)!?,*]+/g) || [];
330 for (var i = 0; i < a.length; i++) {
344 for(var key in conf.keys) {
345 if (t.hasClass('tagget_' + key) || t.hasClass(key)) {
346 words = words.concat(conf.keys[key]);
351 // words = words.sort();
353 for(var i = 0; i < words.length; i++) {
355 if (words[i] != text && words[i].indexOf(text) == 0) {
356 matches.view.push(words[i].replace(/#\{cursor\}/g, ''));
357 matches.insert.push(escapeHtml(words[i].slice(text.length)));
363 return (matches.view.length != 0) ? matches : false;
369 var getText = function(r, after) {
371 // if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\n=]*?$/;
372 if (!r) r = /[^<>\s '"#\.=;]+?$|<[^<>\s '"#\.=;]*?$/;
377 if (document.selection) {
379 // textarea.focus(); // focusなしでもいける
382 var range = document.selection.createRange();
385 var clone = range.duplicate();
387 // textarea内のテキスト全体を選択
388 // [clone start] text1 [range start] text2 [range end] text3 [clone end]
389 clone.moveToElementText(textarea);
391 // cloneの選択範囲終点を、rangeの終点にあわせる
392 // [clone start] text1 [range start] text2 [range/clone end] text3
393 clone.setEndPoint('EndToEnd', range);
396 // [clone start] text1 [range start] text2 [range/clone end] text3
397 // --------------------------------------------------------- clone.text.length == end
398 // ------------------------------------- range.text.length
399 // -------------------- clone.text.length - range.text.length = start
400 start = clone.text.length - range.text.length;
401 end = clone.text.length;
406 else if ('selectionStart' in textarea) {
408 start = textarea.selectionStart;
409 end = textarea.selectionEnd;
416 text = textarea.value.slice(0, start).match(r);
418 text = textarea.value.slice(end).match(r);
426 var getPos = function() {
430 if (document.selection) {
432 var range = document.selection.createRange();
433 x = range.offsetLeft +
434 (document.body.scrollLeft || document.documentElement.scrollLeft) -
435 document.documentElement.clientLeft;
436 y = range.offsetTop +
437 (document.body.scrollTop || document.documentElement.scrollTop) -
438 document.documentElement.clientTop;
440 } else if (window.getComputedStyle) {
442 var span = dummy.children('span');
444 if(!span.is('span')) {
445 span = $('<span></span>');
450 dummy.text(textarea.value.slice(0, textarea.selectionEnd));
453 var offset = getOffset(span.get(0));
455 x = offset.left - textarea.scrollLeft;
456 y = offset.top - textarea.scrollTop;
466 // textarea周囲にHTMLを追加する
469 // wrapの場合、普通にやると参照を取得できないのでparentsで取得
470 var wrapper = t.wrap('<div class="tagget_wrapper"><p class="tagget_main"></p></div>')
471 .parents('div.tagget_wrapper');
474 var menu = (conf.tags || conf.edit) ?
475 wrapper.prepend(('<div class="tagget_menu"></div>')).children('div.tagget_menu') : null;
479 var tags = $('<ul class="tagget_tags"></ul>')
480 .append('<li><a href="#">a</a></li>')
481 .append('<li><a href="#">p</a></li>')
482 .append('<li><a href="#">ul</a></li>')
483 .append('<li><a href="#">li</a></li>')
484 .append('<li><a href="#">div</a></li>')
485 .append('<li><a href="#">span</a></li>')
486 .append('<li><a href="#">pre</a></li>')
487 .append('<li><a href="#">code</a></li>')
488 .append('<li><a href="#">blockquote</a></li>')
489 .append('<li><a href="#">dl</a></li>')
490 .append('<li><a href="#">dt</a></li>')
491 .append('<li><a href="#">dd</a></li>')
492 .append('<li><a href="#">link</a></li>')
493 .append('<li><a href="#">script</a></li>')
494 .append('<li><a href="#">frameset</a></li>')
495 .append('<li><a href="#">frame</a></li>');
501 var edit = $('<div class="tagget_edit"></div>');
505 $('<p class="tagget_encode"></p>').append(
506 $('<select></selected>')
507 .append('<option selected="selected" value="">選択範囲を変換</option>')
508 .append('<option value="entity">& < > → &amp; &lt; &gt;</option>')
509 .append('<option value="raw">&amp; &lt; &gt; → & < ></option>')
510 .append('<option value="enc">encodeURI()</option>')
511 .append('<option value="encc">encodeURIComponent()</option>')
512 .append('<option value="dec">decodeURI()</option>')
513 .append('<option value="decc">decodeURIComponent()</option>')
519 $('<p class="tagget_replace"></p>')
520 .append('<input type="text" value="置換前" />')
522 .append('<input type="text" value="置換後" />')
523 .append('<input type="button" value="置換" />')
529 // jQuery UI CSS Frameworkのclass名を設定
531 menu.addClass('ui-helper-clearfix');
532 wrapper.addClass('ui-widget-header ui-corner-all')
533 .children('div, p').addClass('ui-widget-header')
534 .find('li').addClass('ui-state-default ui-corner-all').hover(function(){
535 $(this).addClass('ui-state-hover');
537 $(this).removeClass('ui-state-hover');
541 var body = $(document.body);
544 var suggest = $('<ul class="tagget_suggest"></ul>');
545 body.append(suggest);
549 if (window.getComputedStyle) {
551 var dummy = $('<pre class="tagget_dummy"></pre>');
553 // textareaのstyleをdummyにコピー
554 var onResize = function() {
556 var org = getComputedStyle(textarea,'');
560 'padding-left', 'padding-right', 'padding-top', 'padding-bottom',
561 'border-left-style', 'border-right-style','border-top-style','border-bottom-style',
562 'border-left-width', 'border-right-width','border-top-width','border-bottom-width',
563 'font-family', 'font-size', 'line-height', 'letter-spacing', 'word-spacing'
566 for(var i = 0; i < props.length; i++){
568 var capitalized = props[i].replace(/-(.)/g, function(m, m1){
569 return m1.toUpperCase();
572 dummy.css(capitalized, org.getPropertyValue(props[i]));
576 var offset = getOffset(textarea);
583 dummy.width(t.width())
585 .scrollLeft(t.scrollLeft())
586 .scrollTop(t.scrollTop());
590 // resize時にはtextareaのサイズも変わるので
591 $(window).resize(function() {
606 var onClick = function(elm, s) {
608 $(elm).click(function() {
616 tags.find('a').each(function() {
619 // title属性に設定するほうがいいか?
620 switch (this.innerHTML) {
623 onClick(this, '<a href="#{cursor}"></a>');
627 onClick(this, '<p>#{cursor}</p>\n');
631 onClick(this, '<ul>\n<li>#{cursor}</li>\n</ul>\n');
635 onClick(this, '<li>#{cursor}</li>\n');
639 onClick(this, '<dl>\n<dt>#{cursor}</dt>\n<dd></dd>\n</dl>\n');
643 onClick(this, '<dt>#{cursor}</dt>\n');
647 onClick(this, '<dd>#{cursor}</dd>\n');
651 onClick(this, '<pre>#{cursor}</pre>\n');
655 onClick(this, '<code>#{cursor}</code>');
659 onClick(this, '<blockquote>#{cursor}</blockquote>\n');
663 onClick(this, '<div>#{cursor}</div>\n');
667 onClick(this, '<span>#{cursor}</span>\n');
671 onClick(this, '<link rel="stylesheet" type="text/css" href="#{cursor}" />\n');
675 onClick(this, '<script type="text/javascript" src="#{cursor}"></script>\n');
679 onClick(this, '<frameset>\n<frame src="#{cursor}" />\n</frameset>\n');
683 onClick(this, '<frame src="#{cursor}" />\n');
698 var onChange = function(func) {
702 if (document.selection) {
704 var range = document.selection.createRange();
705 range.text = func(range.text);
708 } else if ('selectionStart' in textarea) {
710 var top = textarea.scrollTop;
712 var start = textarea.selectionStart;
713 var end = textarea.selectionEnd;
715 textarea.value = textarea.value.slice(0, start) +
716 func(textarea.value.slice(start, end)) +
717 textarea.value.slice(end);
719 textarea.setSelectionRange(end, end);
721 textarea.scrollTop = top;
726 edit.find('.tagget_encode select').change(function() {
728 switch (this.value) {
731 onChange(escapeHtml);
735 onChange(unescapeHtml);
743 onChange(encodeURIComponent);
751 onChange(decodeURIComponent);
762 var inputs = edit.find('.tagget_replace input');
763 inputs.filter('[type=button]').click(function() {
765 var val = textarea.value;
767 var before = inputs.eq(0).val();
768 var after = inputs.eq(1).val();
771 if (before.match(/^\/.+\/([^\/]+)$/)) {
774 before = before.replace(/^\/|\/[^\/]+?$/g, '');
778 textarea.value = val.replace(new RegExp(before, flag), after);
786 // キー入力時のsuggestion設定
788 // keyup(発生タイミングが一番少ない)で候補表示
789 t.keyup(function(e) {
791 var suggests = check();
795 if (suggest.text() != suggests.view.join('')) {
799 for(var i = 0; i < suggests.view.length; i++) {
800 var li = $('<li></li>').attr('title', suggests.insert[i])
801 .append('<a href="#"></a>').text(suggests.view[i])
803 suggest.children('li').removeClass('tagget_current');
804 $(this).addClass('tagget_current');
807 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
810 if (i == 0) li.addClass('tagget_current');
816 suggest.children('li').each(function(i) {
817 $(this).attr('title', suggests.insert[i]);
834 if (conf.bind) $(conf.bind).html(textarea.value);
842 .keydown(function(e) {
845 // keydown以外だとうまくいかない
851 // Shift+Enterで改行 or br入力
853 if (e.shiftKey && e.which == 13) {
857 if ((t.hasClass('html') || t.hasClass('tagget_html')) && suggest.css('display') == 'none') {
867 if (e.ctrlKey && e.which == 13) {
869 // <<-tag name-><----------attr-------------><->->
870 var sTag = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g);
871 var sTag2 = getText(/<\s*[^\/!?=]+?(?:\s*[^=>\s]+?\s*=\s*["']?.*?["']?\s*)*\s*>/g, true);
872 var eTag = getText(/<\s*\/\s*.+?\s*>/g);
873 var eTag2 = getText(/<\s*\/\s*.+?\s*>/g, true);
875 console.log('s:'+sTag);
876 console.log('s2:'+sTag2);
877 console.log('e:'+eTag);
878 console.log('e2:'+eTag2);
880 // 整形式じゃなさそうなのはとりあえず無視
881 if (sTag.length + sTag2.length > eTag.length + eTag2.length) {
883 for (var i = sTag.length - 1; i >= 0; i--) {
885 var s = sTag[i].replace(/^.*<|[\s>].*/g, '');
886 if (!eTag || eTag.length == 0 || s != eTag.shift().replace(/^.*\/|>.*/g, '')) {
887 insert('</' + s + '>');
901 // upだとおしっぱにできない、pressだとおかしくなる
902 if (suggest.css('display') != 'none') {
909 var lis = suggest.children('li');
911 for(var i = 0; i < lis.length; i++) {
915 if(li.hasClass('tagget_current')) {
916 li.removeClass('tagget_current');
917 i = (i == 0) ? lis.length - 1 : i - 1;
918 lis.eq(i).addClass('tagget_current');
927 var lis = suggest.children('li');
929 for(var i = 0; i < lis.length; i++) {
933 if(li.hasClass('tagget_current')) {
934 li.removeClass('tagget_current');
935 i = (i == lis.length - 1 ) ? 0 : i + 1;
936 lis.eq(i).addClass('tagget_current');
945 insert(unescapeHtml(suggest.children('li.tagget_current').attr('title')));
955 var indent = getText(/^[\t ]*/mg);
956 insert('\n' + (indent ? indent[indent.length - 1] : ''));
958 if (window.getComputedStyle) {
959 t.scrollTop(t.scrollTop() + parseInt(getComputedStyle(textarea, '').getPropertyValue('line-height'), 10));