diff --git a/javascript/src/Helper/tagManager.js b/javascript/src/Helper/tagManager.js
new file mode 100644
index 0000000000000000000000000000000000000000..11df1404c1b3d4cc0757960278f3e538c7dc9d7b
--- /dev/null
+++ b/javascript/src/Helper/tagManager.js
@@ -0,0 +1,551 @@
+/* ===================================================
+ * tagmanager.js v3.0.2
+ * http://welldonethings.com/tags/manager
+ * ===================================================
+ * Copyright 2012 Max Favilli
+ *
+ * Licensed under the Mozilla Public License, Version 2.0 You may not use this work except in compliance with the License.
+ *
+ * http://www.mozilla.org/MPL/2.0/
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ * ========================================================== */
+(function ($) {
+
+    "use strict";
+
+    var defaults = {
+            prefilled: null,
+            CapitalizeFirstLetter: false,
+            preventSubmitOnEnter: true,     // deprecated
+            isClearInputOnEsc: true,        // deprecated
+            externalTagId: false,
+            prefillIdFieldName: 'Id',
+            prefillValueFieldName: 'Value',
+            AjaxPush: null,
+            AjaxPushAllTags: null,
+            AjaxPushParameters: null,
+            delimiters: [9, 13, 44],        // tab, enter, comma
+            backspace: [8],
+            maxTags: 0,
+            hiddenTagListName: null,        // deprecated
+            hiddenTagListId: null,          // deprecated
+            replace: true,
+            output: null,
+            deleteTagsOnBackspace: true,    // deprecated
+            tagsContainer: null,
+            tagCloseIcon: 'x',
+            tagClass: '',
+            validator: null,
+            onlyTagList: false,
+            tagList: null,
+            fillInputOnTagRemove: false,
+            AjaxPushDataType: 'json' //allow plugin to send data using different encodings (xml, json, script, text, html)
+        },
+
+        publicMethods = {
+            pushTag: function (tag, ignoreEvents, externalTagId, ignoreValidator) {
+                var $self = $(this), opts = $self.data('opts'), alreadyInList, tlisLowerCase, max, tagId,
+                    tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx, newTagId, newTagRemoveId, escaped,
+                    html, $el, lastTagId, lastTagObj;
+
+                tag = privateMethods.trimTag(tag, opts.delimiterChars);
+
+                if (!tag || tag.length <= 0) {
+                    return;
+                }
+
+                // check if restricted only to the tagList suggestions
+                if (opts.onlyTagList && undefined !== opts.tagList) {
+
+                    //if the list has been updated by look pushed tag in the tagList. if not found return
+                    if (opts.tagList) {
+                        var $tagList = opts.tagList;
+
+                        // change each array item to lower case
+                        $.each($tagList, function (index, item) {
+                            $tagList[index] = item.toLowerCase();
+                        });
+                        var suggestion = $.inArray(tag.toLowerCase(), $tagList);
+
+                        if (-1 === suggestion) {
+                            //console.log("tag:" + tag + " not in tagList, not adding it");
+                            return;
+                        }
+                    }
+
+                }
+
+                if (opts.CapitalizeFirstLetter && tag.length > 1) {
+                    tag = tag.charAt(0).toUpperCase() + tag.slice(1).toLowerCase();
+                }
+
+                // call the validator (if any) and do not let the tag pass if invalid
+                if (!ignoreValidator && opts.validator && !opts.validator(tag)) {
+                    $self.trigger('tm:invalid', tag);
+                    return;
+                }
+
+                // dont accept new tags beyond the defined maximum
+                if (opts.maxTags > 0 && tlis.length >= opts.maxTags) {
+                    return;
+                }
+
+                alreadyInList = false;
+                //use jQuery.map to make this work in IE8 (pure JS map is JS 1.6 but IE8 only supports JS 1.5)
+                tlisLowerCase = jQuery.map(tlis, function (elem) {
+                    return elem.toLowerCase();
+                });
+
+                idx = $.inArray(tag.toLowerCase(), tlisLowerCase);
+
+                if (-1 !== idx) {
+                    // console.log("tag:" + tag + " !!already in list!!");
+                    alreadyInList = true;
+                }
+
+                if (alreadyInList) {
+                    $self.trigger('tm:duplicated', tag);
+                    if (opts.blinkClass) {
+                        for (var i = 0; i < 6; ++i) {
+                            $("#" + $self.data("tm_rndid") + "_" + tlid[idx]).queue((function (_$, loc_opts, next) {
+                                _$(this).toggleClass(loc_opts.blinkClass);
+                                next();
+                            }).bind(this, $, opts)).delay(100);
+                        }
+                    } else {
+                        $("#" + $self.data("tm_rndid") + "_" + tlid[idx]).stop()
+                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
+                            .animate({backgroundColor: opts.blinkBGColor_2}, 100)
+                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
+                            .animate({backgroundColor: opts.blinkBGColor_2}, 100)
+                            .animate({backgroundColor: opts.blinkBGColor_1}, 100)
+                            .animate({backgroundColor: opts.blinkBGColor_2}, 100);
+                    }
+                } else {
+                    if (opts.externalTagId === true) {
+                        if (externalTagId === undefined) {
+                            $.error('externalTagId is not passed for tag -' + tag);
+                        }
+                        tagId = externalTagId;
+                    } else {
+                        max = Math.max.apply(null, tlid);
+                        max = max === -Infinity ? 0 : max;
+
+                        tagId = ++max;
+                    }
+                    if (!ignoreEvents) {
+                        $self.trigger('tm:pushing', [tag, tagId]);
+                    }
+                    tlis.push(tag);
+                    tlid.push(tagId);
+
+                    if (!ignoreEvents)
+                        if (opts.AjaxPush !== null && opts.AjaxPushAllTags == null) {
+                            if ($.inArray(tag, opts.prefilled) === -1) {
+                                $.post(opts.AjaxPush, $.extend({tag: tag}, opts.AjaxPushParameters), null, opts.AjaxPushDataType);
+                            }
+                        }
+
+                    // console.log("tagList: " + tlis);
+
+                    newTagId = $self.data("tm_rndid") + '_' + tagId;
+                    newTagRemoveId = $self.data("tm_rndid") + '_Remover_' + tagId;
+                    escaped = $("<span/>").text(tag).html();
+
+                    html = '<span class="' + privateMethods.tagClasses.call($self) + '" id="' + newTagId + '">';
+                    html += '<span>' + escaped + '</span>';
+                    html += '<a href="#" class="tm-tag-remove" id="' + newTagRemoveId + '" TagIdToRemove="' + tagId + '">';
+                    html += opts.tagCloseIcon + '</a></span> ';
+                    $el = $(html);
+
+
+                    var typeAheadMess = $self.parents('.twitter-typeahead')[0] !== undefined;
+                    if (opts.tagsContainer !== null) {
+                        $(opts.tagsContainer).append($el);
+                    } else {
+                        if (tlid.length > 1) {
+                            if (typeAheadMess) {
+                                lastTagId = $self.data("tm_rndid") + '_' + --tagId;
+                                jQuery('#' + lastTagId).after($el);
+                            } else {
+                                lastTagObj = $self.siblings("#" + $self.data("tm_rndid") + "_" + tlid[tlid.length - 2]);
+                                lastTagObj.after($el);
+                            }
+                        } else {
+                            if (typeAheadMess) {
+                                $self.parents('.twitter-typeahead').before($el);
+                            } else {
+                                $self.before($el);
+                            }
+                        }
+                    }
+
+                    $el.find("#" + newTagRemoveId).on("click", $self, function (e) {
+                        e.preventDefault();
+                        var TagIdToRemove = parseInt($(this).attr("TagIdToRemove"));
+                        privateMethods.spliceTag.call($self, TagIdToRemove, e.data);
+                    });
+
+                    if (!ignoreEvents) {
+                        $self.trigger('tm:pushed', [tag, tagId]);
+                    }
+
+                    privateMethods.refreshHiddenTagList.call($self);
+
+                    privateMethods.showOrHide.call($self);
+                    //if (tagManagerOptions.maxTags > 0 && tlis.length >= tagManagerOptions.maxTags) {
+                    //  obj.hide();
+                    //}
+                }
+                $self.val("");
+
+                // empty field
+                (this).typeahead('val', '');
+            },
+
+            popTag: function () {
+                var $self = $(this), tagId, tagBeingRemoved,
+                    tlis = $self.data("tlis"),
+                    tlid = $self.data("tlid");
+
+                if (tlid.length > 0) {
+                    tagId = tlid.pop();
+
+                    tagBeingRemoved = tlis[tlis.length - 1];
+                    $self.trigger('tm:popping', [tagBeingRemoved, tagId]);
+                    tlis.pop();
+
+                    // console.log("TagIdToRemove: " + tagId);
+                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
+                    privateMethods.refreshHiddenTagList.call($self);
+                    $self.trigger('tm:popped', [tagBeingRemoved, tagId]);
+
+                    privateMethods.showOrHide.call($self);
+                    // console.log(tlis);
+                }
+            },
+
+            empty: function () {
+                var $self = $(this), tlis = $self.data("tlis"), tlid = $self.data("tlid"), tagId;
+
+                while (tlid.length > 0) {
+                    tagId = tlid.pop();
+                    tlis.pop();
+                    // console.log("TagIdToRemove: " + tagId);
+                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
+                    privateMethods.refreshHiddenTagList.call($self);
+                    // console.log(tlis);
+                }
+                $self.trigger('tm:emptied', null);
+
+                privateMethods.showOrHide.call($self);
+                //if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
+                //  obj.show();
+                //}
+            },
+
+            tags: function () {
+                var $self = this, tlis = $self.data("tlis");
+                return tlis;
+            }
+        },
+
+        privateMethods = {
+            showOrHide: function () {
+                var $self = this, opts = $self.data('opts'), tlis = $self.data("tlis");
+
+                if (opts.maxTags > 0 && tlis.length < opts.maxTags) {
+                    $self.show();
+                    $self.trigger('tm:show');
+                }
+
+                if (opts.maxTags > 0 && tlis.length >= opts.maxTags) {
+                    $self.hide();
+                    $self.trigger('tm:hide');
+                }
+            },
+
+            tagClasses: function () {
+                var $self = $(this), opts = $self.data('opts'), tagBaseClass = opts.tagBaseClass,
+                    inputBaseClass = opts.inputBaseClass, cl;
+                // 1) default class (tm-tag)
+                cl = tagBaseClass;
+                // 2) interpolate from input class: tm-input-xxx --> tm-tag-xxx
+                if ($self.attr('class')) {
+                    $.each($self.attr('class').split(' '), function (index, value) {
+                        if (value.indexOf(inputBaseClass + '-') !== -1) {
+                            cl += ' ' + tagBaseClass + value.substring(inputBaseClass.length);
+                        }
+                    });
+                }
+                // 3) tags from tagClass option
+                cl += (opts.tagClass ? ' ' + opts.tagClass : '');
+                return cl;
+            },
+
+            trimTag: function (tag, delimiterChars) {
+                var i;
+                tag = $.trim(tag);
+                // truncate at the first delimiter char
+                i = 0;
+                for (i; i < tag.length; i++) {
+                    if ($.inArray(tag.charCodeAt(i), delimiterChars) !== -1) {
+                        break;
+                    }
+                }
+                return tag.substring(0, i);
+            },
+
+            refreshHiddenTagList: function () {
+                var $self = $(this), tlis = $self.data("tlis"), lhiddenTagList = $self.data("lhiddenTagList");
+
+                if (lhiddenTagList) {
+                    $(lhiddenTagList).val(tlis.join($self.data('opts').baseDelimiter)).change();
+                    $self.trigger('tm:hiddenUpdate', [tlis, $(lhiddenTagList)]);
+                }
+
+                $self.trigger('tm:refresh', tlis.join($self.data('opts').baseDelimiter));
+            },
+
+            killEvent: function (e) {
+                e.cancelBubble = true;
+                e.returnValue = false;
+                e.stopPropagation();
+                e.preventDefault();
+            },
+
+            keyInArray: function (e, ary) {
+                return $.inArray(e.which, ary) !== -1;
+            },
+
+            applyDelimiter: function (e) {
+                var $self = $(this);
+                publicMethods.pushTag.call($self, $(this).val());
+                e.preventDefault();
+            },
+
+            prefill: function (pta) {
+                var $self = $(this);
+                var opts = $self.data('opts');
+                $.each(pta, function (key, val) {
+                    if (opts.externalTagId === true) {
+                        publicMethods.pushTag.call($self, val[opts.prefillValueFieldName], true, val[opts.prefillIdFieldName], true);
+                    } else {
+                        publicMethods.pushTag.call($self, val, true, false, true);
+                    }
+                });
+            },
+
+            pushAllTags: function (e, tag) {
+                var $self = $(this), opts = $self.data('opts'), tlis = $self.data("tlis");
+                if (opts.AjaxPushAllTags) {
+                    if (e.type !== 'tm:pushed' || $.inArray(tag, opts.prefilled) === -1) {
+                        $.post(opts.AjaxPush, $.extend({tags: tlis.join(opts.baseDelimiter)}, opts.AjaxPushParameters));
+                    }
+                }
+            },
+
+            spliceTag: function (tagId) {
+                var $self = this, tlis = $self.data("tlis"), tlid = $self.data("tlid"), idx = $.inArray(tagId, tlid),
+                    tagBeingRemoved;
+
+                // console.log("TagIdToRemove: " + tagId);
+                // console.log("position: " + idx);
+
+                if (-1 !== idx) {
+                    tagBeingRemoved = tlis[idx];
+                    $self.trigger('tm:splicing', [tagBeingRemoved, tagId]);
+                    $("#" + $self.data("tm_rndid") + "_" + tagId).remove();
+                    tlis.splice(idx, 1);
+                    tlid.splice(idx, 1);
+                    privateMethods.refreshHiddenTagList.call($self);
+                    $self.trigger('tm:spliced', [tagBeingRemoved, tagId]);
+                    // console.log(tlis);
+                }
+
+                privateMethods.showOrHide.call($self);
+                //if (tagManagerOptions.maxTags > 0 && tlis.length < tagManagerOptions.maxTags) {
+                //  obj.show();
+                //}
+            },
+
+            init: function (options) {
+                var opts = $.extend({}, defaults, options), delimiters, keyNums;
+
+                opts.hiddenTagListName = (opts.hiddenTagListName === null) ? 'hidden-' + this.attr('name') : opts.hiddenTagListName;
+
+                delimiters = opts.delimeters || opts.delimiters; // 'delimeter' is deprecated
+                keyNums = [9, 13, 17, 18, 19, 37, 38, 39, 40]; // delimiter values to be handled as key codes
+                opts.delimiterChars = [];
+                opts.delimiterKeys = [];
+
+                $.each(delimiters, function (i, v) {
+                    if ($.inArray(v, keyNums) !== -1) {
+                        opts.delimiterKeys.push(v);
+                    } else {
+                        opts.delimiterChars.push(v);
+                    }
+                });
+
+                opts.baseDelimiter = String.fromCharCode(opts.delimiterChars[0] || 44);
+                opts.tagBaseClass = 'tm-tag';
+                opts.inputBaseClass = 'tm-input';
+
+                if (!$.isFunction(opts.validator)) {
+                    opts.validator = null;
+                }
+
+                this.each(function () {
+                    var $self = $(this), hiddenObj = '', rndid = '',
+                        albet = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
+
+                    // prevent double-initialization of TagManager
+                    if ($self.data('tagManager')) {
+                        return false;
+                    }
+                    $self.data('tagManager', true);
+
+                    for (var i = 0; i < 5; i++) {
+                        rndid += albet.charAt(Math.floor(Math.random() * albet.length));
+                    }
+
+                    $self.data("tm_rndid", rndid);
+
+                    // store instance-specific data in the DOM object
+                    $self.data('opts', opts)
+                        .data('tlis', []) //list of string tags
+                        .data('tlid', []); //list of ID of the string tags
+
+                    if (opts.output === null) {
+                        hiddenObj = $('<input/>', {
+                            type: 'hidden',
+                            name: opts.hiddenTagListName
+                        });
+                        $self.after(hiddenObj);
+                        $self.data("lhiddenTagList", hiddenObj);
+                    } else {
+                        $self.data("lhiddenTagList", $(opts.output));
+                    }
+
+                    if (opts.AjaxPushAllTags) {
+                        $self.on('tm:spliced', privateMethods.pushAllTags);
+                        $self.on('tm:popped', privateMethods.pushAllTags);
+                        $self.on('tm:pushed', privateMethods.pushAllTags);
+                    }
+
+                    // hide popovers on focus and keypress events
+                    $self.on('focus keypress', function (e) {
+                        if ($(this).popover) {
+                            $(this).popover('hide');
+                        }
+                    });
+
+                    // handle ESC (keyup used for browser compatibility)
+                    if (opts.isClearInputOnEsc) {
+                        $self.on('keyup', function (e) {
+                            if (e.which === 27) {
+                                // console.log('esc detected');
+                                $(this).val('');
+                                privateMethods.killEvent(e);
+                            }
+                        });
+                    }
+
+                    $self.on('keypress', function (e) {
+                        // push ASCII-based delimiters
+                        if (privateMethods.keyInArray(e, opts.delimiterChars)) {
+                            privateMethods.applyDelimiter.call($self, e);
+                        }
+                    });
+
+                    $self.on('keydown', function (e) {
+                        // disable ENTER
+                        if (e.which === 13) {
+                            if (opts.preventSubmitOnEnter) {
+                                privateMethods.killEvent(e);
+                            }
+                        }
+
+                        // push key-based delimiters (includes <enter> by default)
+                        if (privateMethods.keyInArray(e, opts.delimiterKeys)) {
+                            privateMethods.applyDelimiter.call($self, e);
+                        }
+                    });
+
+                    // BACKSPACE (keydown used for browser compatibility)
+                    if (opts.deleteTagsOnBackspace) {
+                        $self.on('keydown', function (e) {
+                            if (privateMethods.keyInArray(e, opts.backspace)) {
+                                // console.log("backspace detected");
+                                if ($(this).val().length <= 0) {
+                                    publicMethods.popTag.call($self);
+                                    privateMethods.killEvent(e);
+                                }
+                            }
+                        });
+                    }
+
+                    // on tag pop fill back the tag's content to the input field
+                    if (opts.fillInputOnTagRemove) {
+                        $self.on('tm:popped', function (e, tag) {
+                            $(this).val(tag);
+                        });
+                    }
+
+                    $self.change(function (e) {
+                        if (!/webkit/.test(navigator.userAgent.toLowerCase())) {
+                            $self.focus();
+                        } // why?
+
+                        /* unimplemented mode to push tag on blur
+                         else if (tagManagerOptions.pushTagOnBlur) {
+                         console.log('change: pushTagOnBlur ' + tag);
+                         pushTag($(this).val());
+                         } */
+                        privateMethods.killEvent(e);
+                    });
+
+                    if (opts.prefilled !== null) {
+                        if (typeof (opts.prefilled) === "object") {
+                            privateMethods.prefill.call($self, opts.prefilled);
+                        } else if (typeof (opts.prefilled) === "string") {
+                            privateMethods.prefill.call($self, opts.prefilled.split(opts.baseDelimiter));
+                        } else if (typeof (opts.prefilled) === "function") {
+                            privateMethods.prefill.call($self, opts.prefilled());
+                        }
+                    } else if (opts.output !== null) {
+                        if ($(opts.output) && $(opts.output).val()) {
+                            var existing_tags = $(opts.output);
+                        }
+                        privateMethods.prefill.call($self, $(opts.output).val().split(opts.baseDelimiter));
+                    }
+
+                });
+
+                return this;
+            }
+        };
+
+    $.fn.tagsManager = function (method) {
+        var $self = $(this);
+
+        if (!(0 in this)) {
+            return this;
+        }
+
+        if (publicMethods[method]) {
+            return publicMethods[method].apply($self, Array.prototype.slice.call(arguments, 1));
+        } else if (typeof method === 'object' || !method) {
+            return privateMethods.init.apply(this, arguments);
+        } else {
+            $.error('Method ' + method + ' does not exist.');
+            return false;
+        }
+    };
+
+}(jQuery));
\ No newline at end of file
diff --git a/javascript/src/TypeAhead.js b/javascript/src/TypeAhead.js
index 0a6e12b5410061523b3b5c3266790055fe553e1f..c510eea2584e4494d3d9c0e760c0559c9de22771 100644
--- a/javascript/src/TypeAhead.js
+++ b/javascript/src/TypeAhead.js
@@ -25,9 +25,11 @@ var QfqNS = QfqNS || {};
     n.TypeAhead.install = function (typeahead_endpoint) {
 
         $('.qfq-typeahead').each(function () {
-            var $shadowElement, bloodhoundConfiguration;
+            var bloodhoundConfiguration;
 
             var $element = $(this);
+
+            // bloodhound is used to get the remote data (suggestions)
             bloodhoundConfiguration = {
                 // We need to be notified on success, so we need a promise
                 initialize: false,
@@ -41,94 +43,221 @@ var QfqNS = QfqNS || {};
                     wildcard: '%QUERY'
                 }
             };
-            if ($element.val() !== '') {
-                // We prefetch the value provided
-                bloodhoundConfiguration.prefetch = {};
-                bloodhoundConfiguration.prefetch.url = n.TypeAhead.makePrefetchUrl(typeahead_endpoint, $element.val(), $element);
-                // Disable cache, we expect only a few entries. Caching gives sometimes strange behavior.
-                bloodhoundConfiguration.prefetch.cache = false;
+
+            // initialize typeahead (either with or without tags)
+            if ($element.data('typeahead-tags')) {
+                n.TypeAhead.installWithTags($element, bloodhoundConfiguration);
+            } else {
+                n.TypeAhead.installWithoutTags(typeahead_endpoint, $element, bloodhoundConfiguration);
             }
 
-            $shadowElement = n.TypeAhead.makeShadowElement($element);
 
-            var suggestions = new Bloodhound(bloodhoundConfiguration);
-            var promise = suggestions.initialize();
-            promise.done((function ($element, suggestions) {
-                return function () {
-                    n.TypeAhead.fillTypeAheadFromShadowElement($element, suggestions);
-                };
-            })($element, suggestions));
+        });
+
+    };
 
