Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion demos/vanilla/src/examples/example12.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,7 @@ export default class Example12 {
alwaysSaveOnEnterKey: true,
maxLength: 12,
options: {
cols: 45,
useColumnWidth: true,
rows: 6,
buttonTexts: {
cancel: 'Close',
Expand Down
22 changes: 22 additions & 0 deletions packages/common/src/editors/__tests__/longTextEditor.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ const gridStub = {
navigatePrev: vi.fn(),
render: vi.fn(),
onBeforeEditCell: new SlickEvent(),
onColumnsResized: new SlickEvent(),
onCompositeEditorChange: new SlickEvent(),
} as unknown as SlickGrid;

Expand Down Expand Up @@ -174,6 +175,27 @@ describe('LongTextEditor', () => {
expect(editor.editorDomElement.rows).toBe(6);
});

it('should initialize the editor using the container (column) width when useColumnWidth is enabled', () => {
Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 180 });
mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption;
editor = new LongTextEditor(editorArguments);

expect(editor.editorDomElement.style.width).toBe('180px');
});

it('should update the textarea width when a column is resized and useColumnWidth is enabled', () => {
Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 180 });
mockColumn.editor!.options = { useColumnWidth: true } as LongTextEditorOption;
editor = new LongTextEditor(editorArguments);

expect(editor.editorDomElement.style.width).toBe('180px');

Object.defineProperty(editorArguments.container, 'clientWidth', { configurable: true, value: 240 });
gridStub.onColumnsResized.notify({ triggeredByColumn: 'title', grid: gridStub });

expect(editor.editorDomElement.style.width).toBe('240px');
});

