-
-
Notifications
You must be signed in to change notification settings - Fork 213
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Web Apps: switched combobox to select2 #28701
Merged
Merged
Changes from 9 commits
Commits
Show all changes
19 commits
Select commit
Hold shift + click to select a range
ab38860
Fixed bindings for dropdown template
orangejenny 288c96f
Switched combobox to use dropdown template and select2 for rendering
orangejenny aaacd10
Added translations
orangejenny 439b922
Combined DropdownEntry and ComboboxEntry to use the same UI
orangejenny 11575ac
Removed dropdown help text for consistency with prod
orangejenny 6ea3bcb
Removed validation
orangejenny 561a4cb
Added select2 to app preview template
orangejenny 70f0448
Updated to handle blank default value
orangejenny 85f8ded
Extracted ComboboxEntry.filter again to make it available to tests
orangejenny d7783e3
Fixed lint error
orangejenny ac807f5
Added select2 to tests
orangejenny 36ac455
Restored onPreProcess and validation code
orangejenny 367dd0d
Updated tests to reflect changes in entry's options
orangejenny 0c19009
Restored additional validation code
orangejenny 6c72078
Restored valueAllowUnset binding
orangejenny 3fb2a76
Updated ComboboxEntry to use index for both answer and rawAnswer
orangejenny 1482c36
Updated select2 so UI reflects programmatic value
orangejenny c39f408
Updated dropdown and combobox to clear answer when options change
orangejenny 242a564
Moved clearing logic to dropdown instead of combobox
orangejenny File filter
Filter by extension
Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,4 @@ | ||
/* globals moment, MapboxGeocoder */ | ||
/* globals moment, MapboxGeocoder, DOMPurify */ | ||
hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | ||
var Const = hqImport("cloudcare/js/form_entry/const"), | ||
Utils = hqImport("cloudcare/js/form_entry/utils"), | ||
|
@@ -389,7 +389,7 @@ hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | |
}; | ||
|
||
this.helpText = function () { | ||
return 'Phone number or Numeric ID'; | ||
return gettext('Phone number or Numeric ID'); | ||
}; | ||
|
||
this.enableReceiver(question, options); | ||
|
@@ -418,7 +418,7 @@ hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | |
}; | ||
|
||
this.helpText = function () { | ||
return 'Decimal'; | ||
return gettext('Decimal'); | ||
}; | ||
} | ||
FloatEntry.prototype = Object.create(IntEntry.prototype); | ||
|
@@ -530,15 +530,40 @@ hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | |
var self = this; | ||
EntrySingleAnswer.call(this, question, options); | ||
self.templateType = 'dropdown'; | ||
self.placeholderText = gettext('Please choose an item'); | ||
|
||
self.options = ko.pureComputed(function () { | ||
return _.map(question.choices(), function (choice, idx) { | ||
self.helpText = function () { | ||
return ""; | ||
}; | ||
|
||
self.options = ko.computed(function () { | ||
return [{text: "", id: undefined}].concat(_.map(question.choices(), function (choice, idx) { | ||
return { | ||
text: choice, | ||
idx: idx + 1, | ||
id: idx + 1, | ||
}; | ||
}); | ||
})); | ||
}); | ||
|
||
self.options.subscribe(function () { | ||
self.renderSelect2(); | ||
}); | ||
|
||
self.additionalSelect2Options = function () { | ||
return {}; | ||
}; | ||
self.renderSelect2 = function () { | ||
var $input = $('#' + self.entryId); | ||
$input.select2(_.extend({ | ||
allowClear: true, | ||
placeholder: self.placeholderText, | ||
escapeMarkup: function (m) { return DOMPurify.sanitize(m); }, | ||
}, self.additionalSelect2Options())); | ||
}; | ||
|
||
self.afterRender = function () { | ||
self.renderSelect2(); | ||
}; | ||
} | ||
DropdownEntry.prototype = Object.create(EntrySingleAnswer.prototype); | ||
DropdownEntry.prototype.constructor = EntrySingleAnswer; | ||
|
@@ -556,115 +581,67 @@ hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | |
* when the user specifies combobox in the appearance attributes for a | ||
* single select question. | ||
* | ||
* It uses the same UI as the dropdown, but a different matching algorithm. | ||
* | ||
* Docs: https://confluence.dimagi.com/display/commcarepublic/Advanced+CommCare+Android+Formatting#AdvancedCommCareAndroidFormatting-SingleSelect"ComboBox" | ||
*/ | ||
function ComboboxEntry(question, options) { | ||
var self = this, | ||
initialOption; | ||
EntrySingleAnswer.call(this, question, options); | ||
DropdownEntry.call(this, question, options); | ||
|
||
// Specifies the type of matching we will do when a user types a query | ||
self.matchType = options.matchType; | ||
self.lengthLimit = Infinity; | ||
self.templateType = 'str'; | ||
self.placeholderText = gettext('Type to filter answers'); | ||
|
||
self.options = ko.computed(function () { | ||
return _.map(question.choices(), function (choice, idx) { | ||
return { | ||
name: choice, | ||
id: idx + 1, | ||
}; | ||
}); | ||
}); | ||
self.options.subscribe(function () { | ||
self.renderAtwho(); | ||
if (!self.isValid(self.rawAnswer())) { | ||
self.question.error(gettext('Not a valid choice')); | ||
} | ||
}); | ||
self.helpText = function () { | ||
return 'Combobox'; | ||
return gettext('Combobox'); | ||
}; | ||
|
||
// If there is a prexisting answer, set the rawAnswer to the corresponding text. | ||
if (question.answer()) { | ||
initialOption = self.options()[self.answer() - 1]; | ||
self.rawAnswer( | ||
initialOption ? initialOption.name : Const.NO_ANSWER | ||
); | ||
} | ||
|
||
self.renderAtwho = function () { | ||
var $input = $('#' + self.entryId), | ||
limit = Infinity, | ||
$atwhoView; | ||
$input.atwho('destroy'); | ||
$input.atwho('setIframe', window.frameElement, true); | ||
$input.atwho({ | ||
at: '', | ||
data: self.options(), | ||
maxLen: Infinity, | ||
tabSelectsMatch: false, | ||
limit: limit, | ||
suffix: '', | ||
callbacks: { | ||
filter: function (query, data) { | ||
var results = _.filter(data, function (item) { | ||
return ComboboxEntry.filter(query, item, self.matchType); | ||
}); | ||
$atwhoView = $('.atwho-container .atwho-view'); | ||
$atwhoView.attr({ | ||
'data-message': 'Showing ' + Math.min(limit, results.length) + ' of ' + results.length, | ||
}); | ||
return results; | ||
}, | ||
matcher: function () { | ||
return $input.val(); | ||
}, | ||
sorter: function (query, data) { | ||
return data; | ||
}, | ||
}, | ||
}); | ||
self.additionalSelect2Options = function () { | ||
return { | ||
matcher: function (params, option) { | ||
var query = $.trim(params.term); | ||
if (ComboboxEntry.filter(query, option, self.matchType)) { | ||
return option; | ||
} else { | ||
return null; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing trailing comma. (comma-dangle) |
||
}; | ||
}; | ||
|
||
self.isValid = function (value) { | ||
if (!value) { | ||
return true; | ||
} | ||
return _.include( | ||
_.map(self.options(), function (option) { | ||
return option.name; | ||
}), | ||
value | ||
); | ||
}; | ||
|
||
self.afterRender = function () { | ||
self.renderAtwho(); | ||
return _.contains(self.choices(), value); | ||
}; | ||
|
||
self.enableReceiver(question, options); | ||
} | ||
|
||
ComboboxEntry.filter = function (query, d, matchType) { | ||
ComboboxEntry.filter = function (query, option, matchType) { | ||
if (!query || !option.text) { | ||
return true; | ||
} | ||
|
||
var match; | ||
if (matchType === Const.COMBOBOX_MULTIWORD) { | ||
// Multiword filter, matches any choice that contains all of the words in the query | ||
// | ||
// Assumption is both query and choice will not be very long. Runtime is O(nm) | ||
// where n is number of words in the query, and m is number of words in the choice | ||
var wordsInQuery = query.split(' '); | ||
var wordsInChoice = d.name.split(' '); | ||
var wordsInChoice = option.text.split(' '); | ||
|
||
match = _.all(wordsInQuery, function (word) { | ||
return _.include(wordsInChoice, word); | ||
}); | ||
} else if (matchType === Const.COMBOBOX_FUZZY) { | ||
// Fuzzy filter, matches if query is "close" to answer | ||
match = ( | ||
(window.Levenshtein.get(d.name.toLowerCase(), query.toLowerCase()) <= 2 && query.length > 3) || | ||
d.name.toLowerCase() === query.toLowerCase() | ||
(window.Levenshtein.get(option.text.toLowerCase(), query.toLowerCase()) <= 2 && query.length > 3) || | ||
option.text.toLowerCase() === query.toLowerCase() | ||
); | ||
} | ||
|
||
|
@@ -674,29 +651,11 @@ hqDefine("cloudcare/js/form_entry/entrycontrols_full", function () { | |
} | ||
|
||
// Standard filter, matches only start of word | ||
return d.name.toLowerCase().startsWith(query.toLowerCase()); | ||
return option.text.toLowerCase().startsWith(query.toLowerCase()); | ||
}; | ||
|
||
ComboboxEntry.prototype = Object.create(EntrySingleAnswer.prototype); | ||
ComboboxEntry.prototype.constructor = EntrySingleAnswer; | ||
ComboboxEntry.prototype.onPreProcess = function (newValue) { | ||
var value; | ||
if (newValue === Const.NO_ANSWER || newValue === '') { | ||
this.answer(Const.NO_ANSWER); | ||
this.question.error(null); | ||
return; | ||
} | ||
|
||
value = _.find(this.options(), function (d) { | ||
return d.name === newValue; | ||
}); | ||
if (value) { | ||
this.answer(value.id); | ||
this.question.error(null); | ||
} else { | ||
this.question.error(gettext('Not a valid choice')); | ||
} | ||
}; | ||
ComboboxEntry.prototype = Object.create(DropdownEntry.prototype); | ||
ComboboxEntry.prototype.constructor = DropdownEntry; | ||
ComboboxEntry.prototype.receiveMessage = function (message, field) { | ||
// Iterates through options and selects an option that matches message[field]. | ||
// Registers a no answer if message[field] is not in options. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i'm guessing
pureComputed
doesn't have thesubscribe
option?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does, but the subscribers shouldn't have side effects. I think that rendering the select2 counts as a side effect, although I'm totally certain. I'm looking at the "When not to use a pure computed observable" section in these docs: https://knockoutjs.com/documentation/computed-pure.html