From 405e575c7834a24cb944601c10cb9e3189395cfd Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Mon, 29 Apr 2019 11:08:18 -0400 Subject: [PATCH] fix(copy): copy+paste cells was not working, closes #164 - multiple issues found, 1. pasting was creating additional dom elements 2. cell was not active (not in focus) and paste was behaving incorrectly because of that 3. few Editors paste were not working correctly either --- .vscode/launch.json | 4 +- src/app/examples/grid-editor.component.ts | 4 +- .../editors/autoCompleteEditor.ts | 15 ++++- .../angular-slickgrid/editors/dateEditor.ts | 4 +- .../angular-slickgrid/editors/selectEditor.ts | 46 +++++++++++---- .../formatters/checkmarkFormatter.spec.ts | 22 ++++++-- .../formatters/checkmarkFormatter.ts | 6 +- .../models/editor.interface.ts | 56 +++++++++++++++++++ .../services/gridEvent.service.ts | 7 +++ .../angular-slickgrid/services/utilities.ts | 5 ++ .../lib/multiple-select/multiple-select.js | 3 +- 11 files changed, 147 insertions(+), 25 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 103333464..8df90e4c8 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,7 +4,7 @@ { "type": "chrome", "request": "launch", - "name": "Launch Chrome against localhost", + "name": "Chrome Debugger", "url": "http://localhost:4300", "webRoot": "${workspaceFolder}" }, @@ -34,7 +34,7 @@ { "type": "node", "request": "launch", - "name": "Jest Current File", + "name": "Jest Current Spec File", "program": "${workspaceFolder}/node_modules/.bin/jest", "args": [ "--runInBand", diff --git a/src/app/examples/grid-editor.component.ts b/src/app/examples/grid-editor.component.ts index a15240cfb..bf7b298c3 100644 --- a/src/app/examples/grid-editor.component.ts +++ b/src/app/examples/grid-editor.component.ts @@ -515,9 +515,11 @@ export class GridEditorComponent implements OnInit { const tempDataset = []; for (let i = startingIndex; i < (startingIndex + itemCount); i++) { const randomYear = 2000 + Math.floor(Math.random() * 10); + const randomFinishYear = (new Date().getFullYear() - 3) + Math.floor(Math.random() * 10); // use only years not lower than 3 years ago const randomMonth = Math.floor(Math.random() * 11); const randomDay = Math.floor((Math.random() * 29)); const randomPercent = Math.round(Math.random() * 100); + const randomFinish = new Date(randomFinishYear, (randomMonth + 1), randomDay); tempDataset.push({ id: i, @@ -526,7 +528,7 @@ export class GridEditorComponent implements OnInit { percentComplete: randomPercent, percentCompleteNumber: randomPercent, start: new Date(randomYear, randomMonth, randomDay), - finish: new Date(randomYear, (randomMonth + 1), randomDay), + finish: randomFinish < new Date() ? '' : randomFinish, // make sure the random date is earlier than today effortDriven: (i % 5 === 0), prerequisites: (i % 2 === 0) && i !== 0 && i < 12 ? [i, i - 1] : [], countryOfOrigin: (i % 2) ? { code: 'CA', name: 'Canada' } : { code: 'US', name: 'United States' }, diff --git a/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts b/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts index 2a8fd7a6d..5c9a362c5 100644 --- a/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/autoCompleteEditor.ts @@ -9,6 +9,7 @@ import { FieldType } from './../models/index'; import { Constants } from './../constants'; +import { findOrDefault } from '../services/utilities'; // using external non-typed js libraries declare var $: any; @@ -169,10 +170,22 @@ export class AutoCompleteEditor implements Editor { } applyValue(item: any, state: any) { + let newValue = state; const fieldName = this.columnDef && this.columnDef.field; + + // if we have a collection defined, we will try to find the string within the collection and return it + if (Array.isArray(this.collection) && this.collection.length > 0) { + newValue = findOrDefault(this.collection, (collectionItem: any) => { + if (collectionItem && collectionItem.hasOwnProperty(this.labelName)) { + return collectionItem[this.labelName].toString() === state; + } + return collectionItem.toString() === state; + }); + } + // when it's a complex object, then pull the object name only, e.g.: "user.firstName" => "user" const fieldNameFromComplexObject = fieldName.indexOf('.') ? fieldName.substring(0, fieldName.indexOf('.')) : ''; - item[fieldNameFromComplexObject || fieldName] = state; + item[fieldNameFromComplexObject || fieldName] = newValue; } isValueChanged() { diff --git a/src/app/modules/angular-slickgrid/editors/dateEditor.ts b/src/app/modules/angular-slickgrid/editors/dateEditor.ts index 1acd0c3f1..1c110507f 100644 --- a/src/app/modules/angular-slickgrid/editors/dateEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/dateEditor.ts @@ -74,7 +74,6 @@ export class DateEditor implements Editor { this.$input = $(``); this.$input.appendTo(this.args.container); this.flatInstance = (this.$input[0] && typeof this.$input[0].flatpickr === 'function') ? this.$input[0].flatpickr(pickerMergedOptions) : null; - this.show(); // when we're using an alternate input to display data, we'll consider this input as the one to do the focus later on // else just use the top one @@ -106,6 +105,9 @@ export class DateEditor implements Editor { destroy() { this.hide(); this.$input.remove(); + if (this._$inputWithData && typeof this._$inputWithData.remove === 'function') { + this._$inputWithData.remove(); + } } show() { diff --git a/src/app/modules/angular-slickgrid/editors/selectEditor.ts b/src/app/modules/angular-slickgrid/editors/selectEditor.ts index 3c150b278..44fae3ced 100644 --- a/src/app/modules/angular-slickgrid/editors/selectEditor.ts +++ b/src/app/modules/angular-slickgrid/editors/selectEditor.ts @@ -8,6 +8,7 @@ import { Editor, EditorValidator, EditorValidatorOutput, + FieldType, GridOption, MultipleSelectOption, SelectOption, @@ -272,18 +273,34 @@ export class SelectEditor implements Editor { applyValue(item: any, state: any): void { const fieldName = this.columnDef && this.columnDef.field; + const fieldType = this.columnDef && this.columnDef.type; + let value = state; + + // when the provided user defined the column field type as a possible number then try parsing the state value as that + if (fieldType === FieldType.number || fieldType === FieldType.integer || fieldType === FieldType.boolean) { + value = parseFloat(state); + } + + // when set as a multiple selection, we can assume that the 3rd party lib multiple-select will return a CSV string + // we need to re-split that into an array to be the same as the original column + if (this.isMultipleSelect && typeof state === 'string' && state.indexOf(',') >= 0) { + value = state.split(','); + } + // when it's a complex object, then pull the object name only, e.g.: "user.firstName" => "user" const fieldNameFromComplexObject = fieldName.indexOf('.') ? fieldName.substring(0, fieldName.indexOf('.')) : ''; - item[fieldNameFromComplexObject || fieldName] = state; + item[fieldNameFromComplexObject || fieldName] = value; } destroy() { this._destroying = true; - if (this.$editorElm && this.$editorElm.multipleSelect) { - this.$editorElm.multipleSelect('close'); + if (this.$editorElm && typeof this.$editorElm.multipleSelect === 'function') { + this.$editorElm.multipleSelect('destroy'); this.$editorElm.remove(); const elementClassName = this.elementName.toString().replace('.', '\\.'); // make sure to escape any dot "." from CSS class to avoid console error $(`[name=${elementClassName}].ms-drop`).remove(); + } else if (this.$editorElm && typeof this.$editorElm.remove === 'function') { + this.$editorElm.remove(); } this._subscriptions = unsubscribeAllObservables(this._subscriptions); } @@ -309,20 +326,25 @@ export class SelectEditor implements Editor { loadMultipleValues(currentValues: any[]) { // convert to string because that is how the DOM will return these values if (Array.isArray(currentValues)) { - this.defaultValue = currentValues.map((i: any) => i.toString()); + // keep the default values in memory for references + this.defaultValue = currentValues.map((i: any) => i); + + // compare all the array values but as string type since multiple-select always return string + const currentStringValues = currentValues.map((i: any) => i.toString()); this.$editorElm.find('option').each((i: number, $e: any) => { - $e.selected = (this.defaultValue.indexOf($e.value) !== -1); + $e.selected = (currentStringValues.indexOf($e.value) !== -1); }); } } loadSingleValue(currentValue: any) { - // convert to string because that is how the DOM will return these values - // make sure the prop exists first - this.defaultValue = currentValue && currentValue.toString(); + // keep the default value in memory for references + this.defaultValue = currentValue; + // make sure the prop exists first this.$editorElm.find('option').each((i: number, $e: any) => { - $e.selected = (this.defaultValue === $e.value); + // check equality after converting defaultValue to string since the DOM value will always be of type string + $e.selected = (currentValue.toString() === $e.value); }); } @@ -513,7 +535,11 @@ export class SelectEditor implements Editor { const elementOptions = (this.columnDef.internalColumnEditor) ? this.columnDef.internalColumnEditor.elementOptions : {}; this.editorElmOptions = { ...this.defaultOptions, ...elementOptions }; this.$editorElm = this.$editorElm.multipleSelect(this.editorElmOptions); - setTimeout(() => this.$editorElm.multipleSelect('open')); + setTimeout(() => { + if (this.$editorElm && typeof this.$editorElm.multipleSelect === 'function') { + this.$editorElm.multipleSelect('open'); + } + }); } } diff --git a/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.spec.ts b/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.spec.ts index f98d72caf..726778e8a 100644 --- a/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.spec.ts +++ b/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.spec.ts @@ -14,14 +14,24 @@ describe('the Checkmark Formatter', () => { expect(result).toBe(''); }); - it('should return the Font Awesome Checkmark icon when input is True', () => { - const value = true; - const result = checkmarkFormatter(0, 0, value, {} as Column, {}); - expect(result).toBe(''); + it('should return an empty string when the string "FALSE" (case insensitive) is provided', () => { + const value = 'FALSE'; + const result1 = checkmarkFormatter(0, 0, value.toLowerCase(), {} as Column, {}); + const result2 = checkmarkFormatter(0, 0, value.toUpperCase(), {} as Column, {}); + expect(result1).toBe(''); + expect(result2).toBe(''); }); - it('should return the Font Awesome Checkmark icon when input is filled with any string', () => { - const value = 'anything'; + it('should return the Font Awesome Checkmark icon when the string "True" (case insensitive) is provided', () => { + const value = 'True'; + const result1 = checkmarkFormatter(0, 0, value.toLowerCase(), {} as Column, {}); + const result2 = checkmarkFormatter(0, 0, value.toUpperCase(), {} as Column, {}); + expect(result1).toBe(''); + expect(result2).toBe(''); + }); + + it('should return the Font Awesome Checkmark icon when input is True', () => { + const value = true; const result = checkmarkFormatter(0, 0, value, {} as Column, {}); expect(result).toBe(''); }); diff --git a/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.ts b/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.ts index 6a26eea52..a4cbcbf69 100644 --- a/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.ts +++ b/src/app/modules/angular-slickgrid/formatters/checkmarkFormatter.ts @@ -1,5 +1,7 @@ import { Column } from './../models/column.interface'; import { Formatter } from './../models/formatter.interface'; +import { parseBoolean } from '../services/utilities'; -export const checkmarkFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => - value ? `` : ''; +export const checkmarkFormatter: Formatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any) => { + return parseBoolean(value) ? `` : ''; +} diff --git a/src/app/modules/angular-slickgrid/models/editor.interface.ts b/src/app/modules/angular-slickgrid/models/editor.interface.ts index 02d16b215..3d9ac1def 100644 --- a/src/app/modules/angular-slickgrid/models/editor.interface.ts +++ b/src/app/modules/angular-slickgrid/models/editor.interface.ts @@ -1,17 +1,73 @@ import { EditorValidatorOutput } from './editorValidatorOutput.interface'; +/*** + * SlickGrid Editor interface, more info can be found on the SlickGrid repo + * https://github.com/6pac/SlickGrid/wiki/Writing-custom-cell-editors + */ export interface Editor { + /** Initialize the Editor */ init: () => void; + + /** Saves the Editor value */ save?: () => void; + + /** Cancels the Editor */ cancel?: () => void; + + /** + * if implemented, this will be called if the cell being edited is scrolled out of the view + * implement this is your UI is not appended to the cell itself or if you open any secondary + * selector controls (like a calendar for a datepicker input) + */ hide?: () => void; + + /** pretty much the opposite of hide */ show?: () => void; + + /** + * if implemented, this will be called by the grid if any of the cell containers are scrolled + * and the absolute position of the edited cell is changed + * if your UI is constructed as a child of document BODY, implement this to update the + * position of the elements as the position of the cell changes + * + * the cellBox: { top, left, bottom, right, width, height, visible } + */ position?: (position: any) => void; + + /** remove all data, events & dom elements created in the constructor */ destroy: () => void; + + /** set the focus on the main input control (if any) */ focus: () => void; + + /** + * Deserialize the value(s) saved to "state" and apply them to the data item + * this method may get called after the editor itself has been destroyed + * treat it as an equivalent of a Java/C# "static" method - no instance variables should be accessed + */ applyValue: (item: any, state: any) => void; + + /** + * Load the value(s) from the data item and update the UI + * this method will be called immediately after the editor is initialized + * it may also be called by the grid if if the row/cell being edited is updated via grid.updateRow/updateCell + */ loadValue: (item: any) => void; + + /** + * Return the value(s) being edited by the user in a serialized form + * can be an arbitrary object + * the only restriction is that it must be a simple object that can be passed around even + * when the editor itself has been destroyed + */ serializeValue: () => any; + + /** return true if the value(s) being edited by the user has/have been changed */ isValueChanged: () => boolean; + + /** + * Validate user input and return the result along with the validation message, if any + * if the input is valid, return {valid:true,msg:null} + */ validate: () => EditorValidatorOutput; } diff --git a/src/app/modules/angular-slickgrid/services/gridEvent.service.ts b/src/app/modules/angular-slickgrid/services/gridEvent.service.ts index 688d7e535..781c2f417 100644 --- a/src/app/modules/angular-slickgrid/services/gridEvent.service.ts +++ b/src/app/modules/angular-slickgrid/services/gridEvent.service.ts @@ -40,6 +40,13 @@ export class GridEventService { return; } const column = grid.getColumns()[args.cell]; + const gridOptions = grid.getOptions(); + + // only when using autoCommitEdit, we will make the cell active (in focus) when clicked + // setting the cell as active as a side effect and if autoCommitEdit is set to false then the Editors won't save correctly + if (gridOptions && gridOptions.enableCellNavigation && !gridOptions.editable || (gridOptions.editable && gridOptions.autoCommitEdit)) { + grid.setActiveCell(args.row, args.cell); + } // if the column definition has a onCellClick property (a callback function), then run it if (typeof column.onCellClick === 'function') { diff --git a/src/app/modules/angular-slickgrid/services/utilities.ts b/src/app/modules/angular-slickgrid/services/utilities.ts index 8560ab9e6..013bdf798 100644 --- a/src/app/modules/angular-slickgrid/services/utilities.ts +++ b/src/app/modules/angular-slickgrid/services/utilities.ts @@ -403,6 +403,11 @@ export function mapOperatorByFieldType(fieldType: FieldType | string): OperatorT return map; } +/** Parse any input (bool, number, string) and return a boolean or False when not possible */ +export function parseBoolean(input: boolean | number | string) { + return /(true|1)/i.test(input + ''); +} + /** * Parse a date passed as a string and return a Date object (if valid) * @param inputDateString diff --git a/src/assets/lib/multiple-select/multiple-select.js b/src/assets/lib/multiple-select/multiple-select.js index 60c261021..8b27ab2ef 100644 --- a/src/assets/lib/multiple-select/multiple-select.js +++ b/src/assets/lib/multiple-select/multiple-select.js @@ -897,9 +897,8 @@ }, destroy: function () { - this.$el.show(); + this.$el.before(this.$parent).show(); this.$parent.remove(); - delete $.fn.multipleSelect; }, checkAll: function () {