Commit 6b97ebf1 authored by Carsten  Rose's avatar Carsten Rose
Browse files

Merge remote-tracking branch 'origin/develop' into develop

parents f29f1a8a 2093d735
Pipeline #3274 failed with stages
in 5 minutes and 31 seconds
......@@ -65,3 +65,34 @@ Use with LDAP: `typeAheadLdap`
* Defines the string minlegth, typed by the user, before the first lookup is started. Default is 2.
### data-typeahead-pedantic
* If present, only suggested values are allowed in the input element
## Tags Form Element
The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional to the attributes for Typeahead (see above).
Mockups can be found in `mockup/typahead.php`
### .data-typeahead-tags
* If present, the field becomes a tag field
### .data-typeahead-tag-delimiters
* List of ascii key codes of the keys which may be pressed to add a new tag when typing.
### .value
* JSON encoded list of key value pairs of existing tags. e.g.
`[{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]`
### POST data
* JSON encoded list of key value pairs of the selected tags. e.g.
`[{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]`
This diff is collapsed.
......@@ -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);
......
......@@ -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;
......
......@@ -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>
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment