Skip to content
Snippets Groups Projects
Commit 000b6d3c authored by Marc Egger's avatar Marc Egger
Browse files

Merge branch 'marcTagsTypeahead' into 'develop'

Marc tags typeahead

See merge request !253
parents 069e3296 a9953016
No related branches found
No related tags found
3 merge requests!262Develop,!260F9686 - partly revert download filename sanitize,!253Marc tags typeahead
Pipeline #3247 passed
This diff is collapsed.
...@@ -25,9 +25,11 @@ var QfqNS = QfqNS || {}; ...@@ -25,9 +25,11 @@ var QfqNS = QfqNS || {};
n.TypeAhead.install = function (typeahead_endpoint) { n.TypeAhead.install = function (typeahead_endpoint) {
$('.qfq-typeahead').each(function () { $('.qfq-typeahead').each(function () {
var $shadowElement, bloodhoundConfiguration; var bloodhoundConfiguration;
var $element = $(this); var $element = $(this);
// bloodhound is used to get the remote data (suggestions)
bloodhoundConfiguration = { bloodhoundConfiguration = {
// We need to be notified on success, so we need a promise // We need to be notified on success, so we need a promise
initialize: false, initialize: false,
...@@ -41,94 +43,221 @@ var QfqNS = QfqNS || {}; ...@@ -41,94 +43,221 @@ var QfqNS = QfqNS || {};
wildcard: '%QUERY' wildcard: '%QUERY'
} }
}; };
if ($element.val() !== '') {
// We prefetch the value provided // initialize typeahead (either with or without tags)
bloodhoundConfiguration.prefetch = {}; if ($element.data('typeahead-tags')) {
bloodhoundConfiguration.prefetch.url = n.TypeAhead.makePrefetchUrl(typeahead_endpoint, $element.val(), $element); n.TypeAhead.installWithTags($element, bloodhoundConfiguration);
// Disable cache, we expect only a few entries. Caching gives sometimes strange behavior. } else {
bloodhoundConfiguration.prefetch.cache = false; 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({ // initialize bloodhound (typeahead suggestion engine)
hint: n.TypeAhead.getHint($element), var suggestions = new Bloodhound(bloodhoundConfiguration);
highlight: n.TypeAhead.getHighlight($element), suggestions.initialize();
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)
}
});
// 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) { // list to keep tracks of existing tags and those added during the current session
var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget)); // expected JSON format: [{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]
$shadowElement.val(suggestion.key); var existingTags = $element.val() !== '' ? JSON.parse($element.val()) : [];
});
if (!!$element.data('typeahead-pedantic')) { // list of current typeahead suggestions
$element.bind('typeahead:change', n.TypeAhead.makePedanticHandler(suggestions)); var typeaheadList = existingTags.slice();
$element.on('keydown', (function (suggestions) {
return function (event) { // get list of possible keys a user can press to push a tag (list of keycodes)
if (event.which === 13) { var delimiters = $element.data('typeahead-tag-delimiters');
n.TypeAhead.makePedanticHandler(suggestions)(event); delimiters = delimiters !== undefined ? delimiters : [9, 13, 44];
}
}; // validator function for pedantic mode
})(suggestions)); var pedanticValidator = function (tag) {
// The pedantic handler will test if the shadow element has a value set (the KEY). If not, the // check if tag is in typeahead suggestions
// typeahead element is cleared. Thus we have to guarantee that no value exists in the shadow var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
// element the instant the user starts typing since we don't know the outcome of the search. return tagLookup !== undefined;
// };
// 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 // initialize tagsManager
// user might do following steps and end up in an inconsistent state: var tagApi = $element.tagsManager({
// deleteTagsOnBackspace: false,
// 1. Use typeahead to select/autocomplete a suggestion hiddenTagListName: $element.attr('name'),
// 2. delete the suggestion tagClass: 'qfq-typeahead-tag',
// 3. enter a random string delimiters: delimiters,
// 4. submit form validator: !!$element.data('typeahead-pedantic') ? pedanticValidator : null,
// });
// 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. // when tag is pushed, look up key and add it to existingTags
$element.on('input', (function ($shadowElement) { tagApi.bind('tm:pushed', function (e, tag) {
return function () { var tagLookup = typeaheadList.filter(function (t) {return t.value.toLowerCase() === tag.toLowerCase();})[0];
$shadowElement.val(''); if (undefined === tagLookup) {
}; existingTags.push({key: 0, value: tag});
})($shadowElement));
} else { } else {
$element.bind('typeahead:change', function (event, suggestion) { existingTags.push({key: tagLookup.key, value: tagLookup.value});
var $shadowElement = n.TypeAhead.getShadowElement($(event.delegateTarget)); }
// If none pendatic, suggestion key might not exist, so use suggestion instead. });
$shadowElement.val(suggestion.key || suggestion);
}); // 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) { n.TypeAhead.makePedanticHandler = function (bloodhound) {
return function (event) { return function (event) {
var $typeAhead = $(event.delegateTarget); var $typeAhead = $(event.delegateTarget);
......
...@@ -665,6 +665,11 @@ select.qfq-locked:invalid { ...@@ -665,6 +665,11 @@ select.qfq-locked:invalid {
width: 100%; width: 100%;
} }
// typeAhead tags
span.qfq-typeahead-tag {
margin-right: 5px;
}
@media (min-width: 768px) { @media (min-width: 768px) {
.form-horizontal .control-label { .form-horizontal .control-label {
text-align: unset; text-align: unset;
......
...@@ -124,6 +124,56 @@ ...@@ -124,6 +124,56 @@
</div> </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> </form>
</div> </div>
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment