diff --git a/demos/vanilla/src/examples/example12.ts b/demos/vanilla/src/examples/example12.ts index bf6b14f0f..aca2fd972 100644 --- a/demos/vanilla/src/examples/example12.ts +++ b/demos/vanilla/src/examples/example12.ts @@ -173,7 +173,7 @@ export default class Example12 { alwaysSaveOnEnterKey: true, maxLength: 12, options: { - cols: 45, + useColumnWidth: true, rows: 6, buttonTexts: { cancel: 'Close', diff --git a/packages/common/src/editors/__tests__/longTextEditor.spec.ts b/packages/common/src/editors/__tests__/longTextEditor.spec.ts index e46651ba1..b584e294b 100644 --- a/packages/common/src/editors/__tests__/longTextEditor.spec.ts +++ b/packages/common/src/editors/__tests__/longTextEditor.spec.ts @@ -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; @@ -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; diff --git a/packages/common/src/editors/longTextEditor.ts b/packages/common/src/editors/longTextEditor.ts index 3e9340611..c136f5f16 100644 --- a/packages/common/src/editors/longTextEditor.ts +++ b/packages/common/src/editors/longTextEditor.ts @@ -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, @@ -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; @@ -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(); } @@ -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', { @@ -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' }); @@ -200,6 +208,7 @@ export class LongTextEditor implements Editor { destroy(): void { clearTimeout(this._timer); + this._eventHandler.unsubscribeAll(); this._bindEventService.unbindAll(); this._wrapperElm?.remove(); } @@ -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 @@ -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 { + 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; diff --git a/packages/common/src/interfaces/longTextEditorOption.interface.ts b/packages/common/src/interfaces/longTextEditorOption.interface.ts index c9fc82334..76b587937 100644 --- a/packages/common/src/interfaces/longTextEditorOption.interface.ts +++ b/packages/common/src/interfaces/longTextEditorOption.interface.ts @@ -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. diff --git a/test/cypress/e2e/example12.cy.ts b/test/cypress/e2e/example12.cy.ts index e9746743b..8d8d5dfdc 100644 --- a/test/cypress/e2e/example12.cy.ts +++ b/test/cypress/e2e/example12.cy.ts @@ -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(); + }); + }); + }); });