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>