+    n.TypeAhead.installWithTags = function ($element, bloodhoundConfiguration) {
 
-            $element.typeahead({
-                    hint: n.TypeAhead.getHint($element),
-                    highlight: n.TypeAhead.getHighlight($element),
-                    minLength: n.TypeAhead.getMinLength($element)
-                },
-                {
-                    display: 'value',
-                    source: suggestions,
-                    limit: n.TypeAhead.getLimit($element),
-                    templates: {
-                        suggestion: function (obj) {
-                            return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
-                        },
-                        // No message if field is not set to pedantic.
-                        notFound: (function ($_) {
-                            return function (obj) {
-                                if (!!$_.data('typeahead-pedantic'))
-                                    return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
-                            };
-                        })($element)
-                    }
-                });
+        // initialize bloodhound (typeahead suggestion engine)
+        var suggestions = new Bloodhound(bloodhoundConfiguration);
+        suggestions.initialize();
 
+        // prevent form submit when enter key is pressed
+        $element.off('keyup');
+        $element.on('keypress keyup', function (e) {
+            var code = e.keyCode || e.which;
+            if (code === 13) {
+                e.preventDefault();
+                return false;
+            }
+        });
 
-            $element.bind('typeahead:select typeahead:autocomplete', function (event, suggestion) {
-                var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
-                $shadowElement.val(suggestion.key);
-            });
+        // list to keep tracks of existing tags and those added during the current session
+        // expected JSON format: [{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]
+        var existingTags = $element.val() !== '' ? JSON.parse($element.val()) : [];
 
-            if (!!$element.data('typeahead-pedantic')) {
-                $element.bind('typeahead:change', n.TypeAhead.makePedanticHandler(suggestions));
-                $element.on('keydown', (function (suggestions) {
-                    return function (event) {
-                        if (event.which === 13) {
-                            n.TypeAhead.makePedanticHandler(suggestions)(event);
-                        }
-                    };
-                })(suggestions));
-                // The pedantic handler will test if the shadow element has a value set (the KEY). If not, the
-                // typeahead element is cleared. Thus we have to guarantee that no value exists in the shadow
-                // element the instant the user starts typing since we don't know the outcome of the search.
-                //
-                // If we don't clear the shadow element the instant the user starts typing, and simply let the
-                // `typeahead:select` or `typeahead:autocomplete` handler set the selected value, the
-                // user might do following steps and end up in an inconsistent state:
-                //
-                //  1. Use typeahead to select/autocomplete a suggestion
-                //  2. delete the suggestion
-                //  3. enter a random string
-                //  4. submit form
-                //
-                // This would leave a stale value in the shadow element (from step 1.), and the pedantic handler
-                // would not clear the typeahead element, giving the impression the value in the typeahead element will be submitted.
-                $element.on('input', (function ($shadowElement) {
-                    return function () {
-                        $shadowElement.val('');
-                    };
-                })($shadowElement));
+        // list of current typeahead suggestions
+        var typeaheadList = existingTags.slice();
+
+        // get list of possible keys a user can press to push a tag (list of keycodes)
+        var delimiters = $element.data('typeahead-tag-delimiters');
+        delimiters = delimiters !== undefined ? delimiters : [9, 13, 44];
+
+        // validator function for pedantic mode
+        var pedanticValidator = function (tag) {
+            // check if tag is in typeahead suggestions
+            var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
+            return tagLookup !== undefined;
+        };
+
+        // initialize tagsManager
+        var tagApi = $element.tagsManager({
+            deleteTagsOnBackspace: false,
+            hiddenTagListName: $element.attr('name'),
+            tagClass: 'qfq-typeahead-tag',
+            delimiters: delimiters,
+            validator: !!$element.data('typeahead-pedantic') ? pedanticValidator : null,
+        });
+
+        // when tag is pushed, look up key and add it to existingTags
+        tagApi.bind('tm:pushed', function (e, tag) {
+            var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
+            if (undefined === tagLookup) {
+                existingTags.push({key: 0, value: tag});
             } else {
-                $element.bind('typeahead:change', function (event, suggestion) {
-                    var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
-                    // If none pendatic, suggestion key might not exist, so use suggestion instead.
-                    $shadowElement.val(suggestion.key || suggestion);
-                });
+                existingTags.push({key: tagLookup.key, value: tagLookup.value});
+            }
+        });
+
+        // when the hidden input field changes, overwrite value with tag object list
+        tagApi.bind('tm:hiddenUpdate', function (e, tags, hiddenInput) {
+            var tagObjects = $.map(tags, function (t) {
+                return existingTags.filter(function (tt) {return tt.value === t;})[0];
+            });
+            hiddenInput.val(JSON.stringify(tagObjects));
+        });
+        $element.removeAttr('name');
+
+        // add existing tags
+        $.each(existingTags, function (i, tag) {
+            tagApi.tagsManager('pushTag', tag.value);
+        });
+
+        // add typahead
+        $element.typeahead({
+                // options
+                hint: n.TypeAhead.getHint($element),
+                highlight: n.TypeAhead.getHighlight($element),
+                minLength: n.TypeAhead.getMinLength($element)
+            }, {
+                display: 'value',
+                source: suggestions,
+                limit: n.TypeAhead.getLimit($element),
+                templates: {
+                    suggestion: function (obj) {
+                        return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
+                    },
+                    // No message if field is not set to pedantic.
+                    notFound: (function ($_) {
+                        return function (obj) {
+                            if (!!$_.data('typeahead-pedantic'))
+                                return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
+                        };
+                    })($element)
             }
         });
 
+        // directly add tag when clicked on in typahead menu
+        $element.bind('typeahead:selected', function (event, sugg) {
+            tagApi.tagsManager("pushTag", sugg.value);
+        });
+
+        // update typahead list when typahead changes
+        $element.bind('typeahead:render', function (event, sugg) {
+            typeaheadList.length = 0;
+            typeaheadList.push.apply(typeaheadList, sugg);
+        });
     };
 
+    n.TypeAhead.installWithoutTags = function (typeahead_endpoint, $element, bloodhoundConfiguration) {
+        var $shadowElement;
+
+        // Prefetch the value that is already in the field
+        if ($element.val() !== '') {
+            bloodhoundConfiguration.prefetch = {};
+            bloodhoundConfiguration.prefetch.url = n.TypeAhead.makePrefetchUrl(typeahead_endpoint, $element.val(), $element);
+            // Disable cache, we expect only a few entries. Caching gives sometimes strange behavior.
+            bloodhoundConfiguration.prefetch.cache = false;
+        }
+
+        // create a shadow element with the same value. This seems to be important for the pedantic mode. (?)
+        $shadowElement = n.TypeAhead.makeShadowElement($element);
+
+        // prefetch data
+        var suggestions = new Bloodhound(bloodhoundConfiguration);
+        var promise = suggestions.initialize();
+
+        // use shadow element to back fill field value, if it is in the fetched suggestions (why?)
+        promise.done((function ($element, suggestions) {
+            return function () {
+                n.TypeAhead.fillTypeAheadFromShadowElement($element, suggestions);
+            };
+        })($element, suggestions));
+
+
+        $element.typeahead({
+                // options
+                hint: n.TypeAhead.getHint($element),
+                highlight: n.TypeAhead.getHighlight($element),
+                minLength: n.TypeAhead.getMinLength($element)
+            },
+            {
+                // dataset
+                display: 'value',
+                source: suggestions,
+                limit: n.TypeAhead.getLimit($element),
+                templates: {
+                    suggestion: function (obj) {
+                        return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
+                    },
+                    // No message if field is not set to pedantic.
+                    notFound: (function ($_) {
+                        return function (obj) {
+                            if (!!$_.data('typeahead-pedantic'))
+                                return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
+                        };
+                    })($element)
+                }
+            });
+
+
+        // bind select and autocomplete events
+        $element.bind('typeahead:select typeahead:autocomplete', function (event, suggestion) {
+            var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
+            $shadowElement.val(suggestion.key);
+        });
+
+        // bind change event
+        if (!!$element.data('typeahead-pedantic')) {
+            // Typeahead pedantic: Only allow suggested inputs
+            $element.bind('typeahead:change', n.TypeAhead.makePedanticHandler(suggestions));
+            $element.on('keydown', (function (suggestions) {
+                return function (event) {
+                    if (event.which === 13) {
+                        n.TypeAhead.makePedanticHandler(suggestions)(event);
+                    }
+                };
+            })(suggestions));
+            // The pedantic handler will test if the shadow element has a value set (the KEY). If not, the
+            // typeahead element is cleared. Thus we have to guarantee that no value exists in the shadow
+            // element the instant the user starts typing since we don't know the outcome of the search.
+            //
+            // If we don't clear the shadow element the instant the user starts typing, and simply let the
+            // `typeahead:select` or `typeahead:autocomplete` handler set the selected value, the
+            // user might do following steps and end up in an inconsistent state:
+            //
+            //  1. Use typeahead to select/autocomplete a suggestion
+            //  2. delete the suggestion
+            //  3. enter a random string
+            //  4. submit form
+            //
+            // This would leave a stale value in the shadow element (from step 1.), and the pedantic handler
+            // would not clear the typeahead element, giving the impression the value in the typeahead element will be submitted.
+            $element.on('input', (function ($shadowElement) {
+                return function () {
+                    $shadowElement.val('');
+                };
+            })($shadowElement));
+        } else {
+            $element.bind('typeahead:change', function (event, suggestion) {
+                var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget));
+                // If none pendatic, suggestion key might not exist, so use suggestion instead.
+                $shadowElement.val(suggestion.key || suggestion);
+            });
+        }
+    };
+
+
     n.TypeAhead.makePedanticHandler = function (bloodhound) {
         return function (event) {
             var $typeAhead = $(event.delegateTarget);
diff --git a/less/qfq-bs.css.less b/less/qfq-bs.css.less
index e4afadf3b075da5f7e5fbf21ec550143470ac5b6..39f3da3c300b60cdd43b367677c7a7afbc90c9ac 100644
--- a/less/qfq-bs.css.less
+++ b/less/qfq-bs.css.less
@@ -665,6 +665,11 @@ select.qfq-locked:invalid {
   width: 100%;
 }
 
+// typeAhead tags
+span.qfq-typeahead-tag {
+  margin-right: 5px;
+}
+
 @media (min-width: 768px) {
   .form-horizontal .control-label {
     text-align: unset;
diff --git a/mockup/typeahead.html b/mockup/typeahead.php
similarity index 79%
rename from mockup/typeahead.html
rename to mockup/typeahead.php
index 43c434258f2aed4c4e68de05a7dd1f75c287585b..129bbc3df7d89b1ab15f3f902c9178a32de9af30 100644
--- a/mockup/typeahead.html
+++ b/mockup/typeahead.php
@@ -124,6 +124,56 @@
 
         </div>
 
+        <?php
+
+        $tags = [
+            ['value' => "Alabama", 'key' => "AL"],
+            ['value' => "Alaska", 'key' => "AK"]
+        ];
+
+        $tagsSafeJson = htmlentities(json_encode($tags), ENT_QUOTES, 'UTF-8');
+
+        ?>
+
+        <div id="formgroup4" class="form-group">
+            <div class="col-md-2">
+                <label for="tags1" class="control-label">Text input 4 (tags)</label>
+            </div>
+
+            <div class="col-md-6">
+                <input id="tags1" type="text" class="form-control qfq-typeahead" name="tags1"
+                       data-typeahead-sip="abcdef"
+                       data-typeahead-limit="10"
+                       data-typeahead-minlength="1"
+
+                       data-typeahead-tags="true"
+                       data-typeahead-tag-delimiters="[9, 13]"
+                       value="<?php echo $tagsSafeJson; ?>"
+                       >
+            </div>
+
+        </div>
+
+        <div id="formgroup5" class="form-group">
+            <div class="col-md-2">
+                <label for="tags2" class="control-label">Text input 4 (tags, pedantic)</label>
+            </div>
+
+            <div class="col-md-6">
+                <input id="tags2" type="text" class="form-control qfq-typeahead" name="tags2"
+                       data-typeahead-sip="abcdef"
+                       data-typeahead-limit="10"
+                       data-typeahead-minlength="1"
+
+                       data-typeahead-tags="true"
+                       data-typeahead-pedantic="true"
+                       data-typeahead-tag-delimiters="[9, 44]"
+                       value="<?php echo $tagsSafeJson; ?>"
+                >
+            </div>
+
+        </div>
+
     </form>
 </div>