Commit 4c46f967 authored by Marc Egger's avatar Marc Egger
Browse files

Merge branch 'marcTypeaheadInitialSuggestions' into F9517TagInput

parents 1d6abb6f 7ebd59f0
Pipeline #3340 passed with stages
in 3 minutes and 56 seconds
......@@ -41,12 +41,12 @@ call to `api/load.php` upon change.
## Typeahead
Typeahead capable text input elements will be defined by the following attributes:
### .class='qfq-typeahead'
### .data-typeahead-sip
The SIP will store:
The SIP will store:
Use with SQL: `typeAheadSql`
......@@ -56,24 +56,32 @@ Use with LDAP: `typeAheadLdap`
* `typeAheadLdapSearch`
* `typeAheadLdapValuePrintf`
* `typeAheadLdapKeyPrintf`
### .data-typeahead-limit
* Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value.
* Defines the limit of entries shown on the client. Default on client is 5. The server will always send a value.
The server default is 20.
### .data-typeahead-minlength
* Defines the string minlength, 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
### .data-typeahead-static-list
* JSON encoded list of suggestions, which are served with the HTML input element.
* If both this list and the attribute `data-typeahead-sip` are given, then the typeahead
suggestions are taken from both sources.
* Expected JSON example:
`[{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]`
## Tags Form Element
The tags form element depends on Typeahead by default. The following attributes define the tags form element, additional
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`
......@@ -88,10 +96,10 @@ Mockups can be found in `mockup/typahead.php`
### .value
* JSON encoded list of key value pairs of existing tags. e.g.
* 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.
......
......@@ -29,20 +29,24 @@ var QfqNS = QfqNS || {};
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,
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('key', 'value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: function (obj) {
return obj.key;
},
remote: {
url: n.TypeAhead.makeUrl(typeahead_endpoint, $element),
wildcard: '%QUERY'
}
};
//if a API Sip is given, bloodhound is used to get the remote data (suggestions)
if (n.TypeAhead.getSip($element)) {
bloodhoundConfiguration = {
// We need to be notified on success, so we need a promise
initialize: false,
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('key', 'value'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: function (obj) {
return obj.key;
},
remote: {
url: n.TypeAhead.makeUrl(typeahead_endpoint, $element),
wildcard: '%QUERY'
}
};
} else {
bloodhoundConfiguration = null;
}
// initialize typeahead (either with or without tags)
if ($element.data('typeahead-tags')) {
......@@ -58,10 +62,6 @@ var QfqNS = QfqNS || {};
n.TypeAhead.installWithTags = function ($element, bloodhoundConfiguration) {
// initialize bloodhound (typeahead suggestion engine)
var suggestions = new Bloodhound(bloodhoundConfiguration);
suggestions.initialize();
// create actual input field
var $inputField = $('<input/>', {
type: 'text',
......@@ -93,8 +93,23 @@ var QfqNS = QfqNS || {};
// expected JSON format: [{value: "Alaska", key: "AK"}, {value: "Alabama", key: "AL"}]
var existingTags = $element.val() !== '' ? JSON.parse($element.val()) : [];
// static list of tags
var staticList = $element.data('typeahead-static-list');
staticList = staticList !== undefined && staticList !== '' ? staticList : [];
// get initial suggestions
var initialSuggestions = $element.data('typeahead-initial-suggestion');
initialSuggestions = initialSuggestions !== undefined && initialSuggestions !== '' ? initialSuggestions : [];
$.each(initialSuggestions, function(i, tag) {
if (!staticList.filter(function(t) {return t.key === tag.key;})) {
staticList.push(tag);
}
});
// list of current typeahead suggestions
var typeaheadList = existingTags.slice();
var typeaheadListApi = [];
var typeaheadListStatic = [];
// get list of possible keys a user can press to push a tag (list of keycodes)
var delimiters = $element.data('typeahead-tag-delimiters');
......@@ -152,29 +167,64 @@ var QfqNS = QfqNS || {};
});
tagApi.tagsManager('disableHiddenUpdate', false);
// add typahead
$inputField.typeahead({
var notFound = (function ($_) {
return function (obj) {
if (!!$element.data('typeahead-pedantic'))
return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
};
})($inputField);
var typeaheadConfig = [
{
// options
hint: n.TypeAhead.getHint($element),
highlight: n.TypeAhead.getHighlight($element),
minLength: n.TypeAhead.getMinLength($element)
}, {
},
{
name: 'staticList',
display: 'value',
source: n.TypeAhead.substringMatcher(staticList, initialSuggestions),
limit: n.TypeAhead.getLimit($element),
templates: {
// header: '<h3 class="league-name">List</h3>',
suggestion: function (obj) {
return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
},
// only show not found for static list if API is not given.
notFound: bloodhoundConfiguration === null ? notFound : undefined
}
}
];
if (bloodhoundConfiguration !== null) {
// initialize bloodhound (typeahead suggestion engine)
var suggestions = new Bloodhound(bloodhoundConfiguration);
suggestions.initialize();
typeaheadConfig.push({
name: 'restApi',
display: 'value',
source: suggestions,
limit: n.TypeAhead.getLimit($element),
templates: {
// header: '<h3 class="league-name">API</h3>',
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 (!!$element.data('typeahead-pedantic'))
if (!!$element.data('typeahead-pedantic') && typeaheadList.length === 0)
return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
};
})($inputField)
}
});
}
});
}
// add typeahead
$inputField.typeahead.apply($inputField, typeaheadConfig);
// directly add tag when clicked on in typahead menu
$inputField.bind('typeahead:selected', function (event, sugg) {
......@@ -182,17 +232,25 @@ var QfqNS = QfqNS || {};
});
// update typahead list when typahead changes
$inputField.bind('typeahead:render', function (event, sugg) {
$inputField.bind('typeahead:render', function (event, sugg, asynch, datasetName) {
if (datasetName === 'restApi') {
typeaheadListApi.length = 0;
typeaheadListApi.push.apply(typeaheadListApi, sugg);
} else if (datasetName === 'staticList') {
typeaheadListStatic.length = 0;
typeaheadListStatic.push.apply(typeaheadListStatic, sugg);
}
typeaheadList.length = 0;
typeaheadList.push.apply(typeaheadList, sugg);
typeaheadList.push.apply(typeaheadList, typeaheadListApi);
typeaheadList.push.apply(typeaheadList, typeaheadListStatic);
});
};
n.TypeAhead.installWithoutTags = function (typeahead_endpoint, $element, bloodhoundConfiguration) {
var $shadowElement;
// Prefetch the value that is already in the field
if ($element.val() !== '') {
// Prefetch
if ($element.val() !== '' && bloodhoundConfiguration !== null) {
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.
......@@ -202,26 +260,74 @@ var QfqNS = QfqNS || {};
// 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();
// get static list of tags
var staticList = $element.data('typeahead-static-list');
staticList = staticList !== undefined && staticList !== '' ? staticList : [];
// 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));
// get initial suggestions
var initialSuggestions = $element.data('typeahead-initial-suggestion');
initialSuggestions = initialSuggestions !== undefined && initialSuggestions !== '' ? initialSuggestions : [];
$.each(initialSuggestions, function(i, tag) {
if (!staticList.filter(function(t) {return t.key === tag.key;})) {
staticList.push(tag);
}
});
// get key from field value and fetch value from staticList (and later from API if SIP given)
var filledFromStaticList = n.TypeAhead.fillTypeAheadFromShadowElementStaticList($element, staticList);
$element.typeahead({
// list of current typeahead suggestions
var typeaheadList = [];
var typeaheadListApi = [];
var typeaheadListStatic = [];
var notFound = (function ($_) {
return function (obj) {
if (!!$element.data('typeahead-pedantic'))
return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
};
})($element);
var typeaheadConfig = [
{
// options
hint: n.TypeAhead.getHint($element),
highlight: n.TypeAhead.getHighlight($element),
minLength: n.TypeAhead.getMinLength($element)
},
{
// dataset
name: 'staticList',
display: 'value',
source: n.TypeAhead.substringMatcher(staticList, initialSuggestions),
limit: n.TypeAhead.getLimit($element),
templates: {
// header: '<h3 class="league-name">List</h3>',
suggestion: function (obj) {
return "<div>" + n.TypeAhead.htmlEncode(obj.value) + "</div>";
},
// only show not found for static list if API is not given.
notFound: bloodhoundConfiguration === null ? notFound : undefined
}
}
];
if (bloodhoundConfiguration !== null) {
// initialize bloodhound (typeahead suggestion engine)
var suggestions = new Bloodhound(bloodhoundConfiguration);
var promise = suggestions.initialize();
// get key from field value and fetch value from API, if not already filled from static list
if(!filledFromStaticList) {
promise.done((function ($element, suggestions) {
return function () {
n.TypeAhead.fillTypeAheadFromShadowElement($element, suggestions);
};
})($element, suggestions));
}
typeaheadConfig.push({
name: 'restApi',
display: 'value',
source: suggestions,
limit: n.TypeAhead.getLimit($element),
......@@ -232,13 +338,30 @@ var QfqNS = QfqNS || {};
// No message if field is not set to pedantic.
notFound: (function ($_) {
return function (obj) {
if (!!$_.data('typeahead-pedantic'))
if (!!$element.data('typeahead-pedantic') && typeaheadList.length === 0)
return "<div>'" + n.TypeAhead.htmlEncode(obj.query) + "' not found";
};
})($element)
}
});
}
// add typahead
$element.typeahead.apply($element, typeaheadConfig);
// update typahead list when typahead changes
$element.bind('typeahead:render', function (event, sugg, asynch, datasetName) {
if (datasetName === 'restApi') {
typeaheadListApi.length = 0;
typeaheadListApi.push.apply(typeaheadListApi, sugg);
} else if (datasetName === 'staticList') {
typeaheadListStatic.length = 0;
typeaheadListStatic.push.apply(typeaheadListStatic, sugg);
}
typeaheadList.length = 0;
typeaheadList.push.apply(typeaheadList, typeaheadListApi);
typeaheadList.push.apply(typeaheadList, typeaheadListStatic);
});
// bind select and autocomplete events
$element.bind('typeahead:select typeahead:autocomplete', function (event, suggestion) {
......@@ -249,14 +372,14 @@ var QfqNS = QfqNS || {};
// 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) {
$element.bind('typeahead:change', n.TypeAhead.makePedanticHandler());
$element.on('keydown', function () {
return function (event) {
if (event.which === 13) {
n.TypeAhead.makePedanticHandler(suggestions)(event);
n.TypeAhead.makePedanticHandler()(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.
......@@ -286,8 +409,29 @@ var QfqNS = QfqNS || {};
}
};
n.TypeAhead.substringMatcher = function(tags, initialSuggestions) {
return function findMatches(query, sync) {
if (query === '') {
sync(initialSuggestions);
} else {
// an array that will be populated with substring matches
var matches = [];
// regex used to determine if a string contains the substring `q`
var substrRegex = new RegExp(query, 'i');
// iterate through the pool of strings and for any string that
// contains the substring `q`, add it to the `matches` array
$.each(tags, function(i, tag) {
if (substrRegex.test(tag.value)) {
matches.push(tag);
}
});
sync(matches);
}
};
};
n.TypeAhead.makePedanticHandler = function (bloodhound) {
n.TypeAhead.makePedanticHandler = function () {
return function (event) {
var $typeAhead = $(event.delegateTarget);
var $shadowElement = n.TypeAhead.getShadowElement($typeAhead);
......@@ -322,7 +466,11 @@ var QfqNS = QfqNS || {};
};
n.TypeAhead.getMinLength = function ($element) {
return $element.data('typeahead-minlength') || 2;
if ($element.data('typeahead-initial-suggestion') !== undefined) {
return 0;
} else {
return $element.data('typeahead-minlength') || 2;
}
};
n.TypeAhead.getHighlight = function ($element) {
......@@ -376,4 +524,20 @@ var QfqNS = QfqNS || {};
}
$element.typeahead('val', results[0].value);
};
n.TypeAhead.fillTypeAheadFromShadowElementStaticList = function ($element, staticList) {
var $shadowElement = n.TypeAhead.getShadowElement($element);
var key = $shadowElement.val();
if (key === '') {
return false;
}
var tagLookup = staticList.filter(function (t) {return t.key === key;})[0];
if (undefined !== tagLookup) {
$element.typeahead('val', tagLookup.value);
return true;
} else {
return false;
}
};
})(QfqNS);
\ No newline at end of file
......@@ -79,6 +79,35 @@
</div>
<?php
$staticList = [
['value' => "duplicate", 'key' => "duplicate"],
['value' => "one", 'key' => "1"],
['value' => "two", 'key' => "2"],
['value' => "three", 'key' => "3"],
['value' => "four", 'key' => "4"],
['value' => "five", 'key' => "5"],
['value' => "six", 'key' => "6"],
['value' => "seven", 'key' => "7"],
];
$staticListSafeJson = htmlentities(json_encode($staticList), ENT_QUOTES, 'UTF-8');
$initialSuggestions = [
['value' => "duplicate", 'key' => "duplicate"],
['value' => "one2", 'key' => "1x"],
['value' => "two2", 'key' => "2x"],
['value' => "three2", 'key' => "3x"],
['value' => "four2", 'key' => "4x"],
['value' => "five2", 'key' => "5x"],
['value' => "six2", 'key' => "6x"],
['value' => "seven2", 'key' => "7x"],
];
$initialSuggestionsSafeJson = htmlentities(json_encode($initialSuggestions), ENT_QUOTES, 'UTF-8');
?>
<form id="myForm" class="form-horizontal" data-toggle="validator">
......@@ -89,7 +118,12 @@
<div class="col-md-6">
<input id="dropdown1" type="text" class="form-control qfq-typeahead" name="dropdown1"
data-typeahead-sip="abcde" data-typeahead-minlength="1" data-typeahead-limit="3">
data-typeahead-sip="abcde"
data-typeahead-minlength="1"
data-typeahead-limit="3"
data-typeahead-static-list="<?php echo $staticListSafeJson; ?>"
data-typeahead-initial-suggestion="<?php echo $initialSuggestionsSafeJson; ?>"
>
</div>
</div>
......@@ -102,7 +136,8 @@
<div class="col-md-6">
<input id="dropdown2" type="text" class="form-control qfq-typeahead" name="dropdown2"
data-typeahead-sip="abcdef" data-typeahead-limit="10" data-typeahead-minlength="1"
data-typeahead-pedantic="true" required>
data-typeahead-pedantic="true" required
data-typeahead-static-list="<?php echo $staticListSafeJson; ?>" >
</div>
<div class="col-md-4">
......@@ -141,13 +176,15 @@
</div>
<div class="col-md-6">
<input id="tags1" type="hidden" class="form-control qfq-typeahead" name="tags1"
<input id="tags1" type="hidden" 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]"
data-typeahead-static-list="<?php echo $staticListSafeJson; ?>"
data-typeahead-initial-suggestion="<?php echo $initialSuggestionsSafeJson; ?>"
value="<?php echo $tagsSafeJson; ?>"
>
</div>
......@@ -160,7 +197,7 @@
</div>
<div class="col-md-6">
<input id="tags2" type="hidden" class="form-control qfq-typeahead" name="tags2"
<input id="tags2" type="hidden" class="form-control qfq-typeahead" name="tags2"
data-typeahead-sip="abcdef"
data-typeahead-limit="10"
data-typeahead-minlength="1"
......@@ -168,6 +205,8 @@
data-typeahead-tags="true"
data-typeahead-pedantic="true"
data-typeahead-tag-delimiters="[9, 44]"
data-typeahead-static-list="<?php echo $staticListSafeJson; ?>"
data-typeahead-initial-suggestion="<?php echo $initialSuggestionsSafeJson; ?>"
value="<?php echo $tagsSafeJson; ?>"
>
</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