Tags input component with support for space in tagname

1.7k views Asked by At

I need an input field capable of taking tags with spaces in tagname like shown below:

enter image description here

I know that the <p:autoComplete multiple="true"> supports taking tags, but it uses space to separate tags:

enter image description here

I'd like to include space in the tagname and use ; or Enter to advance to next tag. How can I create a new custom component, or customize the <p:autoComplete> for this? I'm using JSF 2.2 and PrimeFaces 3.2.

2

There are 2 answers

0
AudioBubble On BEST ANSWER

Why not rewrite/modify it's JavaScript functions (in autocomplete.js) ?

code & functions of interest :

bindStaticEvents

bindStaticEvents: function() {
        var $this = this;

        this.bindKeyEvents();

        this.dropdown.mouseover(function() {
            $(this).addClass('ui-state-hover');
        }).mouseout(function() {
            $(this).removeClass('ui-state-hover');
        }).mousedown(function() {
            if($this.active) {
                $(this).addClass('ui-state-active');
            }
        }).mouseup(function() {
            if($this.active) {
                $(this).removeClass('ui-state-active');

                $this.search('');
                $this.input.focus();
            }
        }).focus(function() {
            $(this).addClass('ui-state-focus');
        }).blur(function() {
            $(this).removeClass('ui-state-focus');
        }).keydown(function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER || key === keyCode.NUMPAD_ENTER) {
                $(this).addClass('ui-state-active');
            }
        }).keyup(function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which;

            if(key === keyCode.SPACE || key === keyCode.ENTER || key === keyCode.NUMPAD_ENTER) {
                $(this).removeClass('ui-state-active');
                $this.search('');
                $this.input.focus();
                e.preventDefault();
                e.stopPropagation();
            }
        });

bindKeyEvents

bindKeyEvents: function() {
        var $this = this;

        this.currentText = this.input.val();
        this.previousText = this.input.val();

        //bind keyup handler
        this.input.keyup(function(e) {
            var keyCode = $.ui.keyCode,
            key = e.which,
            shouldSearch = true;

            $this.previousText = $this.currentText;
            $this.currentText = this.value;

            // Cancel a possible long running search when selecting an entry via enter
            if (key === keyCode.ENTER || key === keyCode.NUMPAD_ENTER) {
                if ($this.timeout) {
                    clearTimeout($this.timeout);
                }
                shouldSearch = false;
            }
            else if (key === keyCode.ESCAPE) {
                $this.hide();
                shouldSearch = false;
            }
            else if ((e.ctrlKey && key === 65) // ctrl+a
                || (e.ctrlKey && key === 67) // ctrl+c
                || key === keyCode.LEFT
                || key === keyCode.RIGHT
                || key === keyCode.TAB
                || key === 16 // keyCode.SHIFT
                || key === keyCode.HOME
                || key === keyCode.END
                || key === 18 // keyCode.ALT
                || key === 17 // keyCode.CONTROL
                || (key >= 112 && key <= 123)) { // F1-F12
                shouldSearch = false;
            }
            else if(key === keyCode.UP || key === keyCode.DOWN) {
                if($this.panel.is(':visible')) {
                    var highlightedItem = $this.items.filter('.ui-state-highlight');
                    if(highlightedItem.length) {
                        $this.displayAriaStatus(highlightedItem.data('item-label'));
                    }
                }

                shouldSearch = false;
            }
            else if($this.cfg.pojo && !$this.cfg.multiple && ($this.previousText !== $this.currentText)) {
                $this.hinput.val($(this).val());
            }

            if(shouldSearch) {
                var value = $this.input.val();

                if(!value.length) {
                    $this.hide();
                }

                if(value.length >= $this.cfg.minLength) {

                    //Cancel the search request if user types within the timeout
                    if($this.timeout) {
                        clearTimeout($this.timeout);
                    }

                    var delay = $this.cfg.delay;

                    if (value != '' && (key == keyCode.BACKSPACE || key == keyCode.DELETE)) {
                        delay = $this.cfg.deletionDelay;
                    }

                    $this.timeout = setTimeout(function() {
                        $this.search(value);
                    }, delay);
                }
            }

        }).keydown(function(e) {
            var keyCode = $.ui.keyCode;

            if($this.panel.is(':visible')) {
                var highlightedItem = $this.items.filter('.ui-state-highlight');

                switch(e.which) {
                    case keyCode.UP:
                        var prev = highlightedItem.length == 0 ? $this.items.eq(0) : highlightedItem.prevAll('.ui-autocomplete-item:first');

                        if(prev.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            prev.addClass('ui-state-highlight');

                            if($this.cfg.scrollHeight) {
                                PrimeFaces.scrollInView($this.panel, prev);
                            }

                            if($this.cfg.itemtip) {
                                $this.showItemtip(prev);
                            }
                        }

                        e.preventDefault();
                        break;

                    case keyCode.DOWN:
                        var next = highlightedItem.length == 0 ? $this.items.eq(0) : highlightedItem.nextAll('.ui-autocomplete-item:first');

                        if(next.length == 1) {
                            highlightedItem.removeClass('ui-state-highlight');
                            next.addClass('ui-state-highlight');

                            if($this.cfg.scrollHeight) {
                                PrimeFaces.scrollInView($this.panel, next);
                            }

                            if($this.cfg.itemtip) {
                                $this.showItemtip(next);
                            }
                        }

                        e.preventDefault();
                        break;

                    case keyCode.ENTER:
                    case keyCode.NUMPAD_ENTER:
                        highlightedItem.click();

                        e.preventDefault();
                        e.stopPropagation();
                        break;

                    case 18: //keyCode.ALT:
                    case 224:
                        break;

                    case keyCode.TAB:
                        highlightedItem.trigger('click');
                        $this.hide();
                        break;
                }
            }
            else if (e.which == keyCode.TAB) {
                // clear pending search before leaving the field
                if ($this.timeout) {
                    clearTimeout($this.timeout);
                }
            }
        });
    },

