Skip to content

Commit

Permalink
feat(select): collection can also be of type String (#109)
Browse files Browse the repository at this point in the history
* feat(select): collection can also be of type String
- prior to this only collection of value/label pair was accepted.

* refactor(build): fix missing types for build to pass
  • Loading branch information
ghiscoding committed Nov 10, 2018
1 parent 9ac7736 commit 9733d08
Show file tree
Hide file tree
Showing 9 changed files with 147 additions and 94 deletions.
4 changes: 2 additions & 2 deletions aurelia-slickgrid/src/aurelia-slickgrid/aurelia-slickgrid.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ export class AureliaSlickgridCustomElement {
private _columnDefinitions: Column[] = [];
private _dataset: any[];
private _eventHandler: any = new Slick.EventHandler();
private _fixedHeight: number;
private _fixedWidth: number;
private _fixedHeight: number | undefined;
private _fixedWidth: number | undefined;
private _hideHeaderRowAfterPageLoad = false;
groupItemMetadataProvider: any;
isGridInitialized = false;
Expand Down
82 changes: 51 additions & 31 deletions aurelia-slickgrid/src/aurelia-slickgrid/editors/selectEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,12 @@ export class SelectEditor implements Editor {
* The current selected values (multiple select) from the collection
*/
get currentValues(): any[] {
// collection of strings, just return the filtered string that are equals
if (this.collection.every(x => typeof x === 'string')) {
return this.collection.filter(c => this.$editorElm.val().indexOf(c.toString()) !== -1);
}

// collection of label/value pair
const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;

Expand All @@ -168,6 +174,12 @@ export class SelectEditor implements Editor {
* The current selected values (single select) from the collection
*/
get currentValue(): number | string {
// collection of strings, just return the filtered string that are equals
if (this.collection.every(x => typeof x === 'string')) {
return findOrDefault(this.collection, (c: any) => c.toString() === this.$editorElm.val());
}

// collection of label/value pair
const separatorBetweenLabels = this.collectionOptions && this.collectionOptions.separatorBetweenTextLabels || '';
const isIncludingPrefixSuffix = this.collectionOptions && this.collectionOptions.includePrefixSuffixToSelectedValues || false;
const itemFound = findOrDefault(this.collection, (c: any) => c[this.valueName].toString() === this.$editorElm.val());
Expand Down Expand Up @@ -368,38 +380,46 @@ export class SelectEditor implements Editor {
const isRenderHtmlEnabled = this.columnEditor.enableRenderHtml || false;
const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {};

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) && labelKey) ? this.i18n.tr(labelKey || ' ') : labelKey;
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option[this.optionLabel] || '';
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 && prefixText && typeof prefixText === 'string') ? this.i18n.tr(prefixText || ' ') : prefixText;
suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this.i18n.tr(suffixText || ' ') : suffixText;
optionLabel = (this.enableTranslateLabel && optionLabel && typeof optionLabel === 'string') ? this.i18n.tr(optionLabel || ' ') : optionLabel;
// add to a temp array for joining purpose and filter out empty text
const tmpOptionArray = [prefixText, 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 = DOMPurify.sanitize(optionText, sanitizedOptions);
optionText = htmlEncode(sanitizedText);
}
// collection could be an Array of Strings OR Objects
if (collection.every((x: any) => typeof x === 'string')) {
collection.forEach((option: string) => {
options += `<option value="${option}" label="${option}">${option}</option>`;
});
} 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) && labelKey) ? this.i18n.tr(labelKey || ' ') : labelKey;
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option[this.optionLabel] || '';
optionLabel = optionLabel.toString().replace(/\"/g, '\''); // replace double quotes by single quotes to avoid interfering with regular html

options += `<option value="${option[this.valueName]}" label="${optionLabel}">${optionText}</option>`;
});
// also translate prefix/suffix if enableTranslateLabel is true and text is a string
prefixText = (this.enableTranslateLabel && prefixText && typeof prefixText === 'string') ? this.i18n.tr(prefixText || ' ') : prefixText;
suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this.i18n.tr(suffixText || ' ') : suffixText;
optionLabel = (this.enableTranslateLabel && optionLabel && typeof optionLabel === 'string') ? this.i18n.tr(optionLabel || ' ') : optionLabel;
// add to a temp array for joining purpose and filter out empty text
const tmpOptionArray = [prefixText, 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 = DOMPurify.sanitize(optionText, sanitizedOptions);
optionText = htmlEncode(sanitizedText);
}

options += `<option value="${option[this.valueName]}" label="${optionLabel}">${optionText}</option>`;
});
}

