From ad2a7e30a5ba25a9b178bdad97f446d37abc0c41 Mon Sep 17 00:00:00 2001 From: Jeremy Zagorski Date: Sat, 3 Mar 2018 13:36:51 -0500 Subject: [PATCH 1/4] feat(selectEditors): add select grid editors select editors are common with data to ensure data integrity --- .../src/aurelia-slickgrid/editors/index.ts | 4 + .../editors/multipleSelectEditor.ts | 180 ++++++++++++++++++ .../editors/singleSelectEditor.ts | 176 +++++++++++++++++ .../formatters/arrayToCsvFormatter.ts | 3 +- .../formatters/collectionFormatter.ts | 26 +++ .../src/aurelia-slickgrid/formatters/index.ts | 4 + .../models/multipleSelectOption.interface.ts | 2 +- .../services/global-utilities.ts | 15 ++ .../aurelia-slickgrid/services/utilities.ts | 34 ++++ .../styles/slick-bootstrap.scss | 4 + 10 files changed, 446 insertions(+), 2 deletions(-) create mode 100644 aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts create mode 100644 aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts create mode 100644 aurelia-slickgrid/src/aurelia-slickgrid/formatters/collectionFormatter.ts diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/index.ts index 2cda091e9..8054c9059 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/editors/index.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/index.ts @@ -3,6 +3,8 @@ import { DateEditor } from './dateEditor'; import { FloatEditor } from './floatEditor'; import { IntegerEditor } from './integerEditor'; import { LongTextEditor } from './longTextEditor'; +import { MultipleSelectEditor } from './multipleSelectEditor'; +import { SingleSelectEditor } from './singleSelectEditor'; import { TextEditor } from './textEditor'; export const Editors = { @@ -11,5 +13,7 @@ export const Editors = { float: FloatEditor, integer: IntegerEditor, longText: LongTextEditor, + multipleSelect: MultipleSelectEditor, + singleSelect: SingleSelectEditor, text: TextEditor }; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts new file mode 100644 index 000000000..cc7efd068 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts @@ -0,0 +1,180 @@ +import { arraysEqual } from '../services/index'; +import { + Editor, + Column, + MultipleSelectOption, + SelectOption +} from './../models/index'; +import * as $ from 'jquery'; + +/** + * Slickgrid editor class for multiple select lists + */ +export class MultipleSelectEditor implements Editor { + /** + * The JQuery DOM element + */ + $filterElm: any; + /** + * The slick grid column being edited + */ + columnDef: Column; + /** + * The multiple-select options for a multiple select list + */ + defaultOptions: MultipleSelectOption; + /** + * The default item values that are set + */ + defaultValue: any[]; + /** + * The options label/value object to use in the select list + */ + optionCollection: SelectOption[] = []; + /** + * The property name for values in the optionCollection + */ + valueName: string; + /** + * The property name for labels in the optionCollection + */ + labelName: string; + + constructor(private args: any) { + this.defaultOptions = { + container: 'body', + filter: false, + maxHeight: 200, + width: '100%', + okButton: true, + addTitle: true, + selectAllDelimiter: ['', ''] + }; + + this.init(); + } + + /** + * The current selected values from the optionCollection + */ + get currentValues() { + return this.optionCollection + .filter(c => this.$filterElm.val().indexOf(c[this.valueName].toString()) !== -1) + .map(c => c[this.valueName]); + } + + init() { + if (!this.args) { + throw new Error('[Aurelia-SlickGrid] A filter must always have an "init()" ' + + 'with valid arguments.'); + } + + this.columnDef = this.args.column; + + const filterTemplate = this.buildTemplateHtmlString(); + + this.createDomElement(filterTemplate); + } + + applyValue(item: any, state: any): void { + item[this.args.column.field] = state; + } + + destroy() { + this.$filterElm.remove(); + } + + loadValue(item: any): void { + // convert to string because that is how the DOM will return these values + this.defaultValue = item[this.columnDef.field].map((i: any) => i.toString()); + + this.$filterElm.find('option').each((i: number, $e: any) => { + if (this.defaultValue.indexOf($e.value) !== -1) { + $e.selected = true; + } else { + $e.selected = false; + } + }); + + this.refresh(); + } + + serializeValue(): any { + return this.currentValues; + } + + focus() { + this.$filterElm.focus(); + } + + isValueChanged(): boolean { + return !arraysEqual(this.$filterElm.val(), this.defaultValue); + } + + validate() { + if (this.args.column.validator) { + const validationResults = this.args.column.validator(this.currentValues, this.args); + if (!validationResults.valid) { + return validationResults; + } + } + + return { + valid: true, + msg: null + }; + } + + private buildTemplateHtmlString() { + if (!this.columnDef || !this.columnDef.filter || !this.columnDef.filter.collection) { + throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" for ' + + 'the MultipleSelect Filter to work correctly. Also each option should include ' + + 'a value/label pair (or value/labelKey when using Locale). For example:: ' + + '{ filter: type: FilterType.multipleSelect, collection: [{ value: true, label: \'True\' }, ' + + '{ value: false, label: \'False\'}] }'); + } + this.optionCollection = this.columnDef.filter.collection || []; + this.labelName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.label : 'label'; + this.valueName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.value : 'value'; + + let options = ''; + this.optionCollection.forEach((option: SelectOption) => { + if (!option || (option[this.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: ' + + 'type: FilterType.multipleSelect, collection: [ { value: \'1\', label: \'One\' } ])'); + } + const labelKey = (option.labelKey || option[this.labelName]) as string; + const textLabel = labelKey; + + options += ``; + }); + + return ``; + } + + private createDomElement(filterTemplate: string) { + this.$filterElm = $(filterTemplate); + + if (this.$filterElm && typeof this.$filterElm.appendTo === 'function') { + this.$filterElm.appendTo(this.args.container); + } + + if (typeof this.$filterElm.multipleSelect !== 'function') { + // fallback to bootstrap + this.$filterElm.addClass('form-control'); + } else { + const filterOptions = (this.columnDef.filter) ? this.columnDef.filter.filterOptions : {}; + const options: MultipleSelectOption = { ...this.defaultOptions, ...filterOptions }; + this.$filterElm = this.$filterElm.multipleSelect(options); + } + } + + // refresh the jquery object because the selected checkboxes were already set + // prior to this method being called + private refresh() { + if (typeof this.$filterElm.multipleSelect === 'function') { + this.$filterElm.data('multipleSelect').refresh(); + } + } +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts new file mode 100644 index 000000000..4d1a76ff0 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts @@ -0,0 +1,176 @@ +import { + Editor, + Column, + MultipleSelectOption, + SelectOption +} from './../models/index'; +import * as $ from 'jquery'; + +/** + * Slickgrid editor class for single select lists + */ +export class SingleSelectEditor implements Editor { + /** + * The JQuery DOM element + */ + $filterElm: any; + /** + * The slick grid column being edited + */ + columnDef: Column; + /** + * The multiple-select options for a single select + */ + defaultOptions: any; + /** + * The default item value that is set + */ + defaultValue: any; + /** + * The options label/value object to use in the select list + */ + optionCollection: SelectOption[] = []; + /** + * The property name for values in the optionCollection + */ + valueName: string; + /** + * The property name for labels in the optionCollection + */ + labelName: string; + + constructor(private args: any) { + this.defaultOptions = { + container: 'body', + filter: false, + maxHeight: 200, + width: '100%', + single: true + }; + + this.init(); + } + + /** + * The current selected value from the optionCollection + */ + get currentValue() { + return this.optionCollection.findOrDefault(c => + c[this.valueName].toString() === this.$filterElm.val())[this.valueName]; + } + + init() { + if (!this.args) { + throw new Error('[Aurelia-SlickGrid] A filter must always have an "init()" ' + + 'with valid arguments.'); + } + + this.columnDef = this.args.column; + + const filterTemplate = this.buildTemplateHtmlString(); + + this.createDomElement(filterTemplate); + } + + applyValue(item: any, state: any): void { + item[this.args.column.field] = state; + } + + destroy() { + this.$filterElm.remove(); + } + + loadValue(item: any): void { + // convert to string because that is how the DOM will return these values + this.defaultValue = item[this.columnDef.field].toString(); + + this.$filterElm.find('option').each((i: number, $e: any) => { + if (this.defaultValue.indexOf($e.value) !== -1) { + $e.selected = true; + } else { + $e.selected = false; + } + }); + + this.refresh(); + } + + serializeValue(): any { + return this.currentValue; + } + + focus() { + this.$filterElm.focus(); + } + + isValueChanged(): boolean { + return this.$filterElm.val() !== this.defaultValue; + } + + validate() { + if (this.args.column.validator) { + const validationResults = this.args.column.validator(this.currentValue, this.args); + if (!validationResults.valid) { + return validationResults; + } + } + + return { + valid: true, + msg: null + }; + } + + private buildTemplateHtmlString() { + if (!this.columnDef || !this.columnDef.filter || !this.columnDef.filter.collection) { + throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" for ' + + 'the MultipleSelect Filter to work correctly. Also each option should include ' + + 'a value/label pair (or value/labelKey when using Locale). For example:: ' + + '{ filter: type: FilterType.multipleSelect, collection: [{ value: true, label: \'True\' }, ' + + '{ value: false, label: \'False\'}] }'); + } + this.optionCollection = this.columnDef.filter.collection || []; + this.labelName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.label : 'label'; + this.valueName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.value : 'value'; + + let options = ''; + this.optionCollection.forEach((option: SelectOption) => { + if (!option || (option[this.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: ' + + 'type: FilterType.multipleSelect, collection: [ { value: \'1\', label: \'One\' } ])'); + } + const labelKey = (option.labelKey || option[this.labelName]) as string; + const textLabel = labelKey; + + options += ``; + }); + + return ``; + } + + private createDomElement(filterTemplate: string) { + this.$filterElm = $(filterTemplate); + + if (this.$filterElm && typeof this.$filterElm.appendTo === 'function') { + this.$filterElm.appendTo(this.args.container); + } + + if (typeof this.$filterElm.multipleSelect !== 'function') { + // fallback to bootstrap + this.$filterElm.addClass('form-control'); + } else { + const filterOptions = (this.columnDef.filter) ? this.columnDef.filter.filterOptions : {}; + const options: MultipleSelectOption = { ...this.defaultOptions, ...filterOptions }; + this.$filterElm = this.$filterElm.multipleSelect(options); + } + } + + // refresh the jquery object because the selected checkboxes were already set + // prior to this method being called + private refresh() { + if (typeof this.$filterElm.multipleSelect === 'function') { + this.$filterElm.data('multipleSelect').refresh(); + } + } +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/formatters/arrayToCsvFormatter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/arrayToCsvFormatter.ts index f6002ec92..dbac20a2f 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/formatters/arrayToCsvFormatter.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/arrayToCsvFormatter.ts @@ -2,7 +2,8 @@ import { Column, Formatter } from './../models/index'; export const arrayToCsvFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => { if (value && Array.isArray(value)) { - return value.join(', '); + const values = value.join(', '); + return `${values}`; } return ''; }; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/formatters/collectionFormatter.ts b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/collectionFormatter.ts new file mode 100644 index 000000000..84a400168 --- /dev/null +++ b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/collectionFormatter.ts @@ -0,0 +1,26 @@ +import { arrayToCsvFormatter } from './arrayToCsvFormatter'; +import { Column, Formatter } from './../models/index'; + +/** + * A formatter to show the label property value of a filter.collection + */ +export const collectionFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => { + if (!value || !columnDef || !columnDef.filter || !columnDef.filter.collection + || !columnDef.filter.collection.length) { + return ''; + } + + const { filter, filter: { collection } } = columnDef; + const labelName = (filter.customStructure) ? filter.customStructure.label : 'label'; + const valueName = (filter.customStructure) ? filter.customStructure.value : 'value'; + + if (Array.isArray(value)) { + return arrayToCsvFormatter(row, + cell, + value.map((v: any) => collection.findOrDefault((c: any) => c[valueName] === v)[labelName]), + columnDef, + dataContext); + } + + return collection.findOrDefault((c: any) => c[valueName] === value)[labelName] || ''; +}; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/formatters/index.ts b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/index.ts index b22bb066a..7a0284f07 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/formatters/index.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/formatters/index.ts @@ -23,6 +23,7 @@ import { translateFormatter } from './translateFormatter'; import { translateBooleanFormatter } from './translateBooleanFormatter'; import { uppercaseFormatter } from './uppercaseFormatter'; import { yesNoFormatter } from './yesNoFormatter'; +import { collectionFormatter } from './collectionFormatter'; /* export interface GroupFormatter { @@ -47,6 +48,9 @@ export const Formatters = { /** Takes a complex data object and return the data under that property (for example: "user.firstName" will return the first name "John") */ complexObject: complexObjectFormatter, + /** Looks up values from the filter.collection property and convert it to a CSV or string */ + collection: collectionFormatter, + /** Takes a Date object and displays it as an ISO Date format */ dateIso: dateIsoFormatter, diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/models/multipleSelectOption.interface.ts b/aurelia-slickgrid/src/aurelia-slickgrid/models/multipleSelectOption.interface.ts index f40c20ba9..ab4987032 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/models/multipleSelectOption.interface.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/models/multipleSelectOption.interface.ts @@ -90,7 +90,7 @@ export interface MultipleSelectOption { single?: boolean; /** Define the width property of the dropdown list, support a percentage setting.By default this option is set to undefined. Which is the same as the select input field. */ - width?: number; + width?: number | string; // -- // Methods diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/global-utilities.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/global-utilities.ts index e176bd6dd..a9dadc9d6 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/global-utilities.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/global-utilities.ts @@ -5,6 +5,10 @@ declare interface StringConstructor { titleCase(inputStr: string): string; } +declare interface Array { + findOrDefault(logic: (item: any) => boolean): {}; +} + String.format = (format: string, ...args): string => { // const args = (Array.isArray(arguments[1])) ? arguments[1] : Array.prototype.slice.call(arguments, 1); @@ -41,3 +45,14 @@ String.allTitleCase = (inputStr: string): string => { String.titleCase = (inputStr: string): string => { return inputStr.charAt(0).toUpperCase() + inputStr.slice(1); }; + +/** + * Uses the logic function to find an item in an array or returns the default + * value provided (empty object by default) + * @param function logic the logic to find the item + * @param any [defaultVal={}] the default value to return + * @return object the found object or deafult value + */ +Array.prototype.findOrDefault = function(logic: (item: any) => boolean, defaultVal = {}): any { + return this.find(logic) || defaultVal; +}; diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts b/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts index f127bab23..eb9070e5d 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/services/utilities.ts @@ -265,3 +265,37 @@ export function toCamelCase(str: string): string { export function toKebabCase(str: string): string { return toCamelCase(str).replace(/([A-Z])/g, '-$1').toLowerCase(); } + +/** + * Compares two arrays to determine if all the items are equal + * @param a first array + * @param b second array to compare with a + * @param [orderMatters=false] flag if the order matters, if not arrays will be sorted + * @return boolean true if equal, else false + */ +export function arraysEqual(a: any[], b: any[], orderMatters: boolean = false): boolean { + if (a === b) { + return true; + } + + if (a === null || b === null) { + return false; + } + + if (a.length !== b.length) { + return false; + } + + if (!orderMatters) { + a.sort(); + b.sort(); + } + + for (let i = 0; i < a.length; ++i) { + if (a[i] !== b[i]) { + return false; + } + } + + return true; +} diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/styles/slick-bootstrap.scss b/aurelia-slickgrid/src/aurelia-slickgrid/styles/slick-bootstrap.scss index f52062f86..79a5b309c 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/styles/slick-bootstrap.scss +++ b/aurelia-slickgrid/src/aurelia-slickgrid/styles/slick-bootstrap.scss @@ -146,6 +146,10 @@ &.selected { background-color: $row-selected-color; } + select:not([multiple]).form-control { + height: 100%; + padding: 0; + } } } From 81be26300f7db6ee03f643e32cd50da41d48d455 Mon Sep 17 00:00:00 2001 From: Jeremy Zagorski Date: Sat, 3 Mar 2018 16:39:55 -0500 Subject: [PATCH 2/4] docs(demo): add singleSelect and multiSelect to example3 --- .../src/examples/slickgrid/example3.ts | 156 +++++++++++++----- 1 file changed, 118 insertions(+), 38 deletions(-) diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.ts b/aurelia-slickgrid/src/examples/slickgrid/example3.ts index e0ce9f399..06a31eced 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example3.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example3.ts @@ -1,6 +1,21 @@ -import { I18N } from 'aurelia-i18n'; -import { autoinject, bindable } from 'aurelia-framework'; -import { Column, Editors, FieldType, Formatters, GridExtraService, GridExtraUtils, GridOption, OnEventArgs, ResizerService } from '../../aurelia-slickgrid'; +import { + I18N +} from 'aurelia-i18n'; +import { + autoinject, + bindable +} from 'aurelia-framework'; +import { + Column, + Editors, + FieldType, + Formatters, + GridExtraService, + GridExtraUtils, + GridOption, + OnEventArgs, + ResizerService +} from '../../aurelia-slickgrid'; // using external non-typed js libraries declare var Slick: any; @@ -47,40 +62,102 @@ export class Example3 { /* Define grid Options and Columns */ defineGrid() { - this.columnDefinitions = [ - { - id: 'edit', field: 'id', - formatter: Formatters.editIcon, - minWidth: 30, - maxWidth: 30, - // use onCellClick OR grid.onClick.subscribe which you can see down below - onCellClick: (args: OnEventArgs) => { - console.log(args); - this.alertWarning = `Editing: ${args.dataContext.title}`; - this.gridExtraService.highlightRow(args.row, 1500); - this.gridExtraService.setSelectedRow(args.row); - } - }, - { - id: 'delete', field: 'id', - formatter: Formatters.deleteIcon, - minWidth: 30, - maxWidth: 30, - // use onCellClick OR grid.onClick.subscribe which you can see down below - /* - onCellClick: (args: OnEventArgs) => { - console.log(args); - this.alertWarning = `Deleting: ${args.dataContext.title}`; - } - */ + this.columnDefinitions = [{ + id: 'edit', + field: 'id', + formatter: Formatters.editIcon, + minWidth: 30, + maxWidth: 30, + // use onCellClick OR grid.onClick.subscribe which you can see down below + onCellClick: (args: OnEventArgs) => { + console.log(args); + this.alertWarning = `Editing: ${args.dataContext.title}`; + this.gridExtraService.highlightRow(args.row, 1500); + this.gridExtraService.setSelectedRow(args.row); + } + }, { + id: 'delete', + field: 'id', + formatter: Formatters.deleteIcon, + minWidth: 30, + maxWidth: 30, + // use onCellClick OR grid.onClick.subscribe which you can see down below + /* + onCellClick: (args: OnEventArgs) => { + console.log(args); + this.alertWarning = `Deleting: ${args.dataContext.title}`; + } + */ + }, { + id: 'title', + name: 'Title', + field: 'title', + sortable: true, + type: FieldType.string, + editor: Editors.longText, + minWidth: 100 + }, { + id: 'duration', + name: 'Duration (days)', + field: 'duration', + sortable: true, + type: FieldType.number, + editor: Editors.text, + minWidth: 100 + }, { + id: 'complete', + name: '% Complete', + field: 'percentComplete', + formatter: Formatters.multiple, + type: FieldType.number, + editor: Editors.singleSelect, + minWidth: 100, + params: { + formatters: [ Formatters.collection, Formatters.percentCompleteBar ], }, - { id: 'title', name: 'Title', field: 'title', sortable: true, type: FieldType.string, editor: Editors.longText, minWidth: 100 }, - { id: 'duration', name: 'Duration (days)', field: 'duration', sortable: true, type: FieldType.number, editor: Editors.text, minWidth: 100 }, - { id: 'complete', name: '% Complete', field: 'percentComplete', formatter: Formatters.percentCompleteBar, type: FieldType.number, editor: Editors.integer, minWidth: 100 }, - { id: 'start', name: 'Start', field: 'start', formatter: Formatters.dateIso, sortable: true, minWidth: 100, type: FieldType.date, editor: Editors.date, params: { i18n: this.i18n } }, - { id: 'finish', name: 'Finish', field: 'finish', formatter: Formatters.dateIso, sortable: true, minWidth: 100, type: FieldType.date, editor: Editors.date }, - { id: 'effort-driven', name: 'Effort Driven', field: 'effortDriven', formatter: Formatters.checkmark, type: FieldType.number, editor: Editors.checkbox, minWidth: 100 } - ]; + filter: { + collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: `${k}%` })) + } + }, { + id: 'start', + name: 'Start', + field: 'start', + formatter: Formatters.dateIso, + sortable: true, + minWidth: 100, + type: FieldType.date, + editor: Editors.date, + params: { + i18n: this.i18n + } + }, { + id: 'finish', + name: 'Finish', + field: 'finish', + formatter: Formatters.dateIso, + sortable: true, + minWidth: 100, + type: FieldType.date, + editor: Editors.date + }, { + id: 'effort-driven', + name: 'Effort Driven', + field: 'effortDriven', + formatter: Formatters.checkmark, + type: FieldType.number, + editor: Editors.checkbox, + minWidth: 100 + }, { + id: 'prerequisites', + name: 'Prerequisites', + field: 'prerequisites', + sortable: true, + type: FieldType.string, + editor: Editors.multipleSelect, + filter: { + collection: Array.from(Array(1001).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })) + } + }]; this.gridOptions = { asyncEditorLoading: false, @@ -119,7 +196,8 @@ export class Example3 { percentCompleteNumber: randomPercent, start: new Date(randomYear, randomMonth, randomDay), finish: new Date(randomYear, (randomMonth + 1), randomDay), - effortDriven: (i % 5 === 0) + effortDriven: (i % 5 === 0), + prerequisites: (i % 5 === 0) && i > 0 ? [ `Task ${i}`, `Task ${i - 1}` ] : [] }; } this.dataset = mockedDataset; @@ -166,7 +244,9 @@ export class Example3 { setAutoEdit(isAutoEdit) { this.isAutoEdit = isAutoEdit; - this.gridObj.setOptions({ autoEdit: isAutoEdit }); + this.gridObj.setOptions({ + autoEdit: isAutoEdit + }); return true; } From 82196311b556d5683b3303aa990bd116916e62bc Mon Sep 17 00:00:00 2001 From: Jeremy Zagorski Date: Mon, 5 Mar 2018 16:32:47 -0500 Subject: [PATCH 3/4] refactor(editors): use params not filter columnDef prop also make error message and editor property names more meaningful --- .../editors/multipleSelectEditor.ts | 74 +++++++++---------- .../editors/singleSelectEditor.ts | 74 +++++++++---------- .../src/examples/slickgrid/example3.ts | 4 +- 3 files changed, 73 insertions(+), 79 deletions(-) diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts index cc7efd068..a19a16568 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts @@ -14,7 +14,7 @@ export class MultipleSelectEditor implements Editor { /** * The JQuery DOM element */ - $filterElm: any; + $editorElm: any; /** * The slick grid column being edited */ @@ -30,13 +30,13 @@ export class MultipleSelectEditor implements Editor { /** * The options label/value object to use in the select list */ - optionCollection: SelectOption[] = []; + collection: SelectOption[] = []; /** - * The property name for values in the optionCollection + * The property name for values in the collection */ valueName: string; /** - * The property name for labels in the optionCollection + * The property name for labels in the collection */ labelName: string; @@ -55,25 +55,24 @@ export class MultipleSelectEditor implements Editor { } /** - * The current selected values from the optionCollection + * The current selected values from the collection */ get currentValues() { - return this.optionCollection - .filter(c => this.$filterElm.val().indexOf(c[this.valueName].toString()) !== -1) + return this.collection + .filter(c => this.$editorElm.val().indexOf(c[this.valueName].toString()) !== -1) .map(c => c[this.valueName]); } init() { if (!this.args) { - throw new Error('[Aurelia-SlickGrid] A filter must always have an "init()" ' + - 'with valid arguments.'); + throw new Error('[Aurelia-SlickGrid] An editor must always have an "init()" with valid arguments.'); } this.columnDef = this.args.column; - const filterTemplate = this.buildTemplateHtmlString(); + const editorTemplate = this.buildTemplateHtmlString(); - this.createDomElement(filterTemplate); + this.createDomElement(editorTemplate); } applyValue(item: any, state: any): void { @@ -81,14 +80,14 @@ export class MultipleSelectEditor implements Editor { } destroy() { - this.$filterElm.remove(); + this.$editorElm.remove(); } loadValue(item: any): void { // convert to string because that is how the DOM will return these values this.defaultValue = item[this.columnDef.field].map((i: any) => i.toString()); - this.$filterElm.find('option').each((i: number, $e: any) => { + this.$editorElm.find('option').each((i: number, $e: any) => { if (this.defaultValue.indexOf($e.value) !== -1) { $e.selected = true; } else { @@ -104,11 +103,11 @@ export class MultipleSelectEditor implements Editor { } focus() { - this.$filterElm.focus(); + this.$editorElm.focus(); } isValueChanged(): boolean { - return !arraysEqual(this.$filterElm.val(), this.defaultValue); + return !arraysEqual(this.$editorElm.val(), this.defaultValue); } validate() { @@ -126,23 +125,22 @@ export class MultipleSelectEditor implements Editor { } private buildTemplateHtmlString() { - if (!this.columnDef || !this.columnDef.filter || !this.columnDef.filter.collection) { - throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" for ' + - 'the MultipleSelect Filter to work correctly. Also each option should include ' + - 'a value/label pair (or value/labelKey when using Locale). For example:: ' + - '{ filter: type: FilterType.multipleSelect, collection: [{ value: true, label: \'True\' }, ' + - '{ value: false, label: \'False\'}] }'); + if (!this.columnDef || !this.columnDef.params || !this.columnDef.params.collection) { + throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" on the params property in the column definition for ' + + 'the MultipleSelect Editor to work correctly. Also each option should include ' + + 'a value/label pair (or value/labelKey when using Locale). For example: { params: { ' + + '{ collection: [{ value: true, label: \'True\' },{ value: false, label: \'False\'}] } } }'); } - this.optionCollection = this.columnDef.filter.collection || []; - this.labelName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.label : 'label'; - this.valueName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.value : 'value'; + this.collection = this.columnDef.params.collection || []; + this.labelName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.label : 'label'; + this.valueName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.value : 'value'; let options = ''; - this.optionCollection.forEach((option: SelectOption) => { + this.collection.forEach((option: SelectOption) => { if (!option || (option[this.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: ' + - 'type: FilterType.multipleSelect, collection: [ { value: \'1\', label: \'One\' } ])'); + '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 textLabel = labelKey; @@ -153,28 +151,28 @@ export class MultipleSelectEditor implements Editor { return ``; } - private createDomElement(filterTemplate: string) { - this.$filterElm = $(filterTemplate); + private createDomElement(editorTemplate: string) { + this.$editorElm = $(editorTemplate); - if (this.$filterElm && typeof this.$filterElm.appendTo === 'function') { - this.$filterElm.appendTo(this.args.container); + if (this.$editorElm && typeof this.$editorElm.appendTo === 'function') { + this.$editorElm.appendTo(this.args.container); } - if (typeof this.$filterElm.multipleSelect !== 'function') { + if (typeof this.$editorElm.multipleSelect !== 'function') { // fallback to bootstrap - this.$filterElm.addClass('form-control'); + this.$editorElm.addClass('form-control'); } else { - const filterOptions = (this.columnDef.filter) ? this.columnDef.filter.filterOptions : {}; - const options: MultipleSelectOption = { ...this.defaultOptions, ...filterOptions }; - this.$filterElm = this.$filterElm.multipleSelect(options); + const elementOptions = (this.columnDef.params) ? this.columnDef.params.elementOptions : {}; + const options: MultipleSelectOption = { ...this.defaultOptions, ...elementOptions }; + this.$editorElm = this.$editorElm.multipleSelect(options); } } // refresh the jquery object because the selected checkboxes were already set // prior to this method being called private refresh() { - if (typeof this.$filterElm.multipleSelect === 'function') { - this.$filterElm.data('multipleSelect').refresh(); + if (typeof this.$editorElm.multipleSelect === 'function') { + this.$editorElm.data('multipleSelect').refresh(); } } } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts index 4d1a76ff0..97fe4361c 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts @@ -13,7 +13,7 @@ export class SingleSelectEditor implements Editor { /** * The JQuery DOM element */ - $filterElm: any; + $editorElm: any; /** * The slick grid column being edited */ @@ -29,13 +29,13 @@ export class SingleSelectEditor implements Editor { /** * The options label/value object to use in the select list */ - optionCollection: SelectOption[] = []; + collection: SelectOption[] = []; /** - * The property name for values in the optionCollection + * The property name for values in the collection */ valueName: string; /** - * The property name for labels in the optionCollection + * The property name for labels in the collection */ labelName: string; @@ -52,24 +52,23 @@ export class SingleSelectEditor implements Editor { } /** - * The current selected value from the optionCollection + * The current selected value from the collection */ get currentValue() { - return this.optionCollection.findOrDefault(c => - c[this.valueName].toString() === this.$filterElm.val())[this.valueName]; + return this.collection.findOrDefault(c => + c[this.valueName].toString() === this.$editorElm.val())[this.valueName]; } init() { if (!this.args) { - throw new Error('[Aurelia-SlickGrid] A filter must always have an "init()" ' + - 'with valid arguments.'); + throw new Error('[Aurelia-SlickGrid] An editor must always have an "init()" with valid arguments.'); } this.columnDef = this.args.column; - const filterTemplate = this.buildTemplateHtmlString(); + const editorTemplate = this.buildTemplateHtmlString(); - this.createDomElement(filterTemplate); + this.createDomElement(editorTemplate); } applyValue(item: any, state: any): void { @@ -77,14 +76,14 @@ export class SingleSelectEditor implements Editor { } destroy() { - this.$filterElm.remove(); + this.$editorElm.remove(); } loadValue(item: any): void { // convert to string because that is how the DOM will return these values this.defaultValue = item[this.columnDef.field].toString(); - this.$filterElm.find('option').each((i: number, $e: any) => { + this.$editorElm.find('option').each((i: number, $e: any) => { if (this.defaultValue.indexOf($e.value) !== -1) { $e.selected = true; } else { @@ -100,11 +99,11 @@ export class SingleSelectEditor implements Editor { } focus() { - this.$filterElm.focus(); + this.$editorElm.focus(); } isValueChanged(): boolean { - return this.$filterElm.val() !== this.defaultValue; + return this.$editorElm.val() !== this.defaultValue; } validate() { @@ -122,23 +121,22 @@ export class SingleSelectEditor implements Editor { } private buildTemplateHtmlString() { - if (!this.columnDef || !this.columnDef.filter || !this.columnDef.filter.collection) { - throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" for ' + - 'the MultipleSelect Filter to work correctly. Also each option should include ' + - 'a value/label pair (or value/labelKey when using Locale). For example:: ' + - '{ filter: type: FilterType.multipleSelect, collection: [{ value: true, label: \'True\' }, ' + - '{ value: false, label: \'False\'}] }'); + if (!this.columnDef || !this.columnDef.params || !this.columnDef.params.collection) { + throw new Error('[Aurelia-SlickGrid] You need to pass a "collection" on the params property in the column definition for ' + + 'the SingleSelect Editor to work correctly. Also each option should include ' + + 'a value/label pair (or value/labelKey when using Locale). For example: { params: { ' + + '{ collection: [{ value: true, label: \'True\' }, { value: false, label: \'False\'}] } } }'); } - this.optionCollection = this.columnDef.filter.collection || []; - this.labelName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.label : 'label'; - this.valueName = (this.columnDef.filter.customStructure) ? this.columnDef.filter.customStructure.value : 'value'; + this.collection = this.columnDef.params.collection || []; + this.labelName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.label : 'label'; + this.valueName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.value : 'value'; let options = ''; - this.optionCollection.forEach((option: SelectOption) => { + this.collection.forEach((option: SelectOption) => { if (!option || (option[this.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: ' + - 'type: FilterType.multipleSelect, collection: [ { value: \'1\', label: \'One\' } ])'); + '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 textLabel = labelKey; @@ -149,28 +147,28 @@ export class SingleSelectEditor implements Editor { return ``; } - private createDomElement(filterTemplate: string) { - this.$filterElm = $(filterTemplate); + private createDomElement(editorTemplate: string) { + this.$editorElm = $(editorTemplate); - if (this.$filterElm && typeof this.$filterElm.appendTo === 'function') { - this.$filterElm.appendTo(this.args.container); + if (this.$editorElm && typeof this.$editorElm.appendTo === 'function') { + this.$editorElm.appendTo(this.args.container); } - if (typeof this.$filterElm.multipleSelect !== 'function') { + if (typeof this.$editorElm.multipleSelect !== 'function') { // fallback to bootstrap - this.$filterElm.addClass('form-control'); + this.$editorElm.addClass('form-control'); } else { - const filterOptions = (this.columnDef.filter) ? this.columnDef.filter.filterOptions : {}; - const options: MultipleSelectOption = { ...this.defaultOptions, ...filterOptions }; - this.$filterElm = this.$filterElm.multipleSelect(options); + const elementOptions = (this.columnDef.params) ? this.columnDef.params.elementOptions : {}; + const options: MultipleSelectOption = { ...this.defaultOptions, ...elementOptions }; + this.$editorElm = this.$editorElm.multipleSelect(options); } } // refresh the jquery object because the selected checkboxes were already set // prior to this method being called private refresh() { - if (typeof this.$filterElm.multipleSelect === 'function') { - this.$filterElm.data('multipleSelect').refresh(); + if (typeof this.$editorElm.multipleSelect === 'function') { + this.$editorElm.data('multipleSelect').refresh(); } } } diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.ts b/aurelia-slickgrid/src/examples/slickgrid/example3.ts index 06a31eced..52c7d1aa9 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example3.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example3.ts @@ -114,8 +114,6 @@ export class Example3 { minWidth: 100, params: { formatters: [ Formatters.collection, Formatters.percentCompleteBar ], - }, - filter: { collection: Array.from(Array(101).keys()).map(k => ({ value: k, label: `${k}%` })) } }, { @@ -154,7 +152,7 @@ export class Example3 { sortable: true, type: FieldType.string, editor: Editors.multipleSelect, - filter: { + params: { collection: Array.from(Array(1001).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })) } }]; From 8c32223766f87b8c05b1d18f64a1b56e073597f8 Mon Sep 17 00:00:00 2001 From: Jeremy Zagorski Date: Tue, 6 Mar 2018 12:15:47 -0500 Subject: [PATCH 4/4] refactor(editors): add i18n and open select on cell click also create an exact width so it plays nice with the multipl-select library --- .../editors/multipleSelectEditor.ts | 27 +++++++++++++++---- .../editors/singleSelectEditor.ts | 20 ++++++++++---- .../src/examples/slickgrid/example3.ts | 3 ++- 3 files changed, 39 insertions(+), 11 deletions(-) diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts index a19a16568..9d20d1eb7 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/multipleSelectEditor.ts @@ -1,3 +1,4 @@ +import { I18N } from 'aurelia-i18n'; import { arraysEqual } from '../services/index'; import { Editor, @@ -39,18 +40,31 @@ export class MultipleSelectEditor implements Editor { * The property name for labels in the collection */ labelName: string; + /** + * The i18n aurelia library + */ + private _i18n: I18N; constructor(private args: any) { + this._i18n = this.args.column.params.i18n; + this.defaultOptions = { container: 'body', filter: false, maxHeight: 200, - width: '100%', - okButton: true, addTitle: true, - selectAllDelimiter: ['', ''] + okButton: true, + selectAllDelimiter: ['', ''], + width: 150, + offsetLeft: 20 }; + if (this._i18n) { + this.defaultOptions.countSelected = this._i18n.tr('X_OF_Y_SELECTED'); + this.defaultOptions.allSelected = this._i18n.tr('ALL_SELECTED'); + this.defaultOptions.selectAllText = this._i18n.tr('SELECT_ALL'); + } + this.init(); } @@ -134,6 +148,7 @@ export class MultipleSelectEditor implements Editor { this.collection = this.columnDef.params.collection || []; this.labelName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.label : 'label'; this.valueName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.value : 'value'; + const isEnabledTranslate = (this.columnDef.params.enableTranslateLabel) ? this.columnDef.params.enableTranslateLabel : false; let options = ''; this.collection.forEach((option: SelectOption) => { @@ -143,7 +158,8 @@ export class MultipleSelectEditor implements Editor { '{ collection: [ { value: \'1\', label: \'One\' } ])'); } const labelKey = (option.labelKey || option[this.labelName]) as string; - const textLabel = labelKey; + + const textLabel = ((option.labelKey || isEnabledTranslate) && this._i18n && typeof this._i18n.tr === 'function') ? this._i18n.tr(labelKey || ' ') : labelKey; options += ``; }); @@ -165,6 +181,7 @@ export class MultipleSelectEditor implements Editor { const elementOptions = (this.columnDef.params) ? this.columnDef.params.elementOptions : {}; const options: MultipleSelectOption = { ...this.defaultOptions, ...elementOptions }; this.$editorElm = this.$editorElm.multipleSelect(options); + setTimeout(() => this.$editorElm.multipleSelect('open')); } } @@ -172,7 +189,7 @@ export class MultipleSelectEditor implements Editor { // prior to this method being called private refresh() { if (typeof this.$editorElm.multipleSelect === 'function') { - this.$editorElm.data('multipleSelect').refresh(); + this.$editorElm.multipleSelect('refresh'); } } } diff --git a/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts index 97fe4361c..75fdd835b 100644 --- a/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts +++ b/aurelia-slickgrid/src/aurelia-slickgrid/editors/singleSelectEditor.ts @@ -1,3 +1,4 @@ +import { I18N } from 'aurelia-i18n'; import { Editor, Column, @@ -38,13 +39,20 @@ export class SingleSelectEditor implements Editor { * The property name for labels in the collection */ labelName: string; + /** + * The i18n aurelia library + */ + private _i18n: I18N; constructor(private args: any) { + this._i18n = this.args.column.params.i18n; + this.defaultOptions = { container: 'body', filter: false, maxHeight: 200, - width: '100%', + width: 150, + offsetLeft: 20, single: true }; @@ -130,16 +138,17 @@ export class SingleSelectEditor implements Editor { this.collection = this.columnDef.params.collection || []; this.labelName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.label : 'label'; this.valueName = (this.columnDef.params.customStructure) ? this.columnDef.params.customStructure.value : 'value'; + const isEnabledTranslate = (this.columnDef.params.enableTranslateLabel) ? this.columnDef.params.enableTranslateLabel : false; let options = ''; this.collection.forEach((option: SelectOption) => { if (!option || (option[this.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: ' + - '{ collection: [ { value: \'1\', label: \'One\' } ])'); + 'Locale) is required to populate the Select list, for example: { params: { ' + + '{ collection: [ { value: \'1\', label: \'One\' } ] } } }'); } const labelKey = (option.labelKey || option[this.labelName]) as string; - const textLabel = labelKey; + const textLabel = ((option.labelKey || isEnabledTranslate) && this._i18n && typeof this._i18n.tr === 'function') ? this._i18n.tr(labelKey || ' ') : labelKey; options += ``; }); @@ -161,6 +170,7 @@ export class SingleSelectEditor implements Editor { const elementOptions = (this.columnDef.params) ? this.columnDef.params.elementOptions : {}; const options: MultipleSelectOption = { ...this.defaultOptions, ...elementOptions }; this.$editorElm = this.$editorElm.multipleSelect(options); + setTimeout(() => this.$editorElm.multipleSelect('open')); } } @@ -168,7 +178,7 @@ export class SingleSelectEditor implements Editor { // prior to this method being called private refresh() { if (typeof this.$editorElm.multipleSelect === 'function') { - this.$editorElm.data('multipleSelect').refresh(); + this.$editorElm.multipleSelect('refresh'); } } } diff --git a/aurelia-slickgrid/src/examples/slickgrid/example3.ts b/aurelia-slickgrid/src/examples/slickgrid/example3.ts index 52c7d1aa9..ec70aa3d4 100644 --- a/aurelia-slickgrid/src/examples/slickgrid/example3.ts +++ b/aurelia-slickgrid/src/examples/slickgrid/example3.ts @@ -153,7 +153,8 @@ export class Example3 { type: FieldType.string, editor: Editors.multipleSelect, params: { - collection: Array.from(Array(1001).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })) + collection: Array.from(Array(10).keys()).map(k => ({ value: `Task ${k}`, label: `Task ${k}` })), + i18n: this.i18n } }];