jQuery.Tagify.jsでタグ入力機能をさいつよにする

### デモ

https://codepen.io/qwe001/pen/eVdqeV

これを見て何をしようとしているのか察して下さい。

### 要件

* 任意の文字を入力すると、部分一致する入力候補を画面に表示する
* 入力候補を選択するか、入力を確定すると、タグ要素として入力内容を表示する
* 送信ボタンを押すと、タグ要素を任意の区切り文字で分割したひとつの文字列を出力する

### さいつよ要件

* タグ入力フィールドを、複数のテキストエリアで使用できるようにする
* ボタン操作でタグを一括で削除できるようにする
* ボタン操作で文字列をクリップボードにコピーできるようにする
* タグ追加時、そのタグに関連する外部URLを付与する
* ラジオボタンの操作で、タグ入力を通常のテキストエリアに変更する(逆もしかり)

### 必要ライブラリ

* jQuery (筆者バージョンは v2.1.4)

http://jquery.com/download/

* jQuery.ui (筆者バージョンは v1.11.4)

https://jqueryui.com/download/all/

autocomplete機能を使用

* jQuery.Tagify.js

https://github.com/alicial/jQuery.Tagify

### 事前準備

タグの一括削除機能は、jQuery.Tagify.jsで用意されていない。
いろいろ試行錯誤したけど、思ったように動いてくれず、
Tagifyのremoveメソッドをタグの要素数だけループ実行して消すって方法に落ち着いた。

このタグの要素数を取得するメソッドがないため、jQuery.Tagify.jsに以下のメソッドを追加する
(MITライセンスだしソース改変OKですよね？)

###### 修正前

```javascript:jquery.tagify.js
(function ($) {
	
	$.widget("ui.tagify", {
		options: {
			delimiters: [13, 188, 44],          // what user can type to complete a tag in char codes: [enter], [comma]
			outputDelimiter: ',',           // delimiter for tags in original input field
			cssClass: 'tagify-container',   // CSS class to style the tagify div and tags, see stylesheet
			addTagPrompt: 'add tags',       // placeholder text
			addTagOnBlur: false				// Add a tag on blur when not empty
		},

		// (中略)
		
		inputField: function() {
		    return this.tagInput;
		},
		
		containerDiv: function() {
		    return this.tagDiv;
		},

		// remove the div, and show original input
		destroy: function() {
		    $.Widget.prototype.destroy.apply(this);
			this.tagDiv.remove();
			this.element.show();
		}
	});

})(jQuery);
```

###### 修正後

```javascript:jquery.tagify.js
(function ($) {
	
	$.widget("ui.tagify", {
		options: {
			delimiters: [13, 188, 44],          // what user can type to complete a tag in char codes: [enter], [comma]
			outputDelimiter: ',',           // delimiter for tags in original input field
			cssClass: 'tagify-container',   // CSS class to style the tagify div and tags, see stylesheet
			addTagPrompt: 'add tags',       // placeholder text
			addTagOnBlur: false				// Add a tag on blur when not empty
		},

		// (中略)
		
		inputField: function() {
		    return this.tagInput;
		},
		
		containerDiv: function() {
		    return this.tagDiv;
		},

		// タグの要素数を返す
		inputTagIndex: function() {
		    return this.tags.length;
		},

		// remove the div, and show original input
		destroy: function() {
		    $.Widget.prototype.destroy.apply(this);
			this.tagDiv.remove();
			this.element.show();
		}
	});

})(jQuery);
```

### 実装

###### JavaScript