return `<select id="${this.elementName}" class="ms-filter search-filter" ${this.isMultipleSelect ? 'multiple="multiple"' : ''}>${options}</select>`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,22 @@ export class NativeSelectFilter implements Filter {
const isEnabledTranslate = (this.columnDef.filter.enableTranslateLabel) ? this.columnDef.filter.enableTranslateLabel : false;

let options = '';
optionCollection.forEach((option: any) => {
if (!option || (option[labelName] === undefined && option.labelKey === undefined)) {
throw new Error(`A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list, for example: { filter: { model: Filters.select, collection: [ { value: '1', label: 'One' } ] } }`);
}
const labelKey = option.labelKey || option[labelName];
const textLabel = ((option.labelKey || isEnabledTranslate) && this.i18n && typeof this.i18n.tr === 'function') ? this.i18n.tr(labelKey || ' ') : labelKey;
options += `<option value="${option[valueName]}">${textLabel}</option>`;
});

// collection could be an Array of Strings OR Objects
if (optionCollection.every(x => typeof x === 'string')) {
optionCollection.forEach((option: string) => {
options += `<option value="${option}" label="${option}">${option}</option>`;
});
} else {
optionCollection.forEach((option: any) => {
if (!option || (option[labelName] === undefined && option.labelKey === undefined)) {
throw new Error(`A collection with value/label (or value/labelKey when using Locale) is required to populate the Select list, for example:: { filter: model: Filters.select, collection: [ { value: '1', label: 'One' } ]')`);
}
const labelKey = option.labelKey || option[labelName];
const textLabel = ((option.labelKey || isEnabledTranslate) && this.i18n && typeof this.i18n.tr === 'function') ? this.i18n.tr(labelKey || ' ') : labelKey;
options += `<option value="${option[valueName]}">${textLabel}</option>`;
});
}
return `<select class="form-control search-filter">${options}</select>`;
}

Expand Down
87 changes: 51 additions & 36 deletions aurelia-slickgrid/src/aurelia-slickgrid/filters/selectFilter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class SelectFilter implements Filter {
// we will subscribe to the onClose event for triggering our callback
// also add/remove "filled" class for styling purposes
const selectedItems = this.$filterElm.multipleSelect('getSelects');
if (Array.isArray(selectedItems) && selectedItems.length > 0) {

if (Array.isArray(selectedItems) && !(selectedItems.length === 1 && selectedItems[0] === '') || selectedItems.length > 1) {
this.isFilled = true;
this.$filterElm.addClass('filled').siblings('div .search-filter').addClass('filled');
} else {
Expand Down Expand Up @@ -335,43 +336,57 @@ export class SelectFilter implements Filter {
const isRenderHtmlEnabled = this.columnFilter && this.columnFilter.enableRenderHtml || false;
const sanitizedOptions = this.gridOptions && this.gridOptions.sanitizeHtmlOptions || {};

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) ? this.i18n.tr(labelKey || ' ') : labelKey;
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option[this.optionLabel] || '';
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 && prefixText && typeof prefixText === 'string') ? this.i18n.tr(prefixText || ' ') : prefixText;
suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this.i18n.tr(suffixText || ' ') : suffixText;
optionLabel = (this.enableTranslateLabel && optionLabel && typeof optionLabel === 'string') ? this.i18n.tr(optionLabel || ' ') : optionLabel;
// add to a temp array for joining purpose and filter out empty text
const tmpOptionArray = [prefixText, 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 = DOMPurify.sanitize(optionText, sanitizedOptions);
optionText = htmlEncode(sanitizedText);
}
// collection could be an Array of Strings OR Objects
if (optionCollection.every((x: any) => typeof x === 'string')) {
optionCollection.forEach((option: string) => {
const selected = (searchTerms.findIndex((term) => term === option) >= 0) ? 'selected' : '';
options += `<option value="${option}" label="${option}" ${selected}>${option}</option>`;

// if there's at least 1 search term found, we will add the "filled" class for styling purposes
if (selected) {
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) ? this.i18n.tr(labelKey || ' ') : labelKey;
let prefixText = option[this.labelPrefixName] || '';
let suffixText = option[this.labelSuffixName] || '';
let optionLabel = option[this.optionLabel] || '';
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 && prefixText && typeof prefixText === 'string') ? this.i18n.tr(prefixText || ' ') : prefixText;
suffixText = (this.enableTranslateLabel && suffixText && typeof suffixText === 'string') ? this.i18n.tr(suffixText || ' ') : suffixText;
optionLabel = (this.enableTranslateLabel && optionLabel && typeof optionLabel === 'string') ? this.i18n.tr(optionLabel || ' ') : optionLabel;
// add to a temp array for joining purpose and filter out empty text
const tmpOptionArray = [prefixText, 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 = DOMPurify.sanitize(optionText, sanitizedOptions);
optionText = htmlEncode(sanitizedText);
}

// html text of each select option
options += `<option value="${option[this.valueName]}" label="${optionLabel}" ${selected}>${optionText}</option>`;
// html text of each select option
options += `<option value="${option[this.valueName]}" label="${optionLabel}" ${selected}>${optionText}</option>`;

// if there's a search term, we will add the "filled" class for styling purposes
if (selected) {
this.isFilled = true;
}
});
// if there's a search term, we will add the "filled" class for styling purposes
if (selected) {
this.isFilled = true;
}
});
}

