slickgrid
Version:
A lightning fast JavaScript grid/spreadsheet
922 lines (774 loc) • 30.6 kB
text/typescript
import type { Column, Editor, EditorArguments, EditorValidationResult, ElementPosition, GridOption, OnCompositeEditorChangeEventArgs } from './models/index.js';
import { keyCode as keyCode_, Utils as Utils_ } from './slick.core.js';
// for (iife) load Slick methods from global Slick object, or use imports for (esm)
const keyCode = IIFE_ONLY ? Slick.keyCode : keyCode_;
const Utils = IIFE_ONLY ? Slick.Utils : Utils_;
/***
* Contains basic SlickGrid editors.
* @module Editors
* @namespace Slick
*/
export class TextEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: number | string;
protected navOnLR?: boolean;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
this.navOnLR = this.args.grid.getOptions().editorCellNavOnLRKeys;
this.input = Utils.createDomElement('input', { type: 'text', className: 'editor-text' }, this.args.container);
this.input.addEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.focus();
this.input.select();
// don't show Save/Cancel when it's a Composite Editor and also trigger a onCompositeEditorChange event when input changes
if (this.args.compositeEditorOptions) {
this.input.addEventListener('change', this.onChange.bind(this));
}
}
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
}
destroy() {
this.input.removeEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.removeEventListener('change', this.onChange.bind(this));
this.input.remove();
}
focus() {
this.input.focus();
}
getValue() {
return this.input.value;
}
setValue(val: string) {
this.input.value = val;
}
loadValue(item: any) {
this.defaultValue = item[this.args.column.field] || '';
this.input.value = String(this.defaultValue ?? '');
this.input.defaultValue = String(this.defaultValue ?? '');
this.input.select();
}
serializeValue() {
return this.input.value;
}
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
}
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && (this.input.value !== this.defaultValue);
}
validate() {
if (this.args.column.validator) {
const validationResults = this.args.column.validator(this.input.value, this.args);
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
}
}
export class IntegerEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: string | number;
protected navOnLR?: boolean;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
this.navOnLR = this.args.grid.getOptions().editorCellNavOnLRKeys;
this.input = Utils.createDomElement('input', { type: 'text', className: 'editor-text' }, this.args.container);
this.input.addEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.focus();
this.input.select();
// trigger onCompositeEditorChange event when input changes and it's a Composite Editor
if (this.args.compositeEditorOptions) {
this.input.addEventListener('change', this.onChange.bind(this));
}
}
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
}
destroy() {
this.input.removeEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.removeEventListener('change', this.onChange.bind(this));
this.input.remove();
}
focus() {
this.input.focus();
}
loadValue(item: any) {
this.defaultValue = item[this.args.column.field];
this.input.value = String(this.defaultValue ?? '');
this.input.defaultValue = String(this.defaultValue ?? '');
this.input.select();
}
serializeValue() {
return parseInt(this.input.value, 10) || 0;
}
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
}
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && (this.input.value !== this.defaultValue);
}
validate() {
if (isNaN(this.input.value as unknown as number)) {
return {
valid: false,
msg: 'Please enter a valid integer'
};
}
if (this.args.column.validator) {
const validationResults = this.args.column.validator(this.input.value, this.args);
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
}
}
export class FloatEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: string | number;
protected navOnLR?: boolean;
/** Default number of decimal places to use with FloatEditor */
static DefaultDecimalPlaces?: number = undefined;
/** Should we allow empty value when using FloatEditor */
static AllowEmptyValue = false;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
this.navOnLR = this.args.grid.getOptions().editorCellNavOnLRKeys;
this.input = Utils.createDomElement('input', { type: 'text', className: 'editor-text' }, this.args.container);
this.input.addEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.focus();
this.input.select();
// trigger onCompositeEditorChange event when input changes and it's a Composite Editor
if (this.args.compositeEditorOptions) {
this.input.addEventListener('change', this.onChange.bind(this));
}
};
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
};
destroy() {
this.input.removeEventListener('keydown', (this.navOnLR ? handleKeydownLRNav : handleKeydownLRNoNav) as EventListener);
this.input.removeEventListener('change', this.onChange.bind(this));
this.input.remove();
};
focus() {
this.input.focus();
}
getDecimalPlaces() {
// returns the number of fixed decimal places or null
let rtn: number | undefined = this.args.column.editorFixedDecimalPlaces;
if (!Utils.isDefined(rtn)) {
rtn = FloatEditor.DefaultDecimalPlaces;
}
return (!rtn && rtn !== 0 ? null : rtn);
}
loadValue(item: any) {
this.defaultValue = item[this.args.column.field];
const decPlaces = this.getDecimalPlaces();
if (decPlaces !== null
&& (this.defaultValue || this.defaultValue === 0)
&& (this.defaultValue as number)?.toFixed) {
this.defaultValue = (this.defaultValue as number).toFixed(decPlaces);
}
this.input.value = String(this.defaultValue ?? '');
this.input.defaultValue = String(this.defaultValue ?? '');
this.input.select();
}
serializeValue() {
let rtn: number | undefined = parseFloat(this.input.value);
if (FloatEditor.AllowEmptyValue) {
if (!rtn && rtn !== 0) {
rtn = undefined;
}
} else {
rtn = rtn || 0;
}
const decPlaces = this.getDecimalPlaces();
if (decPlaces !== null
&& (rtn || rtn === 0)
&& rtn.toFixed) {
rtn = parseFloat(rtn.toFixed(decPlaces));
}
return rtn as number;
}
applyValue(item: any, state: number | string) {
item[this.args.column.field] = state;
}
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && (this.input.value !== this.defaultValue);
}
validate() {
if (isNaN(this.input.value as unknown as number)) {
return {
valid: false,
msg: 'Please enter a valid number'
};
}
if (this.args.column.validator) {
const validationResults = this.args.column.validator(this.input.value, this.args);
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
}
}
export class FlatpickrEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: string | number;
protected flatpickrInstance: any;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
if (typeof flatpickr === 'undefined') {
throw new Error('Flatpickr not loaded but required in SlickGrid.Editors, refer to Flatpickr documentation: https://flatpickr.js.org/getting-started/');
}
}
init() {
this.input = Utils.createDomElement('input', { type: 'text', className: 'editor-text' }, this.args.container);
this.input.focus();
this.input.select();
const editorOptions = this.args.column.params?.editorOptions; // i.e.: { id: 'start', params: { editorOptions: {altFormat: 'd/m/Y', dateFormat: 'd/m/Y'}} }
this.flatpickrInstance = flatpickr(this.input, {
closeOnSelect: true,
allowInput: true,
altInput: true,
altFormat: editorOptions?.altFormat ?? 'm/d/Y',
dateFormat: editorOptions?.dateFormat ?? 'm/d/Y',
onChange: () => {
// trigger onCompositeEditorChange event when input changes and it's a Composite Editor
if (this.args.compositeEditorOptions) {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
}
},
});
if (!this.args.compositeEditorOptions) {
window.setTimeout(() => {
this.show();
this.focus();
}, 50);
}
Utils.width(this.input, (Utils.width(this.input) as number) - (!this.args.compositeEditorOptions ? 18 : 28));
}
destroy() {
this.hide();
if (this.flatpickrInstance) {
this.flatpickrInstance.destroy();
}
this.input.remove();
}
show() {
if (!this.args.compositeEditorOptions && this.flatpickrInstance) {
this.flatpickrInstance.open();
}
}
hide() {
if (!this.args.compositeEditorOptions && this.flatpickrInstance) {
this.flatpickrInstance.close();
}
}
focus() {
this.input.focus();
}
loadValue(item: any) {
this.defaultValue = item[this.args.column.field];
this.input.value = String(this.defaultValue ?? '');
this.input.defaultValue = String(this.defaultValue ?? '');
this.input.select();
if (this.flatpickrInstance) {
this.flatpickrInstance.setDate(this.defaultValue);
}
}
serializeValue() {
return this.input.value;
}
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
}
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && (this.input.value !== this.defaultValue);
}
validate() {
if (this.args.column.validator) {
const validationResults = this.args.column.validator(this.input.value, this.args);
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
}
}
export class YesNoSelectEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected select!: HTMLSelectElement;
protected defaultValue?: string | number;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
this.select = Utils.createDomElement('select', { tabIndex: 0, className: 'editor-yesno' }, this.args.container);
Utils.createDomElement('option', { value: 'yes', textContent: 'Yes' }, this.select);
Utils.createDomElement('option', { value: 'no', textContent: 'No' }, this.select);
this.select.focus();
// trigger onCompositeEditorChange event when input changes and it's a Composite Editor
if (this.args.compositeEditorOptions) {
this.select.addEventListener('change', this.onChange.bind(this));
}
}
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
}
destroy() {
this.select.removeEventListener('change', this.onChange.bind(this));
this.select.remove();
}
focus() {
this.select.focus();
}
loadValue(item: any) {
this.select.value = ((this.defaultValue = item[this.args.column.field]) ? 'yes' : 'no');
}
serializeValue() {
return this.select.value === 'yes';
}
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
}
isValueChanged() {
return this.select.value !== this.defaultValue;
}
validate() {
return {
valid: true,
msg: null
};
}
}
export class CheckboxEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: boolean;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
this.input = Utils.createDomElement('input', { className: 'editor-checkbox', type: 'checkbox', value: 'true' }, this.args.container);
this.input.focus();
// trigger onCompositeEditorChange event when input checkbox changes and it's a Composite Editor
if (this.args.compositeEditorOptions) {
this.input.addEventListener('change', this.onChange.bind(this));
}
};
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
};
destroy() {
this.input.removeEventListener('change', this.onChange.bind(this));
this.input.remove();
};
focus() {
this.input.focus();
};
loadValue(item: any) {
this.defaultValue = !!(item[this.args.column.field]);
if (this.defaultValue) {
this.input.checked = true;
} else {
this.input.checked = false;
}
};
preClick() {
this.input.checked = !this.input.checked;
}
serializeValue() {
return this.input.checked;
};
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
}
isValueChanged() {
return (this.serializeValue() !== this.defaultValue);
}
validate(): EditorValidationResult {
return {
valid: true,
msg: null
};
}
}
export class PercentCompleteEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLInputElement;
protected defaultValue?: number;
protected picker!: HTMLDivElement;
protected slider!: HTMLInputElement | null;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
sliderInputHandler(e: MouseEvent & { target: HTMLButtonElement }) {
this.input.value = e.target.value;
}
sliderChangeHandler() {
// trigger onCompositeEditorChange event when slider stops and it's a Composite Editor
if (this.args.compositeEditorOptions) {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
}
}
init() {
this.input = Utils.createDomElement('input', { className: 'editor-percentcomplete', type: 'text' }, this.args.container);
Utils.width(this.input, this.args.container.clientWidth - 25);
this.picker = Utils.createDomElement('div', { className: 'editor-percentcomplete-picker' }, this.args.container);
Utils.createDomElement('span', { className: 'editor-percentcomplete-picker-icon' }, this.picker);
const containerHelper = Utils.createDomElement('div', { className: 'editor-percentcomplete-helper' }, this.picker);
const containerWrapper = Utils.createDomElement('div', { className: 'editor-percentcomplete-wrapper' }, containerHelper);
Utils.createDomElement('div', { className: 'editor-percentcomplete-slider' }, containerWrapper);
this.slider = Utils.createDomElement('input', { className: 'editor-percentcomplete-slider', type: 'range', value: String(this.defaultValue ?? '') }, containerWrapper);
const containerButtons = Utils.createDomElement('div', { className: 'editor-percentcomplete-buttons' }, containerWrapper);
Utils.createDomElement('button', { value: '0', className: 'slick-btn slick-btn-default', textContent: 'Not started' }, containerButtons);
containerButtons.appendChild(document.createElement('br'));
Utils.createDomElement('button', { value: '50', className: 'slick-btn slick-btn-default', textContent: 'In Progress' }, containerButtons);
containerButtons.appendChild(document.createElement('br'));
Utils.createDomElement('button', { value: '100', className: 'slick-btn slick-btn-default', textContent: 'Complete' }, containerButtons);
this.input.focus();
this.input.select();
this.slider.addEventListener('input', this.sliderInputHandler.bind(this) as EventListener);
this.slider.addEventListener('change', this.sliderChangeHandler.bind(this));
const buttons = this.picker.querySelectorAll('.editor-percentcomplete-buttons button');
[].forEach.call(buttons, (button: HTMLButtonElement) => {
button.addEventListener('click', this.onClick.bind(this) as EventListener);
});
};
onClick(e: MouseEvent & { target: HTMLButtonElement }) {
this.input.value = String(e.target.value ?? '');
this.slider!.value = String(e.target.value ?? '');
};
destroy() {
this.slider?.removeEventListener('input', this.sliderInputHandler.bind(this) as EventListener);
this.slider?.removeEventListener('change', this.sliderChangeHandler.bind(this));
this.picker.querySelectorAll('.editor-percentcomplete-buttons button')
.forEach(button => button.removeEventListener('click', this.onClick.bind(this) as EventListener));
this.input.remove();
this.picker.remove();
};
focus() {
this.input.focus();
};
loadValue(item: any) {
this.defaultValue = item[this.args.column.field];
this.slider!.value = String(this.defaultValue ?? '');
this.input.value = String(this.defaultValue);
this.input.select();
};
serializeValue() {
return parseInt(this.input.value, 10) || 0;
};
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
};
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && ((parseInt(this.input.value as any, 10) || 0) !== this.defaultValue);
};
validate(): EditorValidationResult {
if (isNaN(parseInt(this.input.value, 10))) {
return {
valid: false,
msg: 'Please enter a valid positive number'
};
}
return {
valid: true,
msg: null
};
};
}
/*
* An example of a 'detached' editor.
* The UI is added onto document BODY and .position(), .show() and .hide() are implemented.
* KeyDown events are also handled to provide handling for Tab, Shift-Tab, Esc and Ctrl-Enter.
*/
export class LongTextEditor<TData = any, C extends Column<TData> = Column<TData>, O extends GridOption<C> = GridOption<C>> implements Editor {
protected input!: HTMLTextAreaElement;
protected wrapper!: HTMLDivElement;
protected defaultValue?: string;
protected selectionStart = 0;
constructor(protected readonly args: EditorArguments<TData, C, O>) {
this.init();
}
init() {
const compositeEditorOptions = this.args.compositeEditorOptions;
this.args.grid.getOptions().editorCellNavOnLRKeys;
const container = compositeEditorOptions ? this.args.container : document.body;
this.wrapper = Utils.createDomElement('div', { className: 'slick-large-editor-text' }, container);
if (compositeEditorOptions) {
this.wrapper.style.position = 'relative';
Utils.setStyleSize(this.wrapper, 'padding', 0);
Utils.setStyleSize(this.wrapper, 'border', 0);
} else {
this.wrapper.style.position = 'absolute';
}
this.input = Utils.createDomElement('textarea', { rows: 5, style: { background: 'white', width: '250px', height: '80px', border: '0', outline: '0' } }, this.wrapper);
// trigger onCompositeEditorChange event when input changes and it's a Composite Editor
if (compositeEditorOptions) {
this.input.addEventListener('change', this.onChange.bind(this));
} else {
const btnContainer = Utils.createDomElement('div', { style: 'text-align:right' }, this.wrapper);
Utils.createDomElement('button', { id: 'save', className: 'slick-btn slick-btn-primary', textContent: 'Save' }, btnContainer);
Utils.createDomElement('button', { id: 'cancel', className: 'slick-btn slick-btn-default', textContent: 'Cancel' }, btnContainer);
this.wrapper.querySelector('#save')!.addEventListener('click', this.save.bind(this));
this.wrapper.querySelector('#cancel')!.addEventListener('click', this.cancel.bind(this));
this.input.addEventListener('keydown', this.handleKeyDown.bind(this) as EventListener);
this.position(this.args.position as ElementPosition);
}
this.input.focus();
this.input.select();
};
onChange() {
const activeCell = this.args.grid.getActiveCell();
// when valid, we'll also apply the new value to the dataContext item object
if (this.validate().valid) {
this.applyValue(this.args.item, this.serializeValue());
}
this.applyValue(this.args.compositeEditorOptions.formValues, this.serializeValue());
this.args.grid.onCompositeEditorChange.notify({
row: activeCell?.row ?? 0,
cell: activeCell?.cell ?? 0,
item: this.args.item,
column: this.args.column,
formValues: this.args.compositeEditorOptions.formValues,
grid: this.args.grid,
editors: this.args.compositeEditorOptions.editors
} as unknown as OnCompositeEditorChangeEventArgs);
};
handleKeyDown(e: KeyboardEvent & { target: HTMLInputElement }) {
if (e.which === keyCode.ENTER && e.ctrlKey) {
this.save();
} else if (e.which === keyCode.ESCAPE) {
e.preventDefault();
this.cancel();
} else if (e.which === keyCode.TAB && e.shiftKey) {
e.preventDefault();
this.args.grid.navigatePrev();
} else if (e.which === keyCode.TAB) {
e.preventDefault();
this.args.grid.navigateNext();
} else if (e.which === keyCode.LEFT || e.which === keyCode.RIGHT) {
if (this.args.grid.getOptions().editorCellNavOnLRKeys) {
const cursorPosition = this.selectionStart;
const textLength = e.target.value.length;
if (e.keyCode === keyCode.LEFT && cursorPosition === 0) {
this.args.grid.navigatePrev();
}
if (e.keyCode === keyCode.RIGHT && cursorPosition >= textLength - 1) {
this.args.grid.navigateNext();
}
}
}
};
save() {
const gridOptions = this.args.grid.getOptions() || {};
if (gridOptions.autoCommitEdit) {
this.args.grid.getEditorLock().commitCurrentEdit();
} else {
this.args.commitChanges();
}
};
cancel() {
this.input.value = String(this.defaultValue ?? '');
this.args.cancelChanges();
};
hide() {
Utils.hide(this.wrapper);
};
show() {
Utils.show(this.wrapper);
};
position(position: ElementPosition) {
Utils.setStyleSize(this.wrapper, 'top', (position.top || 0) - 5);
Utils.setStyleSize(this.wrapper, 'left', (position.left || 0) - 2);
};
destroy() {
if (this.args.compositeEditorOptions) {
this.input.removeEventListener('change', this.onChange.bind(this));
} else {
this.wrapper.querySelector('#save')!.removeEventListener('click', this.save.bind(this));
this.wrapper.querySelector('#cancel')!.removeEventListener('click', this.cancel.bind(this));
this.input.removeEventListener('keydown', this.handleKeyDown.bind(this) as EventListener);
}
this.wrapper.remove();
};
focus() {
this.input.focus();
};
loadValue(item: any) {
this.input.value = this.defaultValue = item[this.args.column.field];
this.input.select();
};
serializeValue() {
return this.input.value;
};
applyValue(item: any, state: any) {
item[this.args.column.field] = state;
};
isValueChanged() {
return (!(this.input.value === '' && !Utils.isDefined(this.defaultValue))) && (this.input.value !== this.defaultValue);
};
validate() {
if (this.args.column.validator) {
const validationResults = this.args.column.validator(this.input.value, this.args);
if (!validationResults.valid) {
return validationResults;
}
}
return {
valid: true,
msg: null
};
};
}
/*
* Depending on the value of Grid option 'editorCellNavOnLRKeys', us
* Navigate to the cell on the left if the cursor is at the beginning of the input string
* and to the right cell if it's at the end. Otherwise, move the cursor within the text
*/
function handleKeydownLRNav(e: KeyboardEvent & { target: HTMLInputElement; selectionStart: number; }) {
const cursorPosition = e.selectionStart;
const textLength = e.target.value.length;
if ((e.keyCode === keyCode.LEFT && cursorPosition > 0) ||
e.keyCode === keyCode.RIGHT && cursorPosition < textLength - 1) {
e.stopImmediatePropagation();
}
}
function handleKeydownLRNoNav(e: KeyboardEvent) {
if (e.keyCode === keyCode.LEFT || e.keyCode === keyCode.RIGHT) {
e.stopImmediatePropagation();
}
}
export const Editors = {
Text: TextEditor,
Integer: IntegerEditor,
Float: FloatEditor,
Flatpickr: FlatpickrEditor,
YesNoSelect: YesNoSelectEditor,
Checkbox: CheckboxEditor,
PercentComplete: PercentCompleteEditor,
LongText: LongTextEditor
};
// extend Slick namespace on window object when building as iife
if (IIFE_ONLY && window.Slick) {
Utils.extend(Slick, {
Editors
});
}