```javascript
<script src="https://code.jquery.com/jquery-2.1.4.min.js"></script>
<script src="https://code.jquery.com/ui/1.11.4/jquery-ui.min.js"></script>
<script type="text/javascript" src="jquery.tagify.js"></script>
<script type="text/javascript">
$(function(){
    /**
     * Generate Tagify Element
     * 
     * @params str target
     * @return void
     */
    function generateTagify(target)
    {
        var target = $('#' + target);
        var dataArrs = [
            ["やまだ", "山田"],
            ["たなか", "田中"],
            ["すずき", "鈴木"],
        ];

        // make tagify elements
        var myTextArea = target.tagify({
            delimiters: [13, 188, 44], // [enter], [comma], [dot]
            outputDelimiter: "、",
            cssClass: 'tagify-container',
            addTagPrompt: "タグを追加...",
            addTagOnBlur: false,
        });

        // make autocomplete elements
        myTextArea.tagify('inputField').autocomplete({
            source: function(request, response){
                // ひらがなで入力しても漢字で候補を出すようにする
                var re = new RegExp('^(' + request.term + ')');
                var list = [];

                $.each(dataArrs, function(i, values){
                    if(values[0].match(re) || values[1].match(re)){
                        list.push(values[1]);
                    }
                });
                response(list);
            },
            position: {
                of: myTextArea.tagify('containerDiv')
            },
            select: function(event, ui) {
                // 入力候補をEnterキー押下で選択した時だけでなく、マウスクリックした時もタグ追加できるようにする
                var origEvent = event;

                while(origEvent.originalEvent !== undefined){
                    origEvent = origEvent.originalEvent;

                    if(origEvent.type == 'keydown' || origEvent.type == 'click'){
                        myTextArea.tagify('add', ui.item.value);
                        return false; // タグ追加後、入力中のテキストをクリアする
                    }
                }
            },
            close: function(event, ui) {
                // 公式ドキュメントには、TagifyのAutocompleteはここに書く例があるが、
                // ここに書くと、入力候補がなくなった瞬間にタグ追加が発生するため、
                // 入力候補にないタグの入力が阻害され、ユーザビリティが下がるので select イベントを使用した。
            }
        });
    }

    /**
     * Append Url To Tag
     * Need To Make Click Event For Link Movement
     * 
     * @params str target
     * @return bool
     */
    function appendUrlToTag(target)
    {
        var dataArrs, text, gen, filtered, html, remove_button;

        dataArrs = [
            { name: "山田", url: "https://www.google.com" },
            { name: "田中", url: "https://www.yahoo.com" },
            { name: "鈴木", url: "https://www.microsoft.com" },
        ];

        text = target.text(); // 山田x
        gen = text.slice(0, -1); // 山田

        filtered = $.grep(dataArrs, function(obj, index){ // PHPで言うところの array_search 的なやつ
            return (obj.name === gen); // object
        });

        if(filtered.length === 0){
            target.addClass('no_url');
            target.attr('data-href', null);

            return false;
        }

        // [CAUTION] If you append anchor tag to span, Tagify element will NOT work normally. That is why I use data href attribute.
        //html = '<a href="' + filtered[0].url + '" target="_blank" class="has_url">' + text + '</a>';
        //remove_button = '<a href="#">x</a>';

        target.addClass('has_url');
        target.attr('data-href', filtered[0].url);
        target.attr('title', filtered[0].url);

        return true;
    }

    /**
     * Copy Text To Clipboard
     * 
     * @params str textVal
     * @return bool
     */
    function copyTextToClipboard(textVal)
    {
        var copyFrom, bodyElm, result;

        copyFrom = document.createElement("textarea");
        copyFrom.textContent = textVal;

        bodyElm = document.getElementsByTagName("body")[0];
        bodyElm.appendChild(copyFrom);

        copyFrom.select();
        result = document.execCommand('copy'); // copy text from selection and return boolean value

        bodyElm.removeChild(copyFrom);

        return result;
    }

    /**
     * Detect Whether Browser Type Is IE Or Not
     * 
     * @return bool
     */
    function isIE()
    {
        var userAgent = window.navigator.userAgent.toLowerCase();

        if(userAgent.match(/(msie|MSIE)/) || userAgent.match(/(T|t)rident/)){
            return true;
        }

        return false;
    }

    // make tagify elements
    $('.tagify').each(function(){
        var elem = $(this).attr('id');
        generateTagify(elem);
    });

    // change tagify element to default textarea
    var tag_type = $('.tag_switch:checked').val();

    if(tag_type == 1){
        $('#textarea1').tagify('destroy');
        $('span[data-delete-from=textarea1]').hide();
    }

    // switch tagify element or default textarea
    $('.tag_switch').click(function(){
        var tag_type = $(this).val();

        if(tag_type == 1){
            var tag_text = $('#textarea1').tagify('serialize');
            $('#textarea1').val(tag_text);
            $('#textarea1').tagify('destroy');
            $('span[data-delete-from=textarea1]').hide();
        }
        else {
            generateTagify('textarea1');
            $('span[data-delete-from=textarea1]').show();

            $('#textarea1').next('.tagify-container').children('span').each(function(){
                appendUrlToTag($(this));
            });
        }
    });

    // append url to tag when document on ready
    $('#textarea1').next('.tagify-container').children('span').each(function(){
        appendUrlToTag($(this));
    });

    // append url to tag when new one insert
    $('#textarea1').next('.tagify-container').on('DOMSubtreeModified propertychange', function(){

        // [NOTICE] DOMSubtreeModified Event is deprecated to use
        // @see http://logroid.blogspot.jp/2013/07/javascript-dom-mutation-events-observer.html

        if(isIE()){
            // IE11だけブラウザがフリーズする致命的なバグが発生し、対処方法が思いつかなかったので蓋をした ^o^
            return false;
        }

        $(this).children('span').each(function(){
            // [WARNING] This function may CRASH via IE 11
            appendUrlToTag($(this));
        });

        return true;
    });

    // tag link movement
    $('.tagify_table').on('click', '.has_url', function(){
        $(this).children('a').click(function(event){
            event.stopPropagation(); // tag remove

            return false;
        });

        window.open($(this).attr('data-href'), '_blank');

        return true;
    });

    $('form').submit(function() {
        $('.tagify').each(function(){
            $(this).tagify('serialize');
        });
    });

    $('.copy').click(function(){
        var from = $(this).data('copy-from');
        from = $('#' + from);

        if(from.val()){
            try {
                var copy = copyTextToClipboard(from.val());

                if(copy){
                    alert("クリップボードにコピーしました");
                    return true;
                }
            }
            catch(e){
                alert("コピー失敗：" + "ブラウザが対応していない可能性があります");
                throw console.log("copy failed:" + e);
            }
        }

        return false;
    });

    $('.delete-all').click(function(){
        var from = $(this).data('delete-from');
        from = $('#' + from);

        if(from.val()){
            try {
                var tagIndex = from.tagify('inputTagIndex'); // this method is self made. NOT INCLUDE in original js files.

                if(tagIndex !== 0){
                    if(confirm("タグを全削除します。よろしいですか？")){
                        for(i = 0; i < tagIndex; i++){
                            from.tagify('remove');
                        }
                        from.val("");
                        return true;
                    }
                }
            }
            catch(e){
                alert("削除失敗：" + "ブラウザが対応していない可能性があります");
                throw console.log("delete failed:" + e);
            }
        }

        return false;
    });

});
```