return `<select class="ms-filter search-filter" multiple="multiple">${options}</select>`;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,19 @@ export const collectionEditorFormatter: Formatter = (row: number, cell: number,
const valueName = (internalColumnEditor.customStructure) ? internalColumnEditor.customStructure.value : 'value';

if (Array.isArray(value)) {
return arrayToCsvFormatter(row,
cell,
value.map((v: any) => findOrDefault(collection, (c: any) => c[valueName] === v)[labelName]),
columnDef,
dataContext);
if (collection.every((x: any) => typeof x === 'string')) {
return arrayToCsvFormatter(row,
cell,
value.map((v: any) => findOrDefault(collection, (c: any) => c === v)),
columnDef,
dataContext);
} else {
return arrayToCsvFormatter(row,
cell,
value.map((v: any) => findOrDefault(collection, (c: any) => c[valueName] === v)[labelName]),
columnDef,
dataContext);
}
}

return findOrDefault(collection, (c: any) => c[valueName] === value)[labelName] || '';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@ import {
} from './../models/index';

export interface ColumnEditor {
/** A collection of items/options that can be used by Select (dropdown) or any other type of Editors */
/**
* A collection of items/options (commonly used with a Select/Multi-Select Editor)
* It can be a collection of string or label/value pair (the pair can be customized via the "customStructure" option)
*/
collection?: any[];

/** A collection of items/options that will be loaded asynchronously (commonly used with a Select/Multi-Select Editor) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ export interface ColumnFilter {
/** Filter to use (input, multipleSelect, singleSelect, select, custom) */
model?: any;

/** A collection of items/options (commonly used with a Select/Multi-Select Filter) */
/**
* A collection of items/options (commonly used with a Select/Multi-Select Filter)
* It can be a collection of string or label/value pair (the pair can be customized via the "customStructure" option)
*/
collection?: any[];

/** A collection of items/options that will be loaded asynchronously (commonly used with a Select/Multi-Select Editor) */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,16 @@ const DATAGRID_PAGINATION_HEIGHT = 35;
let timer: any;

export interface GridDimension {
height: number;
width: number;
heightWithPagination?: number;
height: number | undefined;
width: number | undefined;
heightWithPagination?: number | undefined;
}

@singleton(true)
@inject(EventAggregator)
export class ResizerService {
private _fixedHeight: number;
private _fixedWidth: number;
private _fixedHeight: number | undefined;
private _fixedWidth: number | undefined;
private _grid: any;
private _lastDimensions: GridDimension;
aureliaEventPrefix: string;
Expand Down
6 changes: 1 addition & 5 deletions aurelia-slickgrid/src/examples/slickgrid/example4.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,7 @@ export class Example4 {
// enableRenderHtml: true,
// collection: [{ value: '', label: '' }, { value: true, label: 'True', labelPrefix: `<i class="fa fa-check"></i> ` }, { value: false, label: 'False' }],

collection: [{ isEffort: '', label: '' }, { isEffort: true, label: 'True' }, { isEffort: false, label: 'False' }],
customStructure: {
value: 'isEffort',
label: 'label'
},
collection: ['', 'True', 'False'],
model: Filters.singleSelect,

// we could add certain option(s) to the "multiple-select" plugin
Expand Down

0 comments on commit 9733d08

Please sign in to comment.