From 49107c14ca841edf7c279e9a0ffe334f1d5dc71a Mon Sep 17 00:00:00 2001 From: Ghislain Beaulac Date: Fri, 24 Apr 2020 18:19:39 -0400 Subject: [PATCH] feat(editor): start working on a Compound Editor --- .../common/src/editors/compoundInputEditor.ts | 214 ++++++ packages/common/src/editors/dateEditor.ts | 2 +- packages/common/src/editors/index.ts | 4 + packages/common/src/editors/sliderEditor.ts | 3 +- packages/common/src/styles/_variables.scss | 8 + .../src/styles/slick-default-theme.scss | 7 + packages/common/src/styles/slick-editors.scss | 29 +- .../slick-without-bootstrap-min-styling.scss | 7 + .../styles/slickgrid-theme-salesforce.scss | 1 + .../src/app-routing.ts | 1 - packages/web-demo-vanilla-bundle/src/app.html | 11 +- .../src/examples/example04.ts | 8 +- .../src/examples/example50.html | 38 ++ .../src/examples/example50.scss | 41 ++ .../src/examples/example50.ts | 618 ++++++++++++++++++ .../src/se-styles.scss | 3 +- .../web-demo-vanilla-bundle/webpack.config.js | 2 +- 17 files changed, 981 insertions(+), 16 deletions(-) create mode 100644 packages/common/src/editors/compoundInputEditor.ts create mode 100644 packages/web-demo-vanilla-bundle/src/examples/example50.html create mode 100644 packages/web-demo-vanilla-bundle/src/examples/example50.scss create mode 100644 packages/web-demo-vanilla-bundle/src/examples/example50.ts diff --git a/packages/common/src/editors/compoundInputEditor.ts b/packages/common/src/editors/compoundInputEditor.ts new file mode 100644 index 000000000..c26cd4043 --- /dev/null +++ b/packages/common/src/editors/compoundInputEditor.ts @@ -0,0 +1,214 @@ +import { Constants } from '../constants'; +import { KeyCode } from '../enums/keyCode.enum'; +import { Column, ColumnEditor, Editor, EditorArguments, EditorValidator, EditorValidatorOutput } from '../interfaces/index'; +import { getDescendantProperty, setDeepValue } from '../services/utilities'; + +/* + * An example of a 'detached' editor. + * KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter. + */ +export class CompoundInputEditor implements Editor { + protected _inputType = 'text'; + private _lastInputEvent: KeyboardEvent; + private _leftInput: HTMLInputElement; + private _rightInput: HTMLInputElement; + originalLeftValue: string; + originalRightValue: string; + + /** SlickGrid Grid object */ + grid: any; + + constructor(private args: EditorArguments) { + if (!args) { + throw new Error('[Slickgrid-Universal] Something is wrong with this grid, an Editor must always have valid arguments.'); + } + this.grid = args.grid; + this.init(); + } + + /** Get Column Definition object */ + get columnDef(): Column | undefined { + return this.args && this.args.column; + } + + /** Get Column Editor object */ + get columnEditor(): ColumnEditor { + return this.columnDef && this.columnDef.internalColumnEditor || {}; + } + + /** Getter of input type (text, number, password) */ + get inputType() { + return this._inputType; + } + + /** Setter of input type (text, number, password) */ + set inputType(type: string) { + this._inputType = type; + } + + /** Get the Editor DOM Element */ + get editorDomElement(): { leftInput: HTMLInputElement, rightInput: HTMLInputElement } { + return { leftInput: this._leftInput, rightInput: this._rightInput }; + } + + get hasAutoCommitEdit() { + return this.grid.getOptions().autoCommitEdit; + } + + /** Get the Validator function, can be passed in Editor property or Column Definition */ + get validator(): EditorValidator | undefined { + return (this.columnEditor && this.columnEditor.validator) || (this.columnDef && this.columnDef.validator); + } + + init() { + const editorParams = this.columnEditor.params; + if (!editorParams || !editorParams.leftField || !editorParams.rightField) { + throw new Error(`[Slickgrid-Universal] Please make sure that your Compound Editor has params defined with "leftField" and "rightField" (example: { editor: { model: Editors.compound, params: { leftField: 'firstName', rightField: 'lastName' } }}`); + } + this._leftInput = this.createInput('left'); + this._rightInput = this.createInput('right'); + + const cellContainer = this.args?.container; + if (cellContainer && typeof cellContainer.appendChild === 'function') { + cellContainer.appendChild(this._leftInput); + cellContainer.appendChild(this._rightInput); + } + + this._leftInput.onkeydown = ((event: KeyboardEvent) => { + this._lastInputEvent = event; + if (event.keyCode === KeyCode.LEFT || event.keyCode === KeyCode.RIGHT || event.keyCode === KeyCode.TAB) { + event.stopImmediatePropagation(); + } + }); + + // the lib does not get the focus out event for some reason + // so register it here + if (this.hasAutoCommitEdit) { + this._leftInput.addEventListener('focusout', () => this.save()); + } + + setTimeout(() => this.focus(), 50); + } + + destroy() { + const columnId = this.columnDef && this.columnDef.id; + const elm = document.querySelector(`.compound-editor-text.editor-${columnId}`); + if (elm) { + elm.removeEventListener('focusout', () => { }); + } + } + + createInput(position: 'left' | 'right'): HTMLInputElement { + const columnId = this.columnDef && this.columnDef.id; + const placeholder = this.columnEditor && this.columnEditor.placeholder || ''; + const title = this.columnEditor && this.columnEditor.title || ''; + const input = document.createElement('input') as HTMLInputElement; + input.className = `compound-editor-text editor-${columnId} ${position}`; + input.title = title; + input.type = this._inputType || 'text'; + input.setAttribute('role', 'presentation'); + input.autocomplete = 'off'; + input.placeholder = placeholder; + input.title = title; + + return input; + } + + focus(): void { + this._leftInput.focus(); + } + + getValue(): string { + return this._leftInput.value || ''; + } + + setValue(value: string) { + this._leftInput.value = value; + } + + applyValue(item: any, state: any) { + const fieldName = this.columnDef && this.columnDef.field; + if (fieldName !== undefined) { + const isComplexObject = fieldName && fieldName.indexOf('.') > 0; // is the field a complex object, "address.streetNumber" + + // validate the value before applying it (if not valid we'll set an empty string) + const validation = this.validate(state); + const newValue = (validation && validation.valid) ? state : ''; + + // set the new value to the item datacontext + if (isComplexObject) { + setDeepValue(item, fieldName, newValue); + } else if (fieldName) { + item[fieldName] = newValue; + } + } + } + + isValueChanged(): boolean { + const elmValue = this._leftInput.value; + const lastEvent = this._lastInputEvent && this._lastInputEvent.keyCode; + if (this.columnEditor && this.columnEditor.alwaysSaveOnEnterKey && lastEvent === KeyCode.ENTER) { + return true; + } + return (!(elmValue === '' && this.originalLeftValue === null)) && (elmValue !== this.originalLeftValue); + } + + loadValue(item: any) { + const leftFieldName = this.columnDef && this.columnDef.field; + const rightFieldName = this.columnEditor.params?.rightField; + + // is the field a complex object, "address.streetNumber" + const isComplexObject = leftFieldName && leftFieldName.indexOf('.') > 0; + + if (item && leftFieldName !== undefined && this.columnDef && (item.hasOwnProperty(leftFieldName) || isComplexObject)) { + const leftValue = (isComplexObject) ? getDescendantProperty(item, leftFieldName) : (item.hasOwnProperty(leftFieldName) && item[leftFieldName] || ''); + this.originalLeftValue = leftValue; + this._leftInput.value = this.originalLeftValue; + this._leftInput.select(); + } + + if (item && rightFieldName !== undefined && this.columnDef && (item.hasOwnProperty(rightFieldName) || isComplexObject)) { + const rightValue = (isComplexObject) ? getDescendantProperty(item, rightFieldName) : (item.hasOwnProperty(rightFieldName) && item[rightFieldName] || ''); + this.originalRightValue = rightValue; + this._rightInput.value = this.originalRightValue; + } + } + + save() { + const validation = this.validate(); + if (validation && validation.valid && this.isValueChanged()) { + if (this.hasAutoCommitEdit) { + this.grid.getEditorLock().commitCurrentEdit(); + } else { + this.args.commitChanges(); + } + } + } + + serializeValue() { + return this._leftInput.value; + } + + validate(inputValue?: any): EditorValidatorOutput { + const isRequired = this.columnEditor.required; + const elmValue = (inputValue !== undefined) ? inputValue : this._leftInput && this._leftInput.value; + const errorMsg = this.columnEditor.errorMessage; + + if (this.validator) { + return this.validator(elmValue, this.args); + } + + // by default the editor is almost always valid (except when it's required but not provided) + if (isRequired && elmValue === '') { + return { + valid: false, + msg: errorMsg || Constants.VALIDATION_REQUIRED_FIELD + }; + } + + return { + valid: true, + msg: null + }; + } +} diff --git a/packages/common/src/editors/dateEditor.ts b/packages/common/src/editors/dateEditor.ts index 867deae8e..a3c1327b6 100644 --- a/packages/common/src/editors/dateEditor.ts +++ b/packages/common/src/editors/dateEditor.ts @@ -126,7 +126,7 @@ export class DateEditor implements Editor { // 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 - this._$inputWithData = (pickerMergedOptions && pickerMergedOptions.altInput) ? $(`${inputCssClasses}.flatpickr-alt-input`) : this._$input; + this._$inputWithData = (pickerMergedOptions && pickerMergedOptions.altInput) ? $(`.${pickerMergedOptions.altInputClass.replace(' ', '.')}`) : this._$input; } } diff --git a/packages/common/src/editors/index.ts b/packages/common/src/editors/index.ts index 790f3c3b8..1c3a81575 100644 --- a/packages/common/src/editors/index.ts +++ b/packages/common/src/editors/index.ts @@ -1,5 +1,6 @@ import { AutoCompleteEditor } from './autoCompleteEditor'; import { CheckboxEditor } from './checkboxEditor'; +import { CompoundInputEditor } from './compoundInputEditor'; import { DateEditor } from './dateEditor'; import { FloatEditor } from './floatEditor'; import { IntegerEditor } from './integerEditor'; @@ -16,6 +17,9 @@ export const Editors = { /** Checkbox Editor (uses native checkbox DOM element) */ checkbox: CheckboxEditor, + /** Checkbox Editor (uses native checkbox DOM element) */ + compoundText: CompoundInputEditor, + /** Date Picker Editor (which uses 3rd party lib "flatpickr") */ date: DateEditor, diff --git a/packages/common/src/editors/sliderEditor.ts b/packages/common/src/editors/sliderEditor.ts index 7d36a42ce..67f554dff 100644 --- a/packages/common/src/editors/sliderEditor.ts +++ b/packages/common/src/editors/sliderEditor.ts @@ -99,6 +99,7 @@ export class SliderEditor implements Editor { }); } } + this.focus(); } cancel() { @@ -111,7 +112,7 @@ export class SliderEditor implements Editor { } focus() { - this._$editorElm.focus(); + this._$input.focus(); } getValue(): string { diff --git a/packages/common/src/styles/_variables.scss b/packages/common/src/styles/_variables.scss index 2b101ca27..3621c5c3f 100644 --- a/packages/common/src/styles/_variables.scss +++ b/packages/common/src/styles/_variables.scss @@ -389,6 +389,10 @@ $checkbox-selector-opacity: 0.15 !default; $checkbox-selector-opacity-hover: 0.35 !default; /* Editors */ +$editor-focus-border-color: lighten($primary-color, 10%); +$editor-focus-box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(lighten($primary-color, 3%), .6); +$date-editor-focus-border-color: $editor-focus-border-color; +$date-editor-focus-box-shadow: $editor-focus-box-shadow; $large-editor-background-color: #ffffff !default; $large-editor-border: 2px solid gray !default; $large-editor-text-padding: 5px !default; @@ -408,9 +412,13 @@ $text-editor-padding-bottom: 0 !default; $text-editor-padding-left: 2px !default; $text-editor-padding-right: 0 !default; $text-editor-padding-top: 0 !default; +$text-editor-focus-border-color: $editor-focus-border-color; +$text-editor-focus-box-shadow: $editor-focus-box-shadow; $slider-editor-height: 24px !default; $slider-editor-runnable-track-padding: 0 6px !default; $slider-editor-number-padding: 4px 6px !default; +$slider-editor-focus-border-color: $editor-focus-border-color; +$slider-editor-focus-box-shadow: $editor-focus-box-shadow; /* Compound Filters */ $compound-filter-bgcolor: #e4eacf !default; diff --git a/packages/common/src/styles/slick-default-theme.scss b/packages/common/src/styles/slick-default-theme.scss index 4ab8afe74..1d00e4810 100644 --- a/packages/common/src/styles/slick-default-theme.scss +++ b/packages/common/src/styles/slick-default-theme.scss @@ -92,6 +92,13 @@ outline: 0; transform: translate(0, -2px); } + + input.compound-editor-text { + width: calc(50% - 5px); + height: 100%; + outline: 0; + transform: translate(0, -2px); + } } } diff --git a/packages/common/src/styles/slick-editors.scss b/packages/common/src/styles/slick-editors.scss index fdf4289b8..a46fb7605 100644 --- a/packages/common/src/styles/slick-editors.scss +++ b/packages/common/src/styles/slick-editors.scss @@ -1,6 +1,7 @@ @import './variables'; .slick-cell.active { + input.compound-editor-text, input.editor-text { border: $text-editor-border; border-radius: $text-editor-border-radius; @@ -13,7 +14,33 @@ margin-bottom: $text-editor-margin-bottom; margin-right: $text-editor-margin-right; margin-top: $text-editor-margin-top; - } + + &:focus { + outline: 0; + border-color: $text-editor-focus-border-color; + box-shadow: $text-editor-focus-box-shadow; + } + + &.right { + margin-left: calc(#{$text-editor-margin-left + 9px}); + } + } + + .slider-editor-input { + &:focus { + outline: 0; + border-color: $slider-editor-focus-border-color; + box-shadow: $slider-editor-focus-box-shadow; + } + } + + .flatpickr-alt-input.editor-text { + &:focus { + outline: 0; + border-color: $date-editor-focus-border-color; + box-shadow: $date-editor-focus-box-shadow; + } + } } /* Long Text Editor */ diff --git a/packages/common/src/styles/slick-without-bootstrap-min-styling.scss b/packages/common/src/styles/slick-without-bootstrap-min-styling.scss index facca010d..d94c944ec 100644 --- a/packages/common/src/styles/slick-without-bootstrap-min-styling.scss +++ b/packages/common/src/styles/slick-without-bootstrap-min-styling.scss @@ -120,4 +120,11 @@ *, :after, :before { box-sizing: border-box; } + + .form-control:focus{ + // border-color: #66afe9; + border-color: lighten($primary-color, 10%); + outline: 0; + box-shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px rgba(lighten($primary-color, 3%), .6); + } } diff --git a/packages/common/src/styles/slickgrid-theme-salesforce.scss b/packages/common/src/styles/slickgrid-theme-salesforce.scss index c5a978da0..c81a758b5 100644 --- a/packages/common/src/styles/slickgrid-theme-salesforce.scss +++ b/packages/common/src/styles/slickgrid-theme-salesforce.scss @@ -92,6 +92,7 @@ $multiselect-icon-search: "\F0349"; $multiselect-unchecked-opacity: 0.8; $row-move-plugin-cursor: grab; $row-move-plugin-icon: "\F0278"; +$editor-focus-box-shadow: 0 0 3px $primary-color; $slider-editor-height: 26px; $row-selected-color: darken($cell-odd-background-color, 7%); $row-mouse-hover-color: rgba(128, 183, 231, 0.1); diff --git a/packages/web-demo-vanilla-bundle/src/app-routing.ts b/packages/web-demo-vanilla-bundle/src/app-routing.ts index 049035958..fdf8837df 100644 --- a/packages/web-demo-vanilla-bundle/src/app-routing.ts +++ b/packages/web-demo-vanilla-bundle/src/app-routing.ts @@ -12,7 +12,6 @@ export class AppRouting { { route: 'example06', name: 'example06', title: 'Example06', moduleId: './examples/example06' }, { route: 'example07', name: 'example07', title: 'Example07', moduleId: './examples/example07' }, { route: 'example50', name: 'example50', title: 'Example50', moduleId: './examples/example50' }, - { route: 'example51', name: 'example51', title: 'Example51', moduleId: './examples/example51' }, { route: '', redirect: 'example01' }, { route: '**', redirect: 'example01' } ]; diff --git a/packages/web-demo-vanilla-bundle/src/app.html b/packages/web-demo-vanilla-bundle/src/app.html index 180bd682c..8b8b40cbb 100644 --- a/packages/web-demo-vanilla-bundle/src/app.html +++ b/packages/web-demo-vanilla-bundle/src/app.html @@ -7,14 +7,14 @@

Slickgrid-Universal

- + Example07 - Row Move & Row Selections - + + Example50 - SE Tree Data diff --git a/packages/web-demo-vanilla-bundle/src/examples/example04.ts b/packages/web-demo-vanilla-bundle/src/examples/example04.ts index 4460ef803..81b31e35d 100644 --- a/packages/web-demo-vanilla-bundle/src/examples/example04.ts +++ b/packages/web-demo-vanilla-bundle/src/examples/example04.ts @@ -160,8 +160,8 @@ export class Example4 { placeholder: '🔍 search city', // We can use the autocomplete through 3 ways "collection", "collectionAsync" or with your own autocomplete options - // use your own autocomplete options, instead of $.ajax, use Aurelia HttpClient or FetchClient - // here we use $.ajax just because I'm not sure how to configure Aurelia HttpClient with JSONP and CORS + // use your own autocomplete options, instead of $.ajax, use HttpClient or FetchClient + // here we use $.ajax just because I'm not sure how to configure HttpClient with JSONP and CORS editorOptions: { minLength: 3, forceUserInput: true, @@ -186,8 +186,8 @@ export class Example4 { // We can use the autocomplete through 3 ways "collection", "collectionAsync" or with your own autocomplete options // collectionAsync: this.httpFetch.fetch(URL_COUNTRIES_COLLECTION), - // OR use your own autocomplete options, instead of $.ajax, use Aurelia HttpClient or FetchClient - // here we use $.ajax just because I'm not sure how to configure Aurelia HttpClient with JSONP and CORS + // OR use your own autocomplete options, instead of $.ajax, use HttpClient or FetchClient + // here we use $.ajax just because I'm not sure how to configure HttpClient with JSONP and CORS filterOptions: { minLength: 3, source: (request, response) => { diff --git a/packages/web-demo-vanilla-bundle/src/examples/example50.html b/packages/web-demo-vanilla-bundle/src/examples/example50.html new file mode 100644 index 000000000..4a0d25db5 --- /dev/null +++ b/packages/web-demo-vanilla-bundle/src/examples/example50.html @@ -0,0 +1,38 @@ +

Example 50 - SE Tree View

+
+
+ + + + +
+
+
+
+ +
+
+
+

+ +

+
+
+
+
+
+ +
+
+
+
diff --git a/packages/web-demo-vanilla-bundle/src/examples/example50.scss b/packages/web-demo-vanilla-bundle/src/examples/example50.scss new file mode 100644 index 000000000..69e600394 --- /dev/null +++ b/packages/web-demo-vanilla-bundle/src/examples/example50.scss @@ -0,0 +1,41 @@ +$link-color: #0099ff; + +.cell-effort-driven { + text-align: center; +} + +.editable-field { + background-color: #d5e8e9 !important; +} +.fake-hyperlink { + cursor: pointer; + color: $link-color; + &:hover { + text-decoration: underline; + } +} + +.toggle { + height: 24px; + width: 24px; + display: inline-block; + + &.expand { + cursor: pointer; + &:before { + font-family: "Material Design Icons"; + font-size: 24px; + content: "\F0142"; + } + } + + &.collapse{ + cursor: pointer; + &:before { + font-family: "Material Design Icons"; + font-size: 24px; + content: "\F0140"; + } + } +} + diff --git a/packages/web-demo-vanilla-bundle/src/examples/example50.ts b/packages/web-demo-vanilla-bundle/src/examples/example50.ts new file mode 100644 index 000000000..c3753eced --- /dev/null +++ b/packages/web-demo-vanilla-bundle/src/examples/example50.ts @@ -0,0 +1,618 @@ +import { Column, GridOption, FormatterResultObject, OnEventArgs, SortDirectionString, Formatters } from '@slickgrid-universal/common'; +import { Slicker } from '@slickgrid-universal/vanilla-bundle'; +import '../se-styles.scss'; +import './example50.scss'; + +const ID_PROPERTY_NAME = 'Id'; + +export class Example50 { + _commandQueue = []; + columnDefinitions: Column[]; + gridOptions: GridOption; + dataset: any[]; + dataViewObj: any; + gridObj: any; + slickgridLwc; + slickerGridInstance; + durationOrderByCount = false; + searchString = ''; + sortDirection: SortDirectionString = 'ASC'; + sortSequenceBeforeEdit: number; + + attached() { + this.initializeGrid(); + this.dataset = []; + const gridContainerElm = document.querySelector(`.grid50`); + + gridContainerElm.addEventListener('onclick', this.handleOnClick.bind(this)); + gridContainerElm.addEventListener('oncellchange', this.handleOnCellChange.bind(this)); + gridContainerElm.addEventListener('onvalidationerror', this.handleValidationError.bind(this)); + gridContainerElm.addEventListener('onslickergridcreated', this.handleOnSlickerGridCreated.bind(this)); + gridContainerElm.addEventListener('onbeforeeditcell', this.verifyCellIsEditableBeforeEditing.bind(this)); + this.slickgridLwc = new Slicker.GridBundle(gridContainerElm, this.columnDefinitions, this.gridOptions, []); + this.dataViewObj = this.slickgridLwc.dataView; + this.slickgridLwc.datasetHierarchical = require('c://TEMP/quote1.json') || []; // work data only + } + + initializeGrid() { + this.columnDefinitions = [ + { + id: 'Auth_Sell_Ext_Price__c22', name: 'Auth Ext Sell Price | Multiplier', field: 'Auth_Sell_Ext_Price__c', minWidth: 150, filterable: true, + editor: { + model: Slicker.Editors.compoundText, + params: { + decimalPlaces: 2, + leftField: 'Auth_Sell_Ext_Price__c', + rightField: 'Authorized_Selling_Net_Multiplier__c', + leftValidator: (value, args) => true, + rightValidator: (value, args) => true, + }, + required: true, + alwaysSaveOnEnterKey: true, + }, + formatter: this.authSellFormatter.bind(this) + }, + { + id: 'Sort_Sequence_Number__c', name: 'Sort Seq', field: 'Sort_Sequence_Number__c', minWidth: 90, + formatter: Slicker.Formatters.multiple, sortable: true, filterable: true, filter: { model: Slicker.Filters.compoundInputNumber }, type: Slicker.Enums.FieldType.number, + params: { + formatters: [this.sortSequenceFormatter, this.customEditableInputFormatter.bind(this)] + }, + editor: { + model: Slicker.Editors.text, + required: true, + alwaysSaveOnEnterKey: true, + }, + onCellChange: (e: Event, args: OnEventArgs) => { + if (args && args.columnDef && args.dataContext) { + const item = args.dataContext; + const grid = args.grid; + const dataView = grid && grid.getData(); + const items = dataView.getItems(); + const treeLevelPropName = '__treeLevel'; + const targetedSortSequenceNumber = item.Sort_Sequence_Number__c; + const targetRowItem = items.find((searchItem) => searchItem[treeLevelPropName] === item[treeLevelPropName] && searchItem.Sort_Sequence_Number__c === targetedSortSequenceNumber && searchItem.Id !== item.Id); + if (targetRowItem) { + targetRowItem['Sort_Sequence_Number__c'] = this.sortSequenceBeforeEdit; + dataView.updateItem(targetRowItem[ID_PROPERTY_NAME], targetRowItem); + this.slickgridLwc.sortService.updateSorting([{ columnId: 'Sort_Sequence_Number__c', direction: 'ASC' }]); + } + } + } + }, + { id: 'Translation_Underway__c', name: 'ACE', field: 'Translation_Underway__c', minWidth: 90, formatter: this.aceColumnFormatter, filterable: true, }, + { id: 'Line_Type__c', name: 'Edit', field: 'Line_Type__c', minWidth: 110, formatter: this.editColumnFormatter, filterable: true, }, + { id: 'Translation_Request_Type__c', name: 'Drawings', field: 'Translation_Request_Type__c', minWidth: 110, formatter: this.translationTypeFormatter, filterable: true, }, + { + id: 'Quantity__c', name: 'Qty', field: 'Quantity__c', minWidth: 90, filterable: true, + editor: { model: Slicker.Editors.integer, }, formatter: this.customEditableInputFormatter.bind(this) + }, + { id: 'Line_Item_Number__c', name: 'Item Num.', field: 'Line_Item_Number__c', minWidth: 150, filterable: true, formatter: this.fakeHyperlinkFormatter }, + { + id: 'Product', name: 'Product', field: 'Product', cssClass: 'cell-title', sortable: true, minWidth: 250, width: 300, filterable: true, + queryFieldNameGetterFn: (dataContext) => dataContext.Engineered_Product_Name__c ? 'Engineered_Product_Name__c' : 'Product_Name__r.Name', + formatter: Formatters.tree, + }, + { id: 'ERF_Product_Description__c', name: 'Description', field: 'ERF_Product_Description__c', minWidth: 150, filterable: true }, + { + id: 'Designation__c', name: 'Designation', field: 'Designation__c', minWidth: 150, filterable: true, + editor: { model: Slicker.Editors.longText, }, formatter: this.customEditableInputFormatter.bind(this), + }, + { id: 'Price_Determined_Category_Number__c', name: 'PD Cat', field: 'Price_Determined_Category_Number__c', valueCouldBeUndefined: true, minWidth: 150, sortable: true, filterable: true }, + { id: 'Line_Code__c', name: 'Line Code', field: 'Line_Code__c', minWidth: 150, sortable: true, filterable: true }, + { id: 'priceStatus', name: 'Price Status', field: 'priceStatus', minWidth: 150, filterable: true }, + { + id: 'Unit_List_Price__c', name: 'Unit List Price', field: 'Unit_List_Price__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + { + id: 'Extended_list_Price__c', name: 'Ext. List Price', field: 'Extended_list_Price__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + { + id: 'Purchaser_Profile_Multiplier__c', name: 'Book Mult.', field: 'Purchaser_Profile_Multiplier__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.decimal, params: { minDecimal: 4, maxDecimal: 4, } + }, + { + id: 'Normal_Net_Extended_Price_Formula__c', name: 'Ext. Book Price', field: 'Normal_Net_Extended_Price_Formula__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + // { + // id: 'Recommended_Fix__c', name: 'System Fix', field: 'Recommended_Fix__c', minWidth: 150, + // filterable: true, filter: { + // model: Slicker.Filters.singleSelect, + // collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], + // }, + // formatter: Slicker.Formatters.checkmarkMaterial, + // }, + { + id: 'Authorized_Selling_Net_Multiplier__c', name: 'Auth Sell Net Mult', field: 'Authorized_Selling_Net_Multiplier__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.decimal, params: { minDecimal: 4, maxDecimal: 4, } + }, + { + id: 'Auth_Sell_Ext_Price__c', name: 'Auth Ext Sell Price', field: 'Auth_Sell_Ext_Price__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + // { + // id: 'Auth_Sell_Ext_Price__c22', name: 'Auth Ext Sell Price | Multiplier', field: 'Auth_Sell_Ext_Price__c', minWidth: 150, filterable: true, + // editor: { + // model: Slicker.Editors.compoundText, + // params: { decimalPlaces: 2 }, + // required: true, + // alwaysSaveOnEnterKey: true, + // }, + // formatter: this.authSellFormatter.bind(this) + // }, + { + id: 'Requested_Sell_Net_Multiplier__c', name: 'Req Sell Net Mult.', field: 'Requested_Sell_Net_Multiplier__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.decimal, params: { minDecimal: 4, maxDecimal: 4, } + }, + { + id: 'reqUnitSellNetPrice', name: 'Req Unit Sell Net Price', field: 'reqUnitSellNetPrice', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + { + id: 'Requested_Extended_Selling_Net_price__c', name: 'Req Extended Price', field: 'Requested_Extended_Selling_Net_price__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + { + id: 'Recommended_Fix__c', name: 'Req Fix', field: 'Recommended_Fix__c', minWidth: 150, + filterable: true, filter: { + model: Slicker.Filters.singleSelect, + collection: [{ value: '', label: '' }, { value: true, label: 'True' }, { value: false, label: 'False' }], + }, + formatter: Slicker.Formatters.checkmarkMaterial, + }, + { + id: 'FOB_Amount__c', name: 'Fix Req Price', field: 'FOB_Amount__c', minWidth: 150, filterable: true, + formatter: Slicker.Formatters.dollar, + }, + { id: 'Lead_Time__c', name: 'Lead Time', field: 'Lead_Time__c', minWidth: 150, filterable: true }, + { id: 'Shipping_location__c', name: 'Shipping Location', field: 'Shipping_location__c', minWidth: 150, filterable: true }, + { id: 'ERF_Error_Message__c', name: 'Error Msg', field: 'ERF_Error_Message__c', minWidth: 150, filterable: true }, + ]; + + this.gridOptions = { + datasetIdPropertyName: ID_PROPERTY_NAME, + autoEdit: true, // true single click (false for double-click) + autoCommitEdit: true, + editable: true, + autoResize: { + container: '.demo-container', + }, + contextMenu: { + hideCopyCellValueCommand: true + }, + enableAutoSizeColumns: true, + enableAutoResize: true, + enableCellNavigation: true, + enableCheckboxSelector: true, + enableFiltering: true, + // showHeaderRow: false, + gridMenu: { + hideToggleFilterCommand: true + }, + multiColumnSort: false, + enableRowSelection: true, + enableTreeData: true, + treeDataOptions: { + columnId: 'Product', + parentPropName: 'ACEWeb_Selector__c', + childrenPropName: 'Quote_Line_Items__r', + initialSort: { + columnId: 'Sort_Sequence_Number__c', + direction: 'ASC' + } + }, + rowSelectionOptions: { + // True (Single Selection), False (Multiple Selections) + selectActiveRow: false + }, + dataView: { + syncGridSelection: true, // enable this flag so that the row selection follows the row even if we move it to another position + }, + checkboxSelector: { + hideSelectAllCheckbox: false, // hide the "Select All" from title bar + columnIndexPosition: 2, + selectableOverride: (row: number, dataContext: any) => dataContext['__treeLevel'] === 0 + }, + enableRowMoveManager: true, + rowMoveManager: { + // when using Row Move + Row Selection, you want to enable the following 2 flags so it doesn't cancel row selection + width: 50, + singleRowMove: true, + disableRowSelection: true, + cancelEditOnDrag: true, + usabilityOverride: (row, dataContext, grid) => this.canRowBeMoved(row, dataContext, grid), + onBeforeMoveRows: (e: Event, args: any) => this.onBeforeMoveRow(e, args), + onMoveRows: (e: Event, args: any) => this.onMoveRows(e, args), + }, + formatterOptions: { + minDecimal: 0, + maxDecimal: 2, + thousandSeparator: ',' + }, + // enableSorting: true, + headerRowHeight: 40, + rowHeight: 40, + editCommandHandler: (item, column, editCommand) => { + this._commandQueue.push(editCommand); + editCommand.execute(); + }, + }; + } + + canRowBeMoved(row, dataContext, grid) { + // move icon should only be usable & displayed on root level OR when item has children + const dataView = grid && grid.getData(); + const identifierPropName = dataView.getIdPropertyName() || 'id'; + const treeLevelPropName = '__treeLevel'; + const idx = dataView.getIdxById(dataContext[identifierPropName]); + const nextItemRow = dataView.getItemByIdx(idx + 1); + if (dataContext[treeLevelPropName] === 0 || nextItemRow && nextItemRow[treeLevelPropName] > dataContext[treeLevelPropName]) { + return true; + } + return false; + } + + dispose() { + this.slickgridLwc.dispose(); + } + + searchItem(event: KeyboardEvent) { + this.searchString = (event.target as HTMLInputElement).value; + this.dataViewObj.refresh(); + } + + authSellFormatter(row, cell, value, columnDef, dataContext) { + //Auth_Sell_Ext_Price__c, Requested_Sell_Net_Multiplier__c + let authSellPrice = ''; + if (dataContext.Auth_Sell_Ext_Price__c !== undefined) { + authSellPrice = Slicker.Utilities.formatNumber(dataContext.Auth_Sell_Ext_Price__c, 0, 2, false, '$', '', '.', ','); + } + let authSellMulti = ''; + if (dataContext.Authorized_Selling_Net_Multiplier__c !== undefined) { + authSellMulti = Slicker.Utilities.formatNumber(dataContext.Authorized_Selling_Net_Multiplier__c, 4, 4, false, '', '', '.', ','); + } + return `${authSellPrice} | ${authSellMulti}`; + } + + aceColumnFormatter(row, cell, value, columnDef, dataContext) { + let output = ''; + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + const treeLevel = dataContext[treeLevelPropName]; + const hasAceChecked = dataContext.Engineering_Status__c && dataContext.Engineering_Status__c === 'Processed'; + + if (treeLevel === 0 && hasAceChecked) { + output = ``; + } + return output; + } + + editColumnFormatter(row, cell, value, columnDef, dataContext) { + let output = ''; + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + const __treeLevel = dataContext[treeLevelPropName]; + + if (__treeLevel === 0) { + switch (value) { + case 'Profiled': + output = ``; + break; + case 'Selector': + output = ``; + break; + default: + output = ''; + break; + } + } + return output; + } + + fakeHyperlinkFormatter(row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) { + return value ? `${value}` : ''; + } + + // This Formatter is used in combo with the "usabilityOverride" defined in the RowMoveManager creation + moveIconFormatter(row, cell, value, columnDef, dataContext) { + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + if (dataContext[treeLevelPropName] === 0) { + return { addClasses: 'cell-reorder', text: '' } as FormatterResultObject; + } + return ''; + } + + sortSequenceFormatter(row, cell, value, columnDef, dataContext) { + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + return dataContext[treeLevelPropName] === 0 ? value : ''; + } + + translationTypeFormatter(row, cell, value, columnDef, dataContext) { + let output = ''; + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + + if (treeLevelPropName === 0) { + switch (value) { + case 'Drawing': + output = ``; + break; + default: + output = ''; + break; + } + } + return output; + } + + productTreeFormatter(row, cell, value, columnDef, dataContext, grid) { + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + if (dataContext === undefined) { + return ''; + } + + const dataView = grid.getData(); + const identifierPropName = dataView.getIdPropertyName() || 'id'; + const idx = dataView.getIdxById(dataContext[identifierPropName]); + const spacer = ``; + + if (dataView && dataView.getIdxById && dataView.getItemByIdx) { + const nextItemRow = dataView.getItemByIdx(idx + 1); + const productName = dataContext.Engineered_Product_Name__c || (dataContext.Product_Name__r && dataContext.Product_Name__r.Name) || ''; + + if (nextItemRow && nextItemRow[treeLevelPropName] > dataContext[treeLevelPropName]) { + if (dataContext.__collapsed) { + return `${spacer} ${productName}`; + } else { + return `${spacer} ${productName}`; + } + } + return `${spacer} ${productName}`; + } + return ''; + } + + myFilter(item) { + if (this.searchString !== '' && item['Line_Item_Number__c'].indexOf(this.searchString) === -1) { + return false; + } + if (item.ACEWeb_Selector__c !== null) { + let parent = this.dataset.find(itm => itm[ID_PROPERTY_NAME] === item.ACEWeb_Selector__c); + while (parent) { + if (parent.__collapsed || (this.searchString !== '' && parent['Line_Item_Number__c'].indexOf(this.searchString) === -1)) { + return false; + } + const ACEWeb_Selector__c = parent.ACEWeb_Selector__c !== null ? parent.ACEWeb_Selector__c : null; + parent = this.dataset.find(itm2 => itm2[ID_PROPERTY_NAME] === ACEWeb_Selector__c); + } + } + return true; + } + + customEditableInputFormatter = (row: number, cell: number, value: any, columnDef: Column, dataContext: any, grid: any) => { + const isEditableLine = this.isItemEditable(dataContext, columnDef); + value = (value === null || value === undefined) ? '' : value; + + return isEditableLine ? { text: value, addClasses: 'editable-field', toolTip: 'Click to Edit' } : value; + } + + onCellChange(args: OnEventArgs) { + if (args && args.columnDef && args.dataContext) { + const field = args.columnDef.field; + const item = args.dataContext; + const lastEdit = this._commandQueue.pop(); + const oldValue = lastEdit && lastEdit.prevSerializedValue; + const newValue = item[field]; + const alwaysSaveOnEnterKey = args.columnDef.internalColumnEditor && args.columnDef.internalColumnEditor.alwaysSaveOnEnterKey || false; + + if (alwaysSaveOnEnterKey || oldValue !== newValue) { + this.updateLineItem(item, { fieldName: field, fieldValue: item[field] }); + } + } + } + + updateLineItem(item: any, fieldUpdate: { fieldName: string; fieldValue: string; id?: string; }) { + console.log('item update:', item, fieldUpdate); + } + + onBeforeMoveRow(e, data) { + for (let i = 0; i < data.rows.length; i++) { + // no point in moving before or after itself + if (data.rows[i] === data.insertBefore || data.rows[i] === data.insertBefore - 1) { + e.stopPropagation(); + return false; + } + } + return true; + } + + onMoveRows(e, args) { + const extractedRows = []; + const grid = args && args.grid; + const rows = args && args.rows || []; + const dataView = grid.getData(); + const items = dataView.getItems(); + + if (grid && Array.isArray(rows) && rows.length > 0) { + const insertBefore = args.insertBefore; + const left = items.slice(0, insertBefore); + const right = items.slice(insertBefore, items.length); + + // find the clicked row and the drop target row + // then switch their Sort Sequence + const targetIndex = ((args.insertBefore - 1) >= 0) ? (args.insertBefore - 1) : 0; + const clickedRowItem = grid.getDataItem(rows[0]); + const targetRowItem = grid.getDataItem(targetIndex); + + // interchange sort sequence property of the clicked row to the target row + this.flipItemsSortSequences(clickedRowItem, targetRowItem); + + rows.sort((a, b) => a - b); // sort the rows + + for (let i = 0; i < rows.length; i++) { + extractedRows.push(items[rows[i]]); + } + + rows.reverse(); + + for (let i = 0; i < rows.length; i++) { + const row = rows[i]; + if (row < insertBefore) { + left.splice(row, 1); + } else { + right.splice(row - insertBefore, 1); + } + } + const updatedDataset = left.concat(extractedRows.concat(right)); + this.slickgridLwc.dataset = updatedDataset; + this.slickgridLwc.sortService.updateSorting([{ columnId: 'Sort_Sequence_Number__c', direction: 'ASC' }]); + } + } + + /** + * find the clicked row and the drop target row + * then switch their Sort Sequence + */ + flipItemsSortSequences(clickedRowItem, targetRowItem) { + if (clickedRowItem && targetRowItem) { + const clickedItemSeqNumber = clickedRowItem['Sort_Sequence_Number__c']; + const targetItemSeqNumber = targetRowItem['Sort_Sequence_Number__c']; + clickedRowItem['Sort_Sequence_Number__c'] = targetItemSeqNumber; + targetRowItem['Sort_Sequence_Number__c'] = clickedItemSeqNumber; + } else { + throw new Error('[Slickgrid-Universal] could not find clicked row item'); + } + } + + collapseAll() { + const items = this.dataViewObj.getItems(); + if (Array.isArray(items)) { + items.forEach((item) => item.__collapsed = true); + this.slickgridLwc.dataset = items; + if (this.gridObj) { + this.gridObj.invalidate(); + } + } + } + + expandAll() { + const items = this.dataViewObj.getItems(); + if (Array.isArray(items)) { + items.forEach((item) => item.__collapsed = false); + this.slickgridLwc.dataset = items; + if (this.gridObj) { + this.gridObj.invalidate(); + } + } + } + + handleOnClick(event: any) { + const eventDetail = event && event.detail; + const args = event && event.detail && event.detail.args; + if (eventDetail && args) { + const grid = args.grid; + const dataView = grid.getData(); + const columnDef = grid && grid.getColumns()[args.cell]; + const field = columnDef && columnDef.field || ''; + const cell = this.gridObj.getCellFromEvent(eventDetail.eventData); + const currentRow = cell && cell.row; + const dataContext = this.gridObj.getDataItem(currentRow); + const treeLevelPropName = columnDef.treeData?.levelPropName || '__treeLevel'; + + switch (field) { + case 'Line_Type__c': + if (dataContext[treeLevelPropName] === 0) { + if (dataContext['Line_Type__c'] === 'Profiled') { + alert('call update line modal window'); + } else if (dataContext['Line_Type__c'] === 'Selector') { + alert('selector'); + } + } + break; + case 'Translation_Request_Type__c': + console.log('translation'); + break; + case 'Product': + case 'Product_Name': + if (eventDetail && args) { + const targetElm = eventDetail.eventData.target || {}; + const hasToggleClass = targetElm.className.indexOf('toggle') >= 0 || false; + if (hasToggleClass) { + const item = dataView.getItem(args.row); + if (item) { + item.__collapsed = !item.__collapsed ? true : false; + dataView.updateItem(item[ID_PROPERTY_NAME], item); + grid.invalidate(); + } + event.stopImmediatePropagation(); + } + } + break; + } + } + } + + handleOnCellChange(event) { + const item = event.detail && event.detail.args && event.detail.args.item || {}; + // console.log(item) + } + + handleValidationError(event) { + console.log('handleValidationError', event.detail); + const args = event.detail && event.detail.args; + if (args.validationResults) { + alert(args.validationResults.msg); + } + } + + handleOnSlickerGridCreated(event) { + this.slickerGridInstance = event && event.detail; + this.gridObj = this.slickerGridInstance && this.slickerGridInstance.slickGrid; + this.dataViewObj = this.slickerGridInstance && this.slickerGridInstance.dataView; + } + + logExpandedStructure() { + console.log('exploded array', this.slickgridLwc.datasetHierarchical /* , JSON.stringify(explodedArray, null, 2) */); + } + + logFlatStructure() { + console.log('flat array', this.dataViewObj.getItems() /* , JSON.stringify(outputFlatArray, null, 2) */); + } + + isItemEditable(dataContext: any, columnDef: Column): boolean { + const treeLevelPropName = '__treeLevel'; + if (!dataContext || dataContext[treeLevelPropName] > 0) { + return false; + } + let isEditable = false; + switch (columnDef.id) { + case 'Sort_Sequence_Number__c': + isEditable = true; + break; + case 'Quantity__c': + isEditable = dataContext['Line_Type__c'] === 'Profiled' ? true : false; + break; + case 'Designation__c': + isEditable = true; + break; + case 'Auth_Sell_Ext_Price__c22': + isEditable = true; + break; + } + return isEditable; + } + + verifyCellIsEditableBeforeEditing(event) { + const eventData = event?.detail?.eventData; + const args = event?.detail?.args; + + if (args && args.column && args.item) { + this.sortSequenceBeforeEdit = args?.item?.Sort_Sequence_Number__c || -1; + if (!this.isItemEditable(args.item, args.column)) { + event.preventDefault(); + eventData.stopImmediatePropagation(); + return false; + } + } + } +} diff --git a/packages/web-demo-vanilla-bundle/src/se-styles.scss b/packages/web-demo-vanilla-bundle/src/se-styles.scss index 7642b32f3..dc81c8684 100644 --- a/packages/web-demo-vanilla-bundle/src/se-styles.scss +++ b/packages/web-demo-vanilla-bundle/src/se-styles.scss @@ -1,2 +1,3 @@ /* make sure to add the @import the SlickGrid Bootstrap Theme AFTER the variables changes */ -@import '@slickgrid-universal/common/dist/styles/sass/se-slickgrid-theme-material.scss'; +// @import '@slickgrid-universal/common/dist/styles/sass/se-slickgrid-theme-material.scss'; +@import '@slickgrid-universal/common/dist/styles/sass/slickgrid-theme-salesforce.scss'; diff --git a/packages/web-demo-vanilla-bundle/webpack.config.js b/packages/web-demo-vanilla-bundle/webpack.config.js index ae1c511f6..1edfb213f 100644 --- a/packages/web-demo-vanilla-bundle/webpack.config.js +++ b/packages/web-demo-vanilla-bundle/webpack.config.js @@ -17,7 +17,7 @@ const outDirProd = path.resolve(__dirname, '../../docs'); const srcDir = path.resolve(__dirname, 'src'); const nodeModulesDir = path.resolve(__dirname, 'node_modules'); const platform = { - hmr: true, + hmr: false, open: true, port: 8088, host: 'localhost',