Skip to content

Commit

Permalink
feat(editors): add few editor options to LongText (textarea) Editor
Browse files Browse the repository at this point in the history
- add options to change button texts
- add auto position that will automatically find best window position to show textarea
- add options cols/rows to change textarea size
  • Loading branch information
ghiscoding committed Dec 8, 2020
1 parent 966ebb0 commit 38c7442
Show file tree
Hide file tree
Showing 8 changed files with 251 additions and 26 deletions.
18 changes: 17 additions & 1 deletion src/app/examples/grid-editor.component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
FlatpickrOption,
Formatters,
GridOption,
LongTextEditorOption,
OnEventArgs,
OperatorType,
Sorters,
Expand Down Expand Up @@ -153,7 +154,22 @@ export class GridEditorComponent implements OnInit {
editor: {
model: Editors.longText,
required: true,
validator: myCustomTitleValidator, // use a custom validator
maxLength: 12,
editorOptions: {
// you can change textarea cols,rows (defaults to 40,4)
cols: 42,
rows: 5,
buttonTexts: {
/* you can change button texts (defaults to "Cancel", "Save") */
// cancel: 'Close',
// save: 'Done'

/* or with translations (defaults to "CANCEL", "SAVE") */
// cancelKey: 'CANCEL',
// saveKey: 'SAVE'
}
} as LongTextEditorOption,
validator: myCustomTitleValidator,
},
onCellChange: (e: Event, args: OnEventArgs) => {
console.log(args);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@ import { TestBed } from '@angular/core/testing';
import { TranslateService, TranslateModule } from '@ngx-translate/core';
import { Editors } from '../index';
import { LongTextEditor } from '../longTextEditor';
import { AutocompleteOption, Column, EditorArgs, EditorArguments, GridOption, KeyCode } from '../../models';
import { AutocompleteOption, Column, ColumnEditor, EditorArgs, EditorArguments, GridOption, KeyCode } from '../../models';
import * as utilities from '../../services/utilities';

const mockGetHtmlElementOffset = jest.fn();
// @ts-ignore:2540
utilities.getHtmlElementOffset = mockGetHtmlElementOffset;

const KEY_CHAR_A = 97;
const containerId = 'demo-container';
Expand Down Expand Up @@ -45,6 +50,11 @@ describe('LongTextEditor', () => {
beforeEach(() => {
divContainer = document.createElement('div');
divContainer.innerHTML = template;
divContainer.style.height = '500px';
divContainer.style.width = '600px';
document.body.innerHTML = '';
document.body.style.height = '700px';
document.body.style.width = '1024px';
document.body.appendChild(divContainer);

TestBed.configureTestingModule({
Expand Down Expand Up @@ -76,7 +86,7 @@ describe('LongTextEditor', () => {
container: divContainer,
columnMetaData: null,
dataView: dataViewStub,
gridPosition: { top: 0, left: 0, bottom: 10, right: 10, height: 100, width: 100, visible: true },
gridPosition: { top: 0, left: 0, bottom: 10, right: 10, height: 600, width: 800, visible: true },
position: { top: 0, left: 0, bottom: 10, right: 10, height: 100, width: 100, visible: true },
};
});
Expand Down Expand Up @@ -107,6 +117,7 @@ describe('LongTextEditor', () => {

it('should initialize the editor', () => {
gridOptionMock.i18n = translate;
gridOptionMock.enableTranslate = true;
editor = new LongTextEditor(editorArguments);
const editorCount = document.body.querySelectorAll('.slick-large-editor-text.editor-title textarea').length;
const editorTextCounter = document.body.querySelectorAll<HTMLDivElement>('.slick-large-editor-text.editor-title .editor-footer .counter');
Expand All @@ -119,7 +130,7 @@ describe('LongTextEditor', () => {
expect(editorCount).toBe(1);
expect(editorTextCounter.length).toBe(1);
expect(currentTextLengthElm.textContent).toBe('0');
expect(maxTextLengthElm.textContent).toBe('500');
expect(maxTextLengthElm).toBeNull();
expect(buttonCancelElm.textContent).toBe('Annuler');
expect(buttonSaveElm.textContent).toBe('Sauvegarder');
});
Expand Down Expand Up @@ -191,7 +202,7 @@ describe('LongTextEditor', () => {
const maxTextLengthElm = document.body.querySelector<HTMLDivElement>('.editor-footer .max-length');

expect(currentTextLengthElm.textContent).toBe('6');
expect(maxTextLengthElm.textContent).toBe('500');
expect(maxTextLengthElm).toBeNull();
expect(editor.getValue()).toBe('task 1');
expect(editorElm[0].defaultValue).toBe('task 1');
});
Expand Down Expand Up @@ -645,5 +656,91 @@ describe('LongTextEditor', () => {
expect(validation).toEqual({ valid: false, msg: 'Please make sure your text is less than or equal to 10 characters' });
});
});

describe('Truncate Text when using maxLength', () => {
it('should truncate text to 10 chars when the provided text (with input/keydown event) is more than maxLength(10)', () => {
const eventInput = new (window.window as any).KeyboardEvent('input', { keyCode: KEY_CHAR_A, bubbles: true, cancelable: true });
(mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10;

editor = new LongTextEditor(editorArguments);

editor.setValue('some extra long text that is over the maxLength');
const editorElm = document.body.querySelector('.editor-title textarea') as HTMLTextAreaElement;

editor.focus();
editorElm.dispatchEvent(eventInput);

const currentTextLengthElm = document.body.querySelector('.editor-footer .text-length') as HTMLDivElement;
const maxTextLengthElm = document.body.querySelector('.editor-footer .max-length') as HTMLDivElement;

expect(editorElm.value).toBe('some extra');
expect(currentTextLengthElm.textContent).toBe('10');
expect(maxTextLengthElm.textContent).toBe('10');
expect(editor.isValueChanged()).toBe(true);
});

it('should truncate text to 10 chars when the provided text (with paste event) is more than maxLength(10)', () => {
const eventPaste = new (window.window as any).CustomEvent('paste', { bubbles: true, cancelable: true });
(mockColumn.internalColumnEditor as ColumnEditor).maxLength = 10;

editor = new LongTextEditor(editorArguments);

editor.setValue('some extra long text that is over the maxLength');
const editorElm = document.body.querySelector('.editor-title textarea') as HTMLTextAreaElement;

editor.focus();
editorElm.dispatchEvent(eventPaste);

const currentTextLengthElm = document.body.querySelector('.editor-footer .text-length') as HTMLDivElement;
const maxTextLengthElm = document.body.querySelector('.editor-footer .max-length') as HTMLDivElement;

expect(editorElm.value).toBe('some extra');
expect(currentTextLengthElm.textContent).toBe('10');
expect(maxTextLengthElm.textContent).toBe('10');
expect(editor.isValueChanged()).toBe(true);
});
});

describe('Position Editor', () => {
beforeEach(() => {
Object.defineProperty(window, 'innerHeight', { writable: true, configurable: true, value: 600 });
Object.defineProperty(window, 'innerWidth', { writable: true, configurable: true, value: 1024 });

// cell height/width
editorArguments.position = { top: 0, left: 900, bottom: 10, right: 10, height: 100, width: 310, visible: true };
Object.defineProperty(editorArguments.container, 'offsetHeight', { writable: true, configurable: true, value: 33 });
Object.defineProperty(editorArguments.container, 'offsetWidth', { writable: true, configurable: true, value: 100 });
});

it('should assume editor to positioned on the right & bottom of the cell when there is enough room', () => {
mockGetHtmlElementOffset.mockReturnValue({ top: 100, left: 200 }); // mock cell position

editor = new LongTextEditor(editorArguments);
const editorElm = document.body.querySelector('.slick-large-editor-text') as HTMLDivElement;

expect(editorElm.style.top).toBe('100px');
expect(editorElm.style.left).toBe('200px');
});

it('should assume editor to positioned on the right of the cell when there is NOT enough room on the left', () => {
mockGetHtmlElementOffset.mockReturnValue({ top: 100, left: 900 }); // mock cell position that will be over max of 1024px

editor = new LongTextEditor(editorArguments);
const editorElm = document.body.querySelector('.slick-large-editor-text') as HTMLDivElement;

expect(editorElm.style.top).toBe('100px');
expect(editorElm.style.left).toBe('675px'); // cellLeftPos - (editorWidth - cellWidth + marginAdjust) => (900 - (310 - 100 + 15))
});

it('should assume editor to positioned on the top of the cell when there is NOT enough room on the bottom', () => {
mockGetHtmlElementOffset.mockReturnValue({ top: 550, left: 200 }); // mock cell position that will be over max of 600px

editor = new LongTextEditor(editorArguments);
const editorElm = document.body.querySelector('.slick-large-editor-text') as HTMLDivElement;

expect(editorElm.style.top).toBe('483px');
expect(editorElm.style.left).toBe('200px'); // cellTopPos - (editorHeight - cellHeight) => (550 - (100 - 33))
});
});
});
});
104 changes: 88 additions & 16 deletions src/app/modules/angular-slickgrid/editors/longTextEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ import {
HtmlElementPosition,
KeyCode,
Locale,
LongTextEditorOption,
} from './../models/index';
import { getDescendantProperty, getHtmlElementOffset, getTranslationPrefix, setDeepValue } from '../services/utilities';
import { textValidator } from '../editorValidators/textValidator';

// using external non-typed js libraries
declare const $: any;
const DEFAULT_MAX_LENGTH = 500;

/*
* An example of a 'detached' editor.
Expand Down Expand Up @@ -71,6 +71,10 @@ export class LongTextEditor implements Editor {
return this._$textarea;
}

get editorOptions(): LongTextEditorOption {
return this.columnEditor && this.columnEditor.editorOptions || {};
}

get hasAutoCommitEdit() {
return this.grid.getOptions().autoCommitEdit;
}
Expand All @@ -84,31 +88,36 @@ export class LongTextEditor implements Editor {
let cancelText = '';
let saveText = '';

if (this._translate && this._translate.instant && this._translate.currentLang) {
if (this._translate && this._translate.instant && this.gridOptions.enableTranslate) {
const translationPrefix = getTranslationPrefix(this.gridOptions);
cancelText = this._translate.instant(`${translationPrefix}CANCEL`);
saveText = this._translate.instant(`${translationPrefix}SAVE`);
const cancelKey = this.editorOptions.buttonTexts && this.editorOptions.buttonTexts.cancelKey || `${translationPrefix}CANCEL`;
const saveKey = this.editorOptions.buttonTexts && this.editorOptions.buttonTexts.saveKey || `${translationPrefix}SAVE`;
cancelText = this._translate.instant(`${translationPrefix}${cancelKey}`);
saveText = this._translate.instant(`${translationPrefix}${saveKey}`);
} else {
cancelText = this._locales && this._locales.TEXT_CANCEL;
saveText = this._locales && this._locales.TEXT_SAVE;
cancelText = this.editorOptions.buttonTexts && this.editorOptions.buttonTexts.cancel || this._locales && this._locales.TEXT_CANCEL || 'Cancel';
saveText = this.editorOptions.buttonTexts && this.editorOptions.buttonTexts.save || this._locales && this._locales.TEXT_SAVE || 'Save';
}

const columnId = this.columnDef && this.columnDef.id;
const placeholder = this.columnEditor && this.columnEditor.placeholder || '';
const title = this.columnEditor && this.columnEditor.title || '';
const maxLength = this.columnEditor && this.columnEditor.maxLength || DEFAULT_MAX_LENGTH;
const textAreaRows = this.columnEditor && this.columnEditor.params && this.columnEditor.params.textAreaRows || 6;
const maxLength = this.columnEditor && this.columnEditor.maxLength;
const textAreaCols = this.editorOptions && this.editorOptions.cols || 40;
const textAreaRows = this.editorOptions && this.editorOptions.rows || 4;

const $container = $('body');
this._$wrapper = $(`<div class="slick-large-editor-text editor-${columnId}" />`).appendTo($container);
this._$textarea = $(`<textarea hidefocus rows="${textAreaRows}" placeholder="${placeholder}" title="${title}">`).appendTo(this._$wrapper);
this._$textarea = $(`<textarea hidefocus cols="${textAreaCols}" rows="${textAreaRows}" placeholder="${placeholder}" title="${title}">`).appendTo(this._$wrapper);

const editorFooterElm = $(`<div class="editor-footer"/>`);
const countContainerElm = $(`<span class="counter"/>`);
this._$currentLengthElm = $(`<span class="text-length">0</span>`);
const textMaxLengthElm = $(`<span>/</span><span class="max-length">${maxLength}</span>`);
this._$currentLengthElm.appendTo(countContainerElm);
textMaxLengthElm.appendTo(countContainerElm);
if (maxLength !== undefined) {
const textMaxLengthElm = $(`<span class="separator">/</span><span class="max-length">${maxLength}</span>`);
textMaxLengthElm.appendTo(countContainerElm);
}

const cancelBtnElm = $(`<button class="btn btn-cancel btn-default btn-xs">${cancelText}</button>`);
const saveBtnElm = $(`<button class="btn btn-save btn-primary btn-xs">${saveText}</button>`);
Expand All @@ -121,6 +130,7 @@ export class LongTextEditor implements Editor {
this._$wrapper.find('.btn-cancel').on('click', () => this.cancel());
this._$textarea.on('keydown', this.handleKeyDown.bind(this));
this._$textarea.on('input', this.handleOnInputChange.bind(this));
this._$textarea.on('paste', this.handleOnInputChange.bind(this));

this.position(this.args && this.args.position);
this._$textarea.focus().select();
Expand All @@ -147,6 +157,7 @@ export class LongTextEditor implements Editor {
if (this._$textarea) {
this._$textarea.off('keydown');
this._$textarea.off('input');
this._$textarea.off('paste');
}
if (this._$wrapper) {
this._$wrapper.find('.btn-save').off('click');
Expand Down Expand Up @@ -206,12 +217,45 @@ export class LongTextEditor implements Editor {
}
}

/**
* Reposition the LongText Editor to be right over the cell, so that it looks like we opened the editor on top of the cell when in reality we just reposition (absolute) over the cell.
* By default we use an "auto" mode which will allow to position the LongText Editor to the best logical position in the window, also when we say position, we are talking about the relative position against the grid cell.
* We can assume that in 80% of the time the default position is bottom right, the default is "auto" but we can also override this and use a specific position.
* Most of the time positioning of the editor will be to the "right" of the cell is ok but if our column is completely on the right side then we'll want to change the position to "left" align.
* Same goes for the top/bottom position, Most of the time positioning the editor to the "bottom" but we are clicking on a cell at the bottom of the grid then we might need to reposition to "top" instead.
* NOTE: this only applies to Inline Editing and will not have any effect when using the Composite Editor modal window.
*/
position(parentPosition: HtmlElementPosition) {
const containerOffset = getHtmlElementOffset(this.args.container);
const containerHeight = this.args.container.offsetHeight;
const containerWidth = this.args.container.offsetWidth;
const calculatedEditorHeight = this._$wrapper.height() || this.args.position.height;
const calculatedEditorWidth = this._$wrapper.width() || this.args.position.width;
const calculatedBodyHeight = document.body.offsetHeight || window.innerHeight; // body height/width might be 0 if so use the window height/width
const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth;

// first defined position will be bottom/right (which will position the editor completely over the cell)
let newPositionTop = containerOffset !== undefined && containerOffset.top || parentPosition.top || 0;
let newPositionLeft = containerOffset !== undefined && containerOffset.left || parentPosition.left || 0;

// user could explicitely use a "left" position (when user knows his column is completely on the right)
// or when using "auto" and we detect not enough available space then we'll position to the "left" of the cell
const position = this.editorOptions && this.editorOptions.position || 'auto';
if (position === 'left' || (position === 'auto' && (newPositionLeft + calculatedEditorWidth) > calculatedBodyWidth)) {
const marginRightAdjustment = this.editorOptions && this.editorOptions.marginRight || 15;
newPositionLeft -= (calculatedEditorWidth - containerWidth + marginRightAdjustment);
}

// do the same calculation/reposition with top/bottom (default is bottom of the cell or in other word starting from the cell going down)
if (position === 'top' || (position === 'auto' && (newPositionTop + calculatedEditorHeight) > calculatedBodyHeight)) {
newPositionTop -= (calculatedEditorHeight - containerHeight);
}


// reposition the editor over the cell (90% of the time this will end up using a position on the "right" of the cell)
this._$wrapper
.css('top', (containerOffset.top || parentPosition.top || 0))
.css('left', (containerOffset.left || parentPosition.left || 0));
.css('top', newPositionTop)
.css('left', newPositionLeft);
}

save() {
Expand Down Expand Up @@ -269,8 +313,36 @@ export class LongTextEditor implements Editor {
}

/** On every input change event, we'll update the current text length counter */
private handleOnInputChange(event: KeyboardEvent & { target: HTMLTextAreaElement }) {
const textLength = event.target.value.length;
this._$currentLengthElm.text(textLength);
private handleOnInputChange(event: JQuery.Event & { originalEvent: any, target: HTMLTextAreaElement }) {
const maxLength = this.columnEditor && this.columnEditor.maxLength;

// when user defines a maxLength, we'll make sure that it doesn't go over this limit if so then truncate the text (disregard the extra text)
let isTruncated = false;
if (maxLength) {
isTruncated = this.truncateText(this._$textarea, maxLength);
}

// if the text get truncated then update text length as maxLength, else update text length with actual
if (isTruncated) {
this._$currentLengthElm.text(maxLength);
} else {
const newText = event.type === 'paste' ? event.originalEvent.clipboardData.getData('text') : event.target.value;
this._$currentLengthElm.text(newText.length);
}
}

/**
* Truncate text if the value is longer than the acceptable max length
* @param $inputElm - textarea jQuery element
* @param maxLength - max acceptable length
* @returns truncated - returns True if it truncated or False otherwise
*/
private truncateText($inputElm: JQuery<HTMLTextAreaElement>, maxLength: number): boolean {
const text = $inputElm.val() + '';
if (text.length > maxLength) {
$inputElm.val(text.substring(0, maxLength));
return true;
}
return false;
}
}
2 changes: 1 addition & 1 deletion src/app/modules/angular-slickgrid/editors/selectEditor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,7 +378,7 @@ export class SelectEditor implements Editor {
// else we use the path provided in the Field Column Definition
const objectPath = this.columnEditor && this.columnEditor.complexObjectPath || fieldName;
const currentValue = (isComplexObject) ? getDescendantProperty(item, objectPath) : item[fieldName];
const value = (isComplexObject && currentValue.hasOwnProperty(this.valueName)) ? currentValue[this.valueName] : currentValue;
const value = (isComplexObject && currentValue && currentValue.hasOwnProperty(this.valueName)) ? currentValue[this.valueName] : currentValue;

if (this.isMultipleSelect && Array.isArray(value)) {
this.loadMultipleValues(value);
Expand Down
1 change: 1 addition & 0 deletions src/app/modules/angular-slickgrid/models/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ export * from './jQueryUiSliderResponse.interface';
export * from './keyCode.enum';
export * from './keyTitlePair.interface';
export * from './locale.interface';
export * from './longTextEditorOption.interface';
export * from './menuCallbackArgs.interface';
export * from './menuCommandItem.interface';
export * from './menuCommandItemCallbackArgs.interface';
Expand Down

0 comments on commit 38c7442

Please sign in to comment.