-
Notifications
You must be signed in to change notification settings - Fork 355
ComboBox - New methods + bullet proofing #479
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
Open
mohsinhijazee
wants to merge
3
commits into
htmlstreamofficial:main
Choose a base branch
from
mohsinhijazee:combobox-enhancements
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 |
---|---|---|
|
@@ -19,6 +19,7 @@ import { | |
IComboBox, | ||
IComboBoxOptions, | ||
IComboBoxItemAttr, | ||
QueryTransformer, | ||
} from '../combobox/interfaces'; | ||
|
||
import HSBasePlugin from '../base-plugin'; | ||
|
@@ -34,6 +35,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
apiDataPart: string | null; | ||
apiQuery: string | null; | ||
apiSearchQuery: string | null; | ||
apiSearchQueryTransformer: ((query: string) => string) | null; | ||
apiHeaders: {}; | ||
apiGroupField: string | null; | ||
outputItemTemplate: string | null; | ||
|
@@ -50,6 +52,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
private readonly output: HTMLElement | null; | ||
private readonly itemsWrapper: HTMLElement | null; | ||
private items: HTMLElement[] | []; | ||
private selectedItemElement: HTMLElement | null; | ||
private tabs: HTMLElement[] | []; | ||
private readonly toggle: HTMLElement | null; | ||
private readonly toggleClose: HTMLElement | null; | ||
|
@@ -87,6 +90,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
this.apiDataPart = concatOptions?.apiDataPart ?? null; | ||
this.apiQuery = concatOptions?.apiQuery ?? null; | ||
this.apiSearchQuery = concatOptions?.apiSearchQuery ?? null; | ||
this.apiSearchQueryTransformer = this.parseApiQueryTransformer(concatOptions?.apiSearchQueryTransformer); | ||
this.apiHeaders = concatOptions?.apiHeaders ?? {}; | ||
this.apiGroupField = concatOptions?.apiGroupField ?? null; | ||
this.outputItemTemplate = | ||
|
@@ -150,7 +154,20 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
this.init(); | ||
} | ||
|
||
private parseApiQueryTransformer(query: QueryTransformer | string | null): QueryTransformer | null { | ||
|
||
if (!query) return null; | ||
|
||
if (typeof query === 'string') { | ||
return eval(query) as QueryTransformer; | ||
} | ||
|
||
return query; | ||
} | ||
|
||
private init() { | ||
// So that not to dpeendon preloading whole library. | ||
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. Small typo in the comment. |
||
window.$hsComboBoxCollection = window.$hsComboBoxCollection ?? []; | ||
this.createCollection(window.$hsComboBoxCollection, this); | ||
|
||
this.build(); | ||
|
@@ -239,10 +256,10 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
const equality = | ||
params?.group?.name && group | ||
? group === params.group.name && | ||
elI.getAttribute('data-hs-combo-box-search-text') === | ||
obj[elI.getAttribute('data-hs-combo-box-output-item-field')] | ||
elI.getAttribute('data-hs-combo-box-search-text') === | ||
obj[elI.getAttribute('data-hs-combo-box-output-item-field')] | ||
: elI.getAttribute('data-hs-combo-box-search-text') === | ||
obj[elI.getAttribute('data-hs-combo-box-output-item-field')]; | ||
obj[elI.getAttribute('data-hs-combo-box-output-item-field')]; | ||
|
||
return equality; | ||
}); | ||
|
@@ -319,7 +336,9 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
|
||
try { | ||
const query = `${this.apiQuery}`; | ||
const searchQuery = `${this.apiSearchQuery}=${this.value.toLowerCase()}`; | ||
const initialSearchQuery = `${this.apiSearchQuery}=${this.value.toLowerCase()}`; | ||
const searchQuery = this.apiSearchQueryTransformer ? this.apiSearchQueryTransformer(this.value) : initialSearchQuery; | ||
|
||
let url = this.apiUrl; | ||
if (this.apiQuery && this.apiSearchQuery) { | ||
url += `?${searchQuery}&${query}`; | ||
|
@@ -376,6 +395,12 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
} | ||
|
||
private jsonItemsRender(items: any) { | ||
|
||
// Bullet proofing. | ||
if (Array.isArray(items)) { | ||
return | ||
} | ||
|
||
items.forEach((item: never, index: number) => { | ||
// TODO:: test without checking below | ||
// if (this.isItemExists(item)) return false; | ||
|
@@ -401,6 +426,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
item[el.getAttribute('data-hs-combo-box-output-item-field')] ?? '', | ||
); | ||
}); | ||
// FIXME: Move to combobox options | ||
newItem | ||
.querySelectorAll('[data-hs-combo-box-output-item-attr]') | ||
.forEach((el) => { | ||
|
@@ -416,8 +442,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
if (this.groupingType === 'tabs' || this.groupingType === 'default') { | ||
newItem.setAttribute( | ||
'data-hs-combo-box-output-item', | ||
`{"group": {"name": "${item[this.apiGroupField]}", "title": "${ | ||
item[this.apiGroupField] | ||
`{"group": {"name": "${item[this.apiGroupField]}", "title": "${item[this.apiGroupField] | ||
}"}}`, | ||
); | ||
} | ||
|
@@ -426,13 +451,13 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
|
||
if (!this.preventSelection) { | ||
(newItem as HTMLElement).addEventListener('click', () => { | ||
this.selectedItemElement = newItem; | ||
this.setSelectedByValue(this.valuesBySelector(newItem)); | ||
this.close( | ||
(newItem as HTMLElement) | ||
.querySelector('[data-hs-combo-box-value]') | ||
.getAttribute('data-hs-combo-box-search-text'), | ||
); | ||
|
||
this.setSelectedByValue(this.valuesBySelector(newItem)); | ||
}); | ||
} | ||
|
||
|
@@ -645,6 +670,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
}); | ||
} | ||
|
||
// FIXME: Does not go through the setSelectedByValue method. | ||
private setValue(val: string) { | ||
this.selected = val; | ||
this.value = val; | ||
|
@@ -666,12 +692,12 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
? this.selectedGroup === 'all' | ||
? this.items | ||
: this.items.filter((f: HTMLElement) => { | ||
const { group } = JSON.parse( | ||
f.getAttribute('data-hs-combo-box-output-item'), | ||
); | ||
const { group } = JSON.parse( | ||
f.getAttribute('data-hs-combo-box-output-item'), | ||
); | ||
|
||
return group.name === this.selectedGroup; | ||
}) | ||
return group.name === this.selectedGroup; | ||
}) | ||
: this.items; | ||
|
||
if (this.groupingType === 'tabs' && this.selectedGroup !== 'all') { | ||
|
@@ -739,7 +765,6 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
this.setSelectedByValue([this.selected]); | ||
} | ||
|
||
// Public methods | ||
private setValueAndOpen(val: string) { | ||
this.value = val; | ||
|
||
|
@@ -748,6 +773,15 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
} | ||
} | ||
|
||
private setValueAndClear(val: string | null) { | ||
if (val) this.setValue(val); | ||
else this.setValue(this.selected); | ||
|
||
if (this.outputPlaceholder) this.destroyOutputPlaceholder(); | ||
} | ||
|
||
|
||
// Public methods | ||
public open(val?: string) { | ||
if (this.animationInProcess) return false; | ||
|
||
|
@@ -771,13 +805,6 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
this.isOpened = true; | ||
} | ||
|
||
private setValueAndClear(val: string | null) { | ||
if (val) this.setValue(val); | ||
else this.setValue(this.selected); | ||
|
||
if (this.outputPlaceholder) this.destroyOutputPlaceholder(); | ||
} | ||
|
||
public close(val?: string | null) { | ||
if (this.animationInProcess) return false; | ||
|
||
|
@@ -803,18 +830,36 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
|
||
afterTransition(this.output, () => { | ||
this.output.style.display = 'none'; | ||
|
||
this.setValueAndClear(val); | ||
|
||
this.animationInProcess = false; | ||
}); | ||
|
||
if (this.input.value !== '') this.el.classList.add('has-value'); | ||
else this.el.classList.remove('has-value'); | ||
|
||
|
||
this.isOpened = false; | ||
} | ||
|
||
setSearchQueryTransformer(transformer: (query: string) => string) { | ||
this.apiSearchQueryTransformer = transformer; | ||
} | ||
|
||
public selectedItem(): HTMLElement | null { | ||
return this.selectedItemElement; | ||
} | ||
|
||
selectedValue(): string | null { | ||
return this.selected; | ||
} | ||
|
||
selectedAttr(attr: string): string | null { | ||
return this.selectedItemElement | ||
? this.selectedItemElement.querySelector(`[${attr}]`)?.getAttribute(attr) ?? null | ||
: null; | ||
} | ||
|
||
|
||
public recalculateDirection() { | ||
if ( | ||
isEnoughSpace( | ||
|
@@ -918,13 +963,13 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
|
||
const preparedItems = isReversed | ||
? Array.from( | ||
output.querySelectorAll(':scope > *:not(.--exclude-accessibility)'), | ||
) | ||
.filter((el) => (el as HTMLElement).style.display !== 'none') | ||
.reverse() | ||
output.querySelectorAll(':scope > *:not(.--exclude-accessibility)'), | ||
) | ||
.filter((el) => (el as HTMLElement).style.display !== 'none') | ||
.reverse() | ||
: Array.from( | ||
output.querySelectorAll(':scope > *:not(.--exclude-accessibility)'), | ||
).filter((el) => (el as HTMLElement).style.display !== 'none'); | ||
output.querySelectorAll(':scope > *:not(.--exclude-accessibility)'), | ||
).filter((el) => (el as HTMLElement).style.display !== 'none'); | ||
const items = preparedItems.filter( | ||
(el: any) => !el.classList.contains('disabled'), | ||
); | ||
|
@@ -1058,7 +1103,7 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
(el) => | ||
!isParentOrElementHidden(el.element.el) && | ||
(evt.target as HTMLElement).closest('[data-hs-combo-box]') === | ||
el.element.el, | ||
el.element.el, | ||
); | ||
|
||
const link: HTMLAnchorElement = opened.element.el.querySelector( | ||
|
@@ -1080,8 +1125,8 @@ class HSComboBox extends HSBasePlugin<IComboBoxOptions> implements IComboBox { | |
opened.element.close( | ||
!opened.element.preventSelection | ||
? (evt.target as HTMLElement) | ||
.querySelector('[data-hs-combo-box-value]') | ||
.getAttribute('data-hs-combo-box-search-text') | ||
.querySelector('[data-hs-combo-box-value]') | ||
.getAttribute('data-hs-combo-box-search-text') | ||
: null, | ||
); | ||
} | ||
|
This file contains hidden or 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.
Hi @mohsinhijazee , thanks for corrections.
I noticed that you've added
eval
in theparseApiQueryTransformer
method.eval
is known to be a potential security risk, especially when handling dynamic inputs, as it can allow arbitrary code execution.This raises some concerns on our side, as we strive to keep the codebase secure and avoid introducing vulnerabilities. Could you please explain why
eval
was necessary here and if there might be a safer alternative?We need to ensure that all code changes are secure and well-justified, so your clarification would be appreciated.
Thanks.
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.
Yes, this
eval
is used so that the users of the component can utilisedata-*
attributes to setup the search function (transforming the query before it is sent to the server) without going to write further Javascript (and instantiating a control in pure JS) therefore this does not come from the user input per se, rather from the developer/user of the library and I believe this is a safe use case and can't be exploited because it is not linked to an input control that can be manipulated by the end users to change things around.Even data attribute though dynamically can be changed, but that change won't trigger the
eval
because combobox would be initialised already.Other alternative is to drop the ability of being able to supply a search function via
data-*
attributes which would result in a component that is less capable when used in typical HTML only scenarios.