diff --git a/packages/common/src/editors/__tests__/selectEditor.spec.ts b/packages/common/src/editors/__tests__/selectEditor.spec.ts
index fb3def09d..69196df34 100644
--- a/packages/common/src/editors/__tests__/selectEditor.spec.ts
+++ b/packages/common/src/editors/__tests__/selectEditor.spec.ts
@@ -107,7 +107,7 @@ describe('SelectEditor', () => {
(mockColumn.internalColumnEditor as ColumnEditor).collection = [{ hello: 'world' }];
editor = new SelectEditor(editorArguments, true);
} catch (e) {
- expect(e.toString()).toContain(`[select-editor] A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list`);
+ expect(e.toString()).toContain(`[Slickgrid-Universal] Select Filter/Editor collection with value/label (or value/labelKey when using Locale) is required to populate the Select list`);
done();
}
});
diff --git a/packages/common/src/editors/selectEditor.ts b/packages/common/src/editors/selectEditor.ts
index 4fa106044..47df4afff 100644
--- a/packages/common/src/editors/selectEditor.ts
+++ b/packages/common/src/editors/selectEditor.ts
@@ -20,8 +20,8 @@ import {
SlickGrid,
SlickNamespace,
} from './../interfaces/index';
-import { CollectionService, findOrDefault, TranslaterService } from '../services/index';
-import { getDescendantProperty, getTranslationPrefix, htmlEncode, sanitizeTextByAvailableSanitizer, setDeepValue } from '../services/utilities';
+import { buildSelectEditorOrFilterDomElement, CollectionService, findOrDefault, TranslaterService } from '../services/index';
+import { getDescendantProperty, getTranslationPrefix, setDeepValue } from '../services/utilities';
// using external non-typed js libraries
declare const Slick: SlickNamespace;
@@ -696,70 +696,18 @@ export class SelectEditor implements Editor {
this.finalCollection = finalCollection;
// step 1, create HTML string template
- const editorTemplate = this.buildTemplateHtmlString(finalCollection);
+ const selectBuildResult = buildSelectEditorOrFilterDomElement(
+ 'editor',
+ finalCollection,
+ this.columnDef,
+ this.grid,
+ this.isMultipleSelect,
+ this._translaterService
+ );
// step 2, create the DOM Element of the editor
- // also subscribe to the onClose event
- this.createDomElement(editorTemplate);
- }
-
- /** Build the template HTML string */
- protected buildTemplateHtmlString(collection: any[]): string {
- let options = '';
- const columnId = this.columnDef?.id ?? '';
- const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
- const isRenderHtmlEnabled = this.columnEditor?.enableRenderHtml ?? false;
- const sanitizedOptions = this.gridOptions?.sanitizeHtmlOptions ?? {};
-
- // collection could be an Array of Strings OR Objects
- if (collection.every((x: any) => typeof x === 'string')) {
- collection.forEach((option: string) => {
- options += ``;
- });
- } else {
- // array of objects will require a label/value pair unless a customStructure is passed
- collection.forEach((option: SelectOption) => {
- if (!option || (option[this.labelName] === undefined && option.labelKey === undefined)) {
- throw new Error('[select-editor] A collection with value/label (or value/labelKey when using ' +
- 'Locale) is required to populate the Select list, for example: ' +
- '{ collection: [ { value: \'1\', label: \'One\' } ])');
- }
- const labelKey = (option.labelKey || option[this.labelName]) as string;
- const labelText = ((option.labelKey || (this.enableTranslateLabel && this._translaterService)) && labelKey) ? this._translaterService?.translate(labelKey || ' ') : labelKey;
- let prefixText = option[this.labelPrefixName] || '';
- let suffixText = option[this.labelSuffixName] || '';
- let optionLabel = option[this.optionLabel] || '';
- if (optionLabel?.toString) {
- optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
- }
-
- // also translate prefix/suffix if enableTranslateLabel is true and text is a string
- prefixText = (this.enableTranslateLabel && this._translaterService && prefixText && typeof prefixText === 'string') ? this._translaterService.translate(prefixText || ' ') : prefixText;
- suffixText = (this.enableTranslateLabel && this._translaterService && suffixText && typeof suffixText === 'string') ? this._translaterService.translate(suffixText || ' ') : suffixText;
- optionLabel = (this.enableTranslateLabel && this._translaterService && optionLabel && typeof optionLabel === 'string') ? this._translaterService.translate(optionLabel || ' ') : optionLabel;
-
- // add to a temp array for joining purpose and filter out empty text
- const tmpOptionArray = [prefixText, labelText, suffixText].filter(text => (text !== undefined && text !== ''));
- let optionText = tmpOptionArray.join(separatorBetweenLabels);
-
- // if user specifically wants to render html text, he needs to opt-in else it will stripped out by default
- // also, the 3rd party lib will saninitze any html code unless it's encoded, so we'll do that
- if (isRenderHtmlEnabled) {
- // sanitize any unauthorized html tags like script and others
- // for the remaining allowed tags we'll permit all attributes
- const sanitizedText = sanitizeTextByAvailableSanitizer(this.gridOptions, optionText, sanitizedOptions);
- optionText = htmlEncode(sanitizedText);
- }
-
- // html text of each select option
- let optionValue = option[this.valueName];
- if (optionValue === undefined || optionValue === null) {
- optionValue = '';
- }
- options += ``;
- });
- }
- return ``;
+ // we will later also subscribe to the onClose event to save the Editor whenever that event is triggered
+ this.createDomElement(selectBuildResult.selectElement);
}
/** Create a blank entry that can be added to the collection. It will also reuse the same collection structure provided by the user */
@@ -777,9 +725,12 @@ export class SelectEditor implements Editor {
return blankEntry;
}
- /** From the html template string, create the DOM element of the Multiple/Single Select Editor */
- protected createDomElement(editorTemplate: string) {
- this.$editorElm = $(editorTemplate);
+ /**
+ * From the Select DOM Element created earlier, create a Multiple/Single Select Editor using the jQuery multiple-select.js lib
+ * @param {Object} selectElement
+ */
+ protected createDomElement(selectElement: HTMLSelectElement) {
+ this.$editorElm = $(selectElement);
if (this.$editorElm && typeof this.$editorElm.appendTo === 'function') {
$(this.args.container).empty();
diff --git a/packages/common/src/filters/__tests__/selectFilter.spec.ts b/packages/common/src/filters/__tests__/selectFilter.spec.ts
index b1cf27d6f..6285f0355 100644
--- a/packages/common/src/filters/__tests__/selectFilter.spec.ts
+++ b/packages/common/src/filters/__tests__/selectFilter.spec.ts
@@ -98,7 +98,7 @@ describe('SelectFilter', () => {
mockColumn.filter!.collection = [{ hello: 'world' }];
filter.init(filterArguments);
} catch (e) {
- expect(e.message).toContain(`[select-filter] A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list`);
+ expect(e.message).toContain(`[Slickgrid-Universal] Select Filter/Editor collection with value/label (or value/labelKey when using Locale) is required to populate the Select list`);
done();
}
});
diff --git a/packages/common/src/filters/selectFilter.ts b/packages/common/src/filters/selectFilter.ts
index 87e56b8f8..240b472c5 100644
--- a/packages/common/src/filters/selectFilter.ts
+++ b/packages/common/src/filters/selectFilter.ts
@@ -11,13 +11,12 @@ import {
GridOption,
Locale,
MultipleSelectOption,
- SelectOption,
SlickGrid,
} from './../interfaces/index';
import { CollectionService } from '../services/collection.service';
import { collectionObserver, propertyObserver } from '../services/observers';
-import { getDescendantProperty, getTranslationPrefix, htmlEncode, sanitizeTextByAvailableSanitizer, unsubscribeAll } from '../services/utilities';
-import { RxJsFacade, Subscription, TranslaterService } from '../services/index';
+import { getDescendantProperty, getTranslationPrefix, unsubscribeAll } from '../services/utilities';
+import { buildSelectEditorOrFilterDomElement, RxJsFacade, Subscription, TranslaterService } from '../services/index';
import { renderCollectionOptionsAsync } from './filterUtilities';
export class SelectFilter implements Filter {
@@ -342,94 +341,24 @@ export class SelectFilter implements Filter {
newCollection = this.filterCollection(newCollection);
newCollection = this.sortCollection(newCollection);
- // step 1, create HTML string template
- const filterTemplate = this.buildTemplateHtmlString(newCollection, this.searchTerms || []);
+ // step 1, create HTML DOM element
+ const selectBuildResult = buildSelectEditorOrFilterDomElement(
+ 'filter',
+ newCollection,
+ this.columnDef,
+ this.grid,
+ this.isMultipleSelect,
+ this.translaterService,
+ this.searchTerms || []
+ );
+ this.isFilled = selectBuildResult.hasFoundSearchTerm;
// step 2, create the DOM Element of the filter & pre-load search terms
- // also subscribe to the onClose event
- this.createDomElement(filterTemplate);
+ // we will later also subscribe to the onClose event to filter the data whenever that event is triggered
+ this.createDomElement(selectBuildResult.selectElement);
this._collectionLength = newCollection.length;
}
- /**
- * Create the HTML template as a string
- * @param optionCollection array
- * @param searchTerms array
- * @return html template string
- */
- protected buildTemplateHtmlString(optionCollection: any[], searchTerms: SearchTerm[]): string {
- let options = '';
- const columnId = this.columnDef?.id ?? '';
- const separatorBetweenLabels = this.collectionOptions?.separatorBetweenTextLabels ?? '';
- const isTranslateEnabled = this.gridOptions?.enableTranslate ?? false;
- const isRenderHtmlEnabled = this.columnFilter?.enableRenderHtml ?? false;
- const sanitizedOptions = this.gridOptions?.sanitizeHtmlOptions ?? {};
-
- // collection could be an Array of Strings OR Objects
- if (Array.isArray(optionCollection)) {
- if (optionCollection.every((x: any) => typeof x === 'string')) {
- optionCollection.forEach((option: string) => {
- const selected = (searchTerms.findIndex((term) => term === option) >= 0) ? 'selected' : '';
- options += ``;
-
- // if there's at least 1 search term found, we will add the "filled" class for styling purposes
- // on a single select, we'll also make sure the single value is not an empty string to consider this being filled
- if ((selected && this.isMultipleSelect) || (selected && !this.isMultipleSelect && option !== '')) {
- this.isFilled = true;
- }
- });
- } else {
- // array of objects will require a label/value pair unless a customStructure is passed
- optionCollection.forEach((option: SelectOption) => {
- if (!option || (option[this.labelName] === undefined && option.labelKey === undefined)) {
- throw new Error(`[select-filter] A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list, for example:: { filter: model: Filters.multipleSelect, collection: [ { value: '1', label: 'One' } ]')`);
- }
- const labelKey = (option.labelKey || option[this.labelName]) as string;
- const selected = (searchTerms.findIndex((term) => `${term}` === `${option[this.valueName]}`) >= 0) ? 'selected' : '';
- const labelText = ((option.labelKey || this.enableTranslateLabel) && labelKey && isTranslateEnabled) ? (this.translaterService?.getCurrentLanguage && this.translaterService?.getCurrentLanguage() && this.translaterService.translate(labelKey) || '') : labelKey;
- let prefixText = option[this.labelPrefixName] || '';
- let suffixText = option[this.labelSuffixName] || '';
- let optionLabel = option.hasOwnProperty(this.optionLabel) ? option[this.optionLabel] : '';
- if (optionLabel?.toString) {
- optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html
- }
-
- // also translate prefix/suffix if enableTranslateLabel is true and text is a string
- prefixText = (this.enableTranslateLabel && isTranslateEnabled && prefixText && typeof prefixText === 'string') ? this.translaterService?.getCurrentLanguage && this.translaterService.getCurrentLanguage() && this.translaterService.translate(prefixText || ' ') : prefixText;
- suffixText = (this.enableTranslateLabel && isTranslateEnabled && suffixText && typeof suffixText === 'string') ? this.translaterService?.getCurrentLanguage && this.translaterService.getCurrentLanguage() && this.translaterService.translate(suffixText || ' ') : suffixText;
- optionLabel = (this.enableTranslateLabel && isTranslateEnabled && optionLabel && typeof optionLabel === 'string') ? this.translaterService?.getCurrentLanguage && this.translaterService.getCurrentLanguage() && this.translaterService.translate(optionLabel || ' ') : optionLabel;
- // add to a temp array for joining purpose and filter out empty text
- const tmpOptionArray = [prefixText, (typeof labelText === 'string' || typeof labelText === 'number') ? labelText.toString() : labelText, suffixText].filter((text) => text);
- let optionText = tmpOptionArray.join(separatorBetweenLabels);
-
- // if user specifically wants to render html text, he needs to opt-in else it will stripped out by default
- // also, the 3rd party lib will saninitze any html code unless it's encoded, so we'll do that
- if (isRenderHtmlEnabled) {
- // sanitize any unauthorized html tags like script and others
- // for the remaining allowed tags we'll permit all attributes
- const sanitizedText = sanitizeTextByAvailableSanitizer(this.gridOptions, optionText, sanitizedOptions);
- optionText = htmlEncode(sanitizedText);
- }
-
- // html text of each select option
- let optionValue = option[this.valueName];
- if (optionValue === undefined || optionValue === null) {
- optionValue = '';
- }
- options += ``;
-
- // if there's a search term, we will add the "filled" class for styling purposes
- // on a single select, we'll also make sure the single value is not an empty string to consider this being filled
- if ((selected && this.isMultipleSelect) || (selected && !this.isMultipleSelect && option[this.valueName] !== '')) {
- this.isFilled = true;
- }
- });
- }
- }
-
- return ``;
- }
-
/** Create a blank entry that can be added to the collection. It will also reuse the same collection structure provided by the user */
protected createBlankEntry(): any {
const blankEntry = {
@@ -446,11 +375,10 @@ export class SelectFilter implements Filter {
}
/**
- * From the html template string, create a DOM element of the Multiple/Single Select Filter
- * Subscribe to the onClose event and run the callback when that happens
- * @param filterTemplate
+ * From the Select DOM Element created earlier, create a Multiple/Single Select Filter using the jQuery multiple-select.js lib
+ * @param {Object} selectElement
*/
- protected createDomElement(filterTemplate: string) {
+ protected createDomElement(selectElement: HTMLSelectElement) {
const columnId = this.columnDef?.id ?? '';
// provide the name attribute to the DOM element which will be needed to auto-adjust drop position (dropup / dropdown)
@@ -461,7 +389,7 @@ export class SelectFilter implements Filter {
$($headerElm).empty();
// create the DOM element & add an ID and filter class
- this.$filterElm = $(filterTemplate);
+ this.$filterElm = $(selectElement);
if (typeof this.$filterElm.multipleSelect !== 'function') {
throw new Error(`multiple-select.js was not found, make sure to read the HOWTO Wiki on how to install it.`);
}
diff --git a/packages/common/src/services/domUtilities.ts b/packages/common/src/services/domUtilities.ts
new file mode 100644
index 000000000..38fd1b5a8
--- /dev/null
+++ b/packages/common/src/services/domUtilities.ts
@@ -0,0 +1,128 @@
+import { SearchTerm } from '../enums/index';
+import { Column, SelectOption, SlickGrid } from '../interfaces/index';
+import { TranslaterService } from './translater.service';
+import { htmlEncode, sanitizeTextByAvailableSanitizer } from './utilities';
+
+/**
+ * Create the HTML DOM Element for a Select Editor or Filter, this is specific to these 2 types only and the unit tests are directly under them
+ * @param {String} type - type of select DOM element to build, can be either 'editor' or 'filter'
+ * @param {Array