it('should have a placeholder when defined in its column definition', () => {
const testValue = 'test placeholder';
mockColumn.editor!.placeholder = testValue;
Expand Down
34 changes: 32 additions & 2 deletions packages/common/src/editors/longTextEditor.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { BindingEventService } from '@slickgrid-universal/binding';
import { createDomElement, getOffset, setDeepValue, toSentenceCase, type HtmlElementPosition } from '@slickgrid-universal/utils';
import { SlickEventData, type SlickGrid } from '../core/index.js';
import { SlickEventData, SlickEventHandler, type SlickGrid } from '../core/index.js';
import { textValidator } from '../editorValidators/textValidator.js';
import type {
Column,
Expand All @@ -27,6 +27,7 @@ import { Constants } from './../constants.js';
*/
export class LongTextEditor implements Editor {
protected _bindEventService: BindingEventService;
protected _eventHandler: SlickEventHandler;
protected _defaultTextValue: any;
protected _isValueTouched = false;
protected _locales: Locale;
Expand Down Expand Up @@ -59,6 +60,7 @@ export class LongTextEditor implements Editor {
this._locales = this.gridOptions?.locales || Constants.locales;

this._bindEventService = new BindingEventService();
this._eventHandler = new SlickEventHandler();
this.init();
}

Expand Down Expand Up @@ -126,6 +128,7 @@ export class LongTextEditor implements Editor {
containerElm.appendChild(this._wrapperElm);

// use textarea row if defined but don't go over 3 rows with composite editor modal
const useColWidth = !this.args.isCompositeEditor && (this.editorOptions?.useColumnWidth ?? false);
this._textareaElm = createDomElement(
'textarea',
{
Expand All @@ -134,10 +137,15 @@ export class LongTextEditor implements Editor {
rows: this.args.isCompositeEditor && textAreaRows > 3 ? 3 : textAreaRows,
placeholder: this.columnEditor?.placeholder ?? '',
title: this.columnEditor?.title ?? '',
style: useColWidth ? { width: `${this._getContainerInnerWidth()}px` } : {},
},
this._wrapperElm
);

if (useColWidth) {
this._eventHandler.subscribe(this.grid.onColumnsResized, this.handleColumnsResized.bind(this));
}

const editorFooterElm = createDomElement('div', { className: 'editor-footer' });
const countContainerElm = createDomElement('span', { className: 'counter' });
this._currentLengthElm = createDomElement('span', { className: 'text-length', textContent: '0' });
Expand Down Expand Up @@ -200,6 +208,7 @@ export class LongTextEditor implements Editor {

destroy(): void {
clearTimeout(this._timer);
this._eventHandler.unsubscribeAll();
this._bindEventService.unbindAll();
this._wrapperElm?.remove();
}
Expand Down Expand Up @@ -320,8 +329,10 @@ export class LongTextEditor implements Editor {
const calculatedBodyWidth = document.body.offsetWidth || window.innerWidth;

// first defined position will be bottom/right (which will position the editor completely over the cell)
// offset left by the container's padding-left so the wrapper aligns with the cell's text content
const containerPaddingLeft = parseFloat(getComputedStyle(this.args.container).paddingLeft) || 0;
let newPositionTop = this.args.container ? containerOffset.top : (parentPosition.top ?? 0);
let newPositionLeft = this.args.container ? containerOffset.left : (parentPosition.left ?? 0);
let newPositionLeft = this.args.container ? containerOffset.left + containerPaddingLeft : (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
Expand Down Expand Up @@ -421,6 +432,25 @@ export class LongTextEditor implements Editor {
this.disable(isCellEditable === false);
}

protected handleColumnsResized(): void {
if (this._textareaElm) {
this._textareaElm.style.width = `${this._getContainerInnerWidth()}px`;
}
}

protected _getContainerInnerWidth(): number {
Copy link
Copy Markdown
Owner

@ghiscoding ghiscoding Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok so for that approach, I already have a util that does the same, so I would rather use that and have less lines of code :)

getInnerSize(this.args.container, 'width');

Copy link
Copy Markdown
Collaborator Author

@zewa666 zewa666 Mar 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good catch. already out of office and I'll be on vacation starting tomorrow though. so lets just keep the PR open until next week and you go ahead with your release as planned

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's ok, I can modify it myself, thanks again for the contribution :)

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after giving it a try, your calculation is better since it also includes the wrapper element padding, so it's probably better to keep your private function since it's more accurate. We could revisit in the future, but for now it's probably best to keep as it is. Thanks

const container = this.args.container;
const containerStyle = getComputedStyle(container);
const wrapperStyle = getComputedStyle(this._wrapperElm);
return (
container.clientWidth -
(parseFloat(containerStyle.paddingLeft) || 0) -
(parseFloat(containerStyle.paddingRight) || 0) -
(parseFloat(wrapperStyle.paddingLeft) || 0) -
(parseFloat(wrapperStyle.paddingRight) || 0)
);
}

protected handleKeyDown(e: KeyboardEvent): void {
const key = e.key;
this._isValueTouched = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,13 @@ export interface LongTextEditorOption {
*/
cols?: number;

/**
* Defaults to false, when enabled the editor textarea will use the column width (in pixels) instead of the `cols` setting,
* and will also auto-resize the textarea width whenever the column itself gets resized.
* Note: this only applies to Inline Editing and will not have any effect when using the Composite Editor modal window.
*/
useColumnWidth?: boolean;

/**
* Defaults to 6, that is the number of visible text lines for the textarea control.
* Note: this only applies to Inline Editing and will not have any effect when using the Composite Editor modal window which will be fixed to 3 rows.
Expand Down
53 changes: 53 additions & 0 deletions test/cypress/e2e/example12.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -894,4 +894,57 @@ describe('Example 12 - Composite Editor Modal', () => {
cy.get('.vc:visible .vc-year').should('have.attr', 'data-vc-year').should('contain', today.getFullYear());
cy.get('.slick-editor-modal-footer .btn-cancel').click();
});

it('should open the "Title" LongText editor and expect its textarea width to match the column width', () => {
cy.get('.grid12')
.find('.slick-header-columns:nth(1)')
.children('.slick-header-column:nth(1)')
.should('contain', 'Title')
.then(($col) => {
const columnWidth = $col[0].getBoundingClientRect().width;

cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).click();

cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => {
const textareaWidth = parseFloat($textarea[0].style.width);
// account for borders/padding
const paddings = 10;
expect(textareaWidth).to.be.closeTo(columnWidth - paddings, 20);
});

cy.get('.editor-title .editor-footer .btn-cancel').click();
});
});

it('should resize the "Title" column and expect the textarea width to update accordingly when the editor is reopened', () => {
cy.get('.grid12')
.find('.slick-header-columns:nth(1)')
.children('.slick-header-column:nth(1)')
.should('contain', 'Title')
.then(($col) => {
const widthBefore = $col[0].getBoundingClientRect().width;

cy.get('.grid12 .slick-resizable-handle:nth(0)')
.trigger('mousedown', { which: 1, force: true })
.trigger('mousemove', { clientX: $col[0].getBoundingClientRect().right + 80, force: true });
cy.get('.grid12 .slick-header-column:nth(2)').trigger('mousemove', 'right').trigger('mouseup', { which: 1, force: true });

cy.get('.grid12')
.find('.slick-header-columns:nth(1)')
.children('.slick-header-column:nth(1)')
.then(($colAfter) => {
const widthAfter = $colAfter[0].getBoundingClientRect().width;
expect(widthAfter).to.be.greaterThan(widthBefore);

cy.get(`[style="transform: translateY(${GRID_ROW_HEIGHT * 0}px);"] > .slick-cell:nth(1)`).click();
cy.get('.slick-large-editor-text.editor-title textarea').then(($textarea) => {
const textareaWidth = parseFloat($textarea[0].style.width);
const paddings = 10;
expect(textareaWidth).to.be.closeTo(widthAfter - paddings, 20);
});

cy.get('.editor-title .editor-footer .btn-cancel').click();
});
});
});
});
Loading