bindDynamicEvents

 bindDynamicEvents: function() {
        var $this = this;

        //visuals and click handler for items
        this.items.bind('mouseover', function() {
            var item = $(this);
        if(!item.hasClass('ui-state-highlight')) {
            $this.items.filter('.ui-state-highlight').removeClass('ui-state-highlight');
            item.addClass('ui-state-highlight');

            if($this.cfg.itemtip) {
                $this.showItemtip(item);
            }
        }
    })
    .bind('click', function(event) {
        var item = $(this),
        itemValue = item.attr('data-item-value');

        if($this.cfg.multiple) {
            var itemDisplayMarkup = '<li data-token-value="' + item.attr('data-item-value') + '"class="ui-autocomplete-token ui-state-active ui-corner-all ui-helper-hidden">';
            itemDisplayMarkup += '<span class="ui-autocomplete-token-icon ui-icon ui-icon-close" />';
            itemDisplayMarkup += '<span class="ui-autocomplete-token-label">' + item.attr('data-item-label') + '</span></li>';

            $this.inputContainer.before(itemDisplayMarkup);
            $this.multiItemContainer.children('.ui-helper-hidden').fadeIn();
            $this.input.val('').focus();

            $this.hinput.append('<option value="' + itemValue + '" selected="selected"></option>');
        }
        else {
            $this.input.val(item.attr('data-item-label')).focus();

            this.currentText = $this.input.val();
            this.previousText = $this.input.val();

            if($this.cfg.pojo) {
                $this.hinput.val(itemValue);
            }
        }

        $this.invokeItemSelectBehavior(event, itemValue);

        $this.hide();
    });
},

Suggestions : Replace key === keyCode.SPACE with key === 188 (which is the code for ;)

Note : It might have some side effects, especially if you are using autocomplete somewhere else and differently..

1
Ali Cheaito On

Take a look at this jquery plugin that allows customization around the tags which can allow spaces (see example page).

You'll want to create a composite component with an h:input, then apply the jquery plugin to it. That'll give you something similar to the language tags in stackoverflow.

The composite component would aggregate the hidden inputs that tagit generates using javascript and sets the value of an h:inputHidden component. Something like this:

<ui:composition xmlns="http://www.w3.org/1999/xhtml"
xmlns:h="http://java.sun.com/jsf/html"
xmlns:cc="http://java.sun.com/jsf/composite"
xmlns:ui="http://java.sun.com/jsf/facelets">

<cc:interface shortDescription="TagIt wrapper">
    <cc:attribute name="val" type="java.lang.String" required="true" shortDescription="value" />        
</cc:interface>

<cc:implementation>

    <h:outputStylesheet library="cui" name="jquery.tagit.css" target="head" />
    <h:outputStylesheet library="cui" name="tagit.ui-zendesk.css" target="head" />

    <h:outputScript library="primefaces" name="jquery/jquery.js" target="head" />
    <h:outputScript library="cui" name="jquery-ui.min.js" target="head" />
    <h:outputScript library="cui" name="tag-it.min.js" target="head" />             

    <h:outputScript>                                
        $(function() {
            //Activate the tagit plugin with the allow spaces option. 
            //we insert the clientId to insure uniqueness of ids in case the component 
            //was used multiple times on the same page. We must escape JSF's column 
            //separator since it's considered a special character for jQuery
            var tagId = ("#" + "#{cc.clientId}:myTags".replace(/:/g, "\\:"));                           
            $(tagId).tagit({allowSpaces: true});                              

            //find the input element generated by the plugin and whenever it loses focus, 
            //update the hidden input with the values of all the tags separated by semi-columns
            //The hidden input is wired to the 'val' attribute and will feed the value to the backing bean             
            $("input[type='text'].ui-widget-content").blur(function() {                                                     
                var allTags = "";
                $('.tagit-hidden-field').each(function() {
                    allTags += $(this).val() + ";"                      
                });
                var hiddenInputId = ("#" + "#{cc.clientId}:tagValue".replace(/:/g, "\\:"));                                
                $(hiddenInputId).val(allTags);
            });
        });                
    </h:outputScript>

    <h:inputHidden id="tagValue" value="#{cc.attrs.val}"/>

    <p>Hit Enter or Comma to separate tags</p>

    <ul id="#{cc.clientId}:myTags">         
    </ul>

</cc:implementation>

You would then use it on the page like this:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
    xmlns:h="http://java.sun.com/jsf/html"  
    xmlns:cui="http://java.sun.com/jsf/composite/cui">
<h:head></h:head>
<h:body>
    <h:form id="form">      
        <cui:tagit val="#{page1.tags}" />                                   
        <h:commandButton value="Submit">
          <f:ajax execute="@form" render="output"/>
        </h:commandButton>
    </h:form>
</h:body>
</html>