###### HTML

```html
<form action="store" method="POST">
	<table class="tagify_table">
		<tbody>
			<tr>
				<th>表示区分 *</th>
				<td>
					<ul>
						<li>
							<input type="radio" name="tag_switch" value="0" id="tag_switch_1" class="tag_switch" checked="checked">
							<label for="tag_switch_1">タグ入力フィールドにする</label>
						</li>
						<li>
							<input type="radio" name="tag_switch" value="1" id="tag_switch_2" class="tag_switch">
							<label for="tag_switch_2">テキストエリアにする</label>
						</li>
					</ul>
				</td>
			</tr>
			<tr>
				<th>
					<span>テキストエリア1</span>
					<br>
					<span class="button copy" data-copy-from="textarea1">コピー</span>
					<span class="button delete-all" data-delete-from="textarea1">全削除</span>
				</th>
				<td><textarea name="textarea1" id="textarea1" class="tagify">あいう、えお、かきく、けこ</td>
			</tr>
			<tr>
				<th>
					<span>テキストエリア2</span>
					<br>
					<span class="button copy" data-copy-from="textarea2">コピー</span>
					<span class="button delete-all" data-delete-from="textarea2">全削除</span>
				</th>
				<td><textarea name="textarea2" id="textarea2" class="tagify">さしす、せそ、なにぬ、ねの</td>
			</tr>
		</tbody>
	</table>
</form>
```

### 参考URL

* [Facebookのタグ入力欄のようなUIを実現するjQueryプラグイン](http://webrocketsmagazine.com/entry/20111109/facebook-like-tagging-jquery-plugin.html)
* [jQuery UI Autocompleteでかなを入力した際に漢字を表示](http://shanabrian.com/web/jquery/autocomplete01.php)
* [Clear form field after select for jQuery UI Autocomplete](https://stackoverflow.com/questions/2561903/clear-form-field-after-select-for-jquery-ui-autocomplete)
* [jQuery UI autocomplete select event not working with mouse click](https://stackoverflow.com/questions/7315556/jquery-ui-autocomplete-select-event-not-working-with-mouse-click)
* [changeではできない？JavaScriptでdivの変更を監視する方法](http://blog.keisuke11.com/webdesign/div-watch-js/)
* [jQuery on で後から追加した要素にもイベントを定義したい](https://webbibouroku.com/Blog/Article/jquery-on-after)