UNPKG

@handsontable/angular-wrapper

Version:

Best Data Grid for Angular with Spreadsheet Look and Feel.

1 lines • 111 kB
{"version":3,"file":"handsontable-angular-wrapper.mjs","sources":["../../../projects/hot-table/src/lib/editor/custom-editor-placeholder.component.ts","../../../projects/hot-table/src/lib/editor/base-editor-adapter.ts","../../../projects/hot-table/src/lib/renderer/hot-cell-renderer-base.directive.ts","../../../projects/hot-table/src/lib/renderer/hot-cell-renderer.component.ts","../../../projects/hot-table/src/lib/editor/hot-cell-editor-base.directive.ts","../../../projects/hot-table/src/lib/editor/hot-cell-editor.component.ts","../../../projects/hot-table/src/lib/editor/editor-factory-adapter.ts","../../../projects/hot-table/src/lib/renderer/hot-cell-renderer-advanced.component.ts","../../../projects/hot-table/src/lib/editor/hot-cell-editor-advanced.component.ts","../../../projects/hot-table/src/lib/renderer/hot-dynamic-renderer-component.service.ts","../../../projects/hot-table/src/lib/services/hot-settings-resolver.service.ts","../../../projects/hot-table/src/lib/services/hot-global-config.service.ts","../../../projects/hot-table/src/lib/hot-table.component.ts","../../../projects/hot-table/src/lib/hot-table.module.ts","../../../projects/hot-table/src/public-api.ts","../../../projects/hot-table/src/handsontable-angular-wrapper.ts"],"sourcesContent":["import { ChangeDetectionStrategy, Component, ComponentRef, Input, ViewChild, ViewContainerRef } from '@angular/core';\nimport { HotCellEditorComponent } from './hot-cell-editor.component';\nimport { HotCellEditorAdvancedComponent } from './hot-cell-editor-advanced.component';\n\n/**\n * Component representing a placeholder for a custom editor in Handsontable.\n * It is used only within the wrapper.\n */\n@Component({\n template: ` <div\n [class]=\"placeholderCustomClass\"\n [style.display]=\"display\"\n [style.width.px]=\"width\"\n [style.height.px]=\"height\"\n [style.maxWidth.px]=\"width\"\n [style.maxHeight.px]=\"height\"\n [style.top.px]=\"top\"\n [style.left.px]=\"left\"\n >\n <ng-template #inputPlaceholder></ng-template>\n </div>`,\n changeDetection: ChangeDetectionStrategy.OnPush,\n standalone: true,\n})\nexport class CustomEditorPlaceholderComponent {\n /** The top position of the editor. */\n @Input() top: number;\n\n /** The left position of the editor. */\n @Input() left: number;\n\n /** The height of the editor. */\n @Input() height: number;\n\n /** The width of the editor. */\n @Input() width: number;\n\n @Input()\n set isVisible(value: boolean) {\n this._isVisible = value;\n }\n\n @Input() placeholderCustomClass = 'handsontableInputHolder ht_clone_master';\n\n /** The reference to the component instance of the editor. */\n @Input() set componentRef(\n hotEditorComponentRef: ComponentRef<HotCellEditorComponent<any>> |\n ComponentRef<HotCellEditorAdvancedComponent<any>>) {\n if (hotEditorComponentRef) {\n this.container.insert(hotEditorComponentRef.hostView);\n }\n }\n\n /** The container for the editor's input placeholder. */\n @ViewChild('inputPlaceholder', { read: ViewContainerRef, static: true }) container: ViewContainerRef;\n\n /** The display style of the editor. */\n get display(): string {\n return this._isVisible ? 'block' : 'none';\n }\n\n private _isVisible = false;\n\n /**\n * Detaches the container from the Handsontable.\n */\n detachEditor(): void {\n this.container.detach();\n }\n}\n","import Handsontable from 'handsontable/base';\nimport {\n ComponentRef,\n createComponent,\n EnvironmentInjector,\n} from '@angular/core';\nimport { CustomEditorPlaceholderComponent } from './custom-editor-placeholder.component';\nimport { ColumnSettingsInternal } from '../models/column-settings';\nimport { HotCellEditorComponent } from './hot-cell-editor.component';\nimport { Subscription } from 'rxjs';\nimport { take } from 'rxjs/operators';\n\n/**\n * Adapter for BaseEditor from Handsontable.\n */\nexport class BaseEditorAdapter extends Handsontable.editors.BaseEditor {\n /** Reference to the custom editor component. */\n private _componentRef?: ComponentRef<HotCellEditorComponent<any>>;\n\n /** Reference to the editor placeholder component. */\n private _editorPlaceHolderRef: ComponentRef<CustomEditorPlaceholderComponent>;\n\n /** Flag indicating whether the placeholder is ready. */\n private _isPlaceholderReady = false;\n\n /** Subscription for the finish edit event. */\n private _finishEditSubscription?: Subscription;\n\n /** Subscription for the cancel edit event. */\n private _cancelEditSubscription?: Subscription;\n\n /**\n * Creates an instance of BaseEditorAdapter.\n * @param instance The Handsontable instance.\n */\n constructor(instance: Handsontable.Core) {\n super(instance);\n\n this.hot.addHook('afterRowResize', this.onAfterRowResize.bind(this));\n this.hot.addHook('afterColumnResize', this.onAfterColumnResize.bind(this));\n this.hot.addHook('afterDestroy', this.onAfterDestroy.bind(this));\n }\n\n /**\n * Prepares the editor for editing. Parameters are passed from Handsontable.\n * @param row The row index.\n * @param column The column index.\n * @param prop The property name.\n * @param TD The table cell element.\n * @param originalValue The original value of the cell.\n * @param cellProperties The cell properties.\n */\n override prepare(\n row: number,\n column: number,\n prop: string | number,\n TD: HTMLTableCellElement,\n originalValue: any,\n cellProperties: Handsontable.CellProperties\n ): void {\n if (!this.isOpened()) {\n super.prepare(row, column, prop, TD, originalValue, cellProperties);\n const columnMeta: ColumnSettingsInternal = this.hot.getColumnMeta(\n column\n ) as ColumnSettingsInternal;\n\n if (!this._isPlaceholderReady) {\n this.createEditorPlaceholder(columnMeta._environmentInjector);\n this._isPlaceholderReady = true;\n }\n\n this._componentRef = columnMeta._editorComponentReference;\n\n this.cleanupSubscriptions();\n\n this._finishEditSubscription = this._componentRef.instance.finishEdit\n .pipe(take(1))\n .subscribe(() => {\n this.finishEditing();\n });\n\n this._cancelEditSubscription = this._componentRef.instance.cancelEdit\n .pipe(take(1))\n .subscribe(() => {\n this.cancelChanges();\n });\n }\n }\n\n /**\n * Closes the editor. This event is triggered by Handsontable.\n */\n close(): void {\n if (this.isOpened()) {\n this.resetEditorState();\n this._editorPlaceHolderRef?.changeDetectorRef.detectChanges();\n this._editorPlaceHolderRef?.instance.detachEditor();\n this._componentRef?.instance.onClose();\n }\n }\n\n /**\n * Focuses the editor. This event is triggered by Handsontable.\n */\n focus(): void {\n this._componentRef?.instance.onFocus();\n }\n\n /**\n * Gets the value from the editor.\n * @returns The value from the editor.\n */\n getValue(): any {\n return this._componentRef?.instance?.getValue();\n }\n\n /**\n * Opens the editor. This event is triggered by Handsontable.\n * When opening, we set the shortcut context to 'editor'.\n * This allows the built-in keyboard shortcuts to operate within the editor.\n * @param event The event that triggered the opening of the editor.\n * @remarks When entering edit mode using double-click, keyboard shortcuts do not work.\n */\n open(event?: Event): void {\n this.hot.getShortcutManager().setActiveContextName('editor');\n this.applyPropsToEditor();\n this._componentRef?.instance.onOpen(event);\n }\n\n /**\n * Sets the value for the custom editor.\n * @param newValue The value to set.\n */\n setValue(newValue?: any): void {\n this._componentRef?.instance?.setValue(newValue);\n this._componentRef?.changeDetectorRef.detectChanges();\n }\n\n /**\n * Applies properties to the custom editor and editor placeholder.\n */\n private applyPropsToEditor(): void {\n if (!this._componentRef || !this._editorPlaceHolderRef) {\n return;\n }\n\n const rect = this.getEditedCellRect();\n\n if (!this.isInFullEditMode()) {\n this._componentRef.instance.setValue(null);\n }\n\n this._componentRef.setInput('originalValue', this.originalValue);\n this._componentRef.setInput('row', this.row);\n this._componentRef.setInput('column', this.col);\n this._componentRef.setInput('prop', this.prop);\n this._componentRef.setInput('cellProperties', this.cellProperties);\n\n this._editorPlaceHolderRef.setInput('top', rect.top);\n this._editorPlaceHolderRef.setInput('left', rect.start);\n this._editorPlaceHolderRef.setInput('height', rect.height);\n this._editorPlaceHolderRef.setInput('width', rect.width);\n this._editorPlaceHolderRef.setInput('isVisible', true);\n this._editorPlaceHolderRef.setInput('componentRef', this._componentRef);\n this._editorPlaceHolderRef.changeDetectorRef.detectChanges();\n }\n\n /**\n * Creates the editor placeholder and append it to hot rootElement.\n * @param injector The environment injector.\n */\n private createEditorPlaceholder(injector: EnvironmentInjector): void {\n this._editorPlaceHolderRef = createComponent(\n CustomEditorPlaceholderComponent,\n {\n environmentInjector: injector as EnvironmentInjector,\n }\n );\n\n this.hot.rootElement.appendChild(\n this._editorPlaceHolderRef.location.nativeElement\n );\n }\n\n /**\n * Handles the after column resize event.\n * Helps adjust the editor size to the column size and update its position.\n */\n private onAfterColumnResize(): void {\n if (this.isOpened()) {\n this.applyPropsToEditor();\n }\n }\n\n /**\n * Handles the after row resize event.\n * Helps adjust the editor size to the column size and update its position.\n */\n private onAfterRowResize(): void {\n if (this.isOpened()) {\n this.applyPropsToEditor();\n }\n }\n\n /**\n * Handles the after destroy event.\n */\n private onAfterDestroy(): void {\n this.cleanupSubscriptions();\n this._editorPlaceHolderRef?.destroy();\n }\n\n /**\n * Unsubscribes the finish/cancel edit subscriptions if they are still active.\n * Without this, destroying the table while an editor was prepared but never\n * finished/cancelled leaves the `take(1)` subscriptions hanging (they never emit).\n */\n private cleanupSubscriptions(): void {\n if (this._finishEditSubscription) {\n this._finishEditSubscription.unsubscribe();\n this._finishEditSubscription = undefined;\n }\n\n if (this._cancelEditSubscription) {\n this._cancelEditSubscription.unsubscribe();\n this._cancelEditSubscription = undefined;\n }\n }\n\n /**\n * Resets the editor placeholder state.\n * We need to reset the editor placeholder state because we use it\n * to store multiple references to the custom editor.\n */\n private resetEditorState(): void {\n if (!this._editorPlaceHolderRef) {\n return;\n }\n\n this._editorPlaceHolderRef.setInput('top', undefined);\n this._editorPlaceHolderRef.setInput('left', undefined);\n this._editorPlaceHolderRef.setInput('height', undefined);\n this._editorPlaceHolderRef.setInput('width', undefined);\n this._editorPlaceHolderRef.setInput('isVisible', false);\n this._editorPlaceHolderRef.setInput('componentRef', undefined);\n }\n}\n","import Handsontable from 'handsontable/base';\nimport { Directive, Input } from '@angular/core';\n\n/**\n * Shared base directive for HotCellRendererComponent and HotCellRendererAdvancedComponent.\n * Holds all @Input() properties and getProps() that both renderer variants share.\n *\n * @template TValue - The type of the rendered cell value.\n * @template TProps - The type of additional renderer properties.\n */\n@Directive()\nexport abstract class HotCellRendererBase<\n TValue extends string | number | boolean | Record<string, any> | Array<any> = string,\n TProps extends {} = any\n> {\n @Input() value: TValue = '' as TValue;\n\n @Input() instance: Handsontable;\n @Input() td: HTMLTableCellElement;\n @Input() row: number;\n @Input() col: number;\n @Input() prop: string | number;\n\n @Input() cellProperties: Handsontable.CellProperties & { rendererProps?: TProps };\n\n public getProps(): TProps {\n return this.cellProperties?.rendererProps ?? ({} as TProps);\n }\n}\n","import { ChangeDetectionStrategy, Component } from '@angular/core';\nimport { HotCellRendererBase } from './hot-cell-renderer-base.directive';\n\n/**\n * Abstract base component for creating custom cell renderer components for Handsontable.\n *\n * Extend this component and provide your own template to implement a custom renderer.\n * Value type is limited to primitives (`string | number | boolean`).\n * For object and array values use {@link HotCellRendererAdvancedComponent}.\n *\n * @template TValue - The type of the component renderer.\n * @template TProps - The type of additional renderer properties.\n */\n@Component({\n selector: 'hot-cell-renderer',\n template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport abstract class HotCellRendererComponent<TValue extends string | number | boolean = string, TProps extends {} = any>\n extends HotCellRendererBase<TValue, TProps> {\n static readonly RENDERER_MARKER = Symbol('HotCellRendererComponent');\n}\n","import Handsontable from 'handsontable/base';\nimport { Directive, EventEmitter, HostBinding, Input, Output } from '@angular/core';\n\n/**\n * Shared base directive for HotCellEditorComponent and HotCellEditorAdvancedComponent.\n * Holds all @Input(), @Output() and @HostBinding() declarations that both editor variants share.\n *\n * @template T - The type of the edited cell value.\n */\n@Directive()\nexport abstract class HotCellEditorBase<\n T extends string | number | boolean | Record<string, any> | Array<any>\n> {\n @HostBinding('style.height.%') protected heightFitParentContainer = 100;\n @HostBinding('style.width.%') protected widthFitParentContainer = 100;\n\n @Input() row: number;\n @Input() column: number;\n @Input() prop: string | number;\n @Input() originalValue: T;\n @Input() cellProperties: Handsontable.CellProperties;\n\n @Output() finishEdit = new EventEmitter<void>();\n @Output() cancelEdit = new EventEmitter<void>();\n\n protected value: T;\n\n getValue(): T {\n return this.value;\n }\n\n setValue(value: T): void {\n this.value = value;\n }\n}\n","import { Directive, HostBinding } from '@angular/core';\nimport { HotCellEditorBase } from './hot-cell-editor-base.directive';\n\n/**\n * Abstract class representing a basic Handsontable cell editor in Angular.\n *\n * Extend this class and decorate the subclass with `@Component()` to implement a custom editor.\n * Value type is limited to primitives (`string | number | boolean`).\n * For object and array values use {@link HotCellEditorAdvancedComponent}.\n */\n@Directive()\nexport abstract class HotCellEditorComponent<T extends string | number | boolean> extends HotCellEditorBase<T> {\n static readonly EDITOR_MARKER = Symbol('HotCellEditorComponent');\n\n /** The tabindex attribute for the editor. */\n @HostBinding('attr.tabindex') protected tabindex = -1;\n\n /** The data-hot-input attribute for the editor. */\n @HostBinding('attr.data-hot-input') protected dataHotInput = '';\n\n /** The handsontableInput class for the editor. */\n @HostBinding('class.handsontableInput') protected handsontableInputClass = true;\n\n /** Event triggered by Handsontable on closing the editor. */\n onClose(): void {}\n\n /** Event triggered by Handsontable on opening the editor. */\n onOpen(event?: Event): void {}\n\n /**\n * Event triggered by Handsontable on focusing the editor.\n * Must be implemented by the subclass to move focus to the actual input element.\n * @example\n * ```typescript\n * @Component({ template: `<input #inputElement>` })\n * class CustomEditor extends HotCellEditorComponent<string> {\n * @ViewChild('inputElement') inputElement!: ElementRef;\n * onFocus(): void { this.inputElement.nativeElement.focus(); }\n * }\n * ```\n */\n abstract onFocus(): void;\n}\n","import { ComponentRef, createComponent, EnvironmentInjector } from '@angular/core';\nimport { CustomEditorPlaceholderComponent } from './custom-editor-placeholder.component';\nimport { AngularEditorProperties, HotInstanceWithAngularInjector } from './models/factory-editor-properties';\nimport { editorFactory, ExtendedEditor } from 'handsontable/editors/factory';\nimport { take } from 'rxjs/operators';\nimport { HotCellEditorAdvancedComponent } from './hot-cell-editor-advanced.component';\n\n/**\n * Combined type representing an extended editor with Angular component properties.\n * Used internally by the factory adapter to bridge Angular and Handsontable.\n */\ntype EditorInstance = ExtendedEditor<AngularEditorProperties & HotCellEditorAdvancedComponent<any>>;\n\n/**\n * Factory function to create a custom Handsontable editor adapter for Angular components.\n *\n * This adapter integrates Angular components with Handsontable's editor system using the new\n * editorFactory API, allowing you to use Angular components as custom cell editors while\n * maintaining full Angular lifecycle management and change detection.\n *\n * @returns A custom editor class that can be used in Handsontable column settings.\n *\n */\nexport const FactoryEditorAdapter = (componentRef: ComponentRef<HotCellEditorAdvancedComponent<any>>) =>\n editorFactory<ExtendedEditor<any>>({\n position: componentRef.instance.position,\n shortcuts: componentRef.instance.shortcuts,\n config: componentRef.instance.config,\n init(editor: EditorInstance): void {\n editor._componentRef = componentRef;\n editor._editorPlaceHolderRef = undefined;\n editor._finishEditSubscription = undefined;\n editor._cancelEditSubscription = undefined;\n\n createEditorPlaceholder(editor, (editor.hot as HotInstanceWithAngularInjector)._angularEnvironmentInjector);\n editor.input = editor._editorPlaceHolderRef?.location.nativeElement ?? document.createElement('div');\n\n editor._afterRowResizeCallback = (): void => {\n if (editor.isOpened()) {\n applyPropsToEditor(editor);\n }\n };\n\n editor._afterColumnResizeCallback = (): void => {\n if (editor.isOpened()) {\n applyPropsToEditor(editor);\n }\n };\n\n editor._afterDestroyCallback = (): void => {\n cleanupSubscriptions(editor);\n if (editor._editorPlaceHolderRef) {\n editor._editorPlaceHolderRef.destroy();\n }\n };\n\n // Hooks are automatically removed by Handsontable on table destroy\n editor.hot.addHook('afterRowResize', editor._afterRowResizeCallback);\n editor.hot.addHook('afterColumnResize', editor._afterColumnResizeCallback);\n editor.hot.addHook('afterDestroy', editor._afterDestroyCallback);\n },\n\n afterInit: (editor) => editor._componentRef.instance.afterInit?.(editor),\n beforeOpen: (editor: EditorInstance, context) => {\n cleanupSubscriptions(editor);\n\n applyPropsToEditor(editor);\n\n editor._finishEditSubscription = editor._componentRef.instance.finishEdit.pipe(take(1)).subscribe((): void => {\n editor.finishEditing();\n });\n\n editor._cancelEditSubscription = editor._componentRef.instance.cancelEdit.pipe(take(1)).subscribe((): void => {\n editor.cancelChanges();\n });\n editor._componentRef.instance.beforeOpen?.(editor, context);\n },\n afterOpen: (editor, event) => {\n editor._componentRef.instance.afterOpen?.(editor, event);\n },\n onFocus: (editor) => editor._componentRef.instance.onFocus?.(editor),\n afterClose: (editor: EditorInstance) => {\n resetEditorState(editor);\n editor._editorPlaceHolderRef?.changeDetectorRef.detectChanges();\n editor._editorPlaceHolderRef?.instance.detachEditor();\n editor._componentRef.instance.afterClose?.(editor);\n },\n getValue: (editor) => editor._componentRef.instance.getValue(),\n setValue: (editor: EditorInstance, value) => {\n editor.value = value;\n editor._componentRef.instance.setValue(value);\n editor._componentRef.changeDetectorRef.detectChanges();\n },\n });\n\n/**\n * Creates the editor placeholder component.\n * @param editor The editor instance.\n * @param injector The environment injector from Angular.\n */\nfunction createEditorPlaceholder(editor: EditorInstance, injector: EnvironmentInjector | undefined): void {\n if (!injector) {\n return;\n }\n\n editor._editorPlaceHolderRef = createComponent(CustomEditorPlaceholderComponent, {\n environmentInjector: injector,\n });\n}\n\n/**\n * Applies properties to the custom Angular editor and editor placeholder.\n * Updates position, size, and cell context information.\n * @param editor The editor instance.\n */\nfunction applyPropsToEditor(editor: EditorInstance): void {\n if (!editor._componentRef || !editor._editorPlaceHolderRef) {\n return;\n }\n\n editor._componentRef.setInput('originalValue', editor.originalValue);\n editor._componentRef.setInput('row', editor.row);\n editor._componentRef.setInput('column', editor.col);\n editor._componentRef.setInput('prop', editor.prop);\n editor._componentRef.setInput('cellProperties', editor.cellProperties);\n\n const rect = editor.hot.getCell(editor.row, editor.col)?.getBoundingClientRect();\n\n editor._editorPlaceHolderRef.setInput('placeholderCustomClass', '');\n editor._editorPlaceHolderRef.setInput('height', rect?.height ?? 0);\n editor._editorPlaceHolderRef.setInput('width', rect?.width ?? 0);\n editor._editorPlaceHolderRef.setInput('isVisible', true);\n editor._editorPlaceHolderRef.setInput('componentRef', editor._componentRef);\n\n editor._editorPlaceHolderRef.changeDetectorRef.detectChanges();\n}\n\n/**\n * Resets the editor placeholder state.\n * Clears all positioning and visibility settings.\n * @param editor The editor instance.\n */\nfunction resetEditorState(editor: EditorInstance): void {\n if (!editor._editorPlaceHolderRef) {\n return;\n }\n\n editor._editorPlaceHolderRef.setInput('top', undefined);\n editor._editorPlaceHolderRef.setInput('left', undefined);\n editor._editorPlaceHolderRef.setInput('height', undefined);\n editor._editorPlaceHolderRef.setInput('width', undefined);\n editor._editorPlaceHolderRef.setInput('isVisible', false);\n editor._editorPlaceHolderRef.setInput('componentRef', undefined);\n}\n\n/**\n * Cleans up existing subscriptions.\n * @param editor The editor instance.\n */\nfunction cleanupSubscriptions(editor: EditorInstance): void {\n if (editor._finishEditSubscription) {\n editor._finishEditSubscription.unsubscribe();\n editor._finishEditSubscription = undefined;\n }\n\n if (editor._cancelEditSubscription) {\n editor._cancelEditSubscription.unsubscribe();\n editor._cancelEditSubscription = undefined;\n }\n}\n","import { ChangeDetectionStrategy, Component } from '@angular/core';\nimport { HotCellRendererBase } from './hot-cell-renderer-base.directive';\n\n/**\n * Abstract base component for creating advanced custom cell renderer components for Handsontable.\n *\n * Extend this component and provide your own template to implement a custom renderer.\n * Unlike {@link HotCellRendererComponent}, this variant also accepts object and array values.\n *\n * @template TValue - The type of the component renderer.\n * @template TProps - The type of additional renderer properties.\n */\n@Component({\n selector: 'hot-cell-renderer-advanced',\n template: `<!-- This is an abstract component. Extend this component and provide your own template. -->`,\n standalone: true,\n changeDetection: ChangeDetectionStrategy.OnPush,\n})\nexport abstract class HotCellRendererAdvancedComponent<\n TValue extends string | number | boolean | Record<string, any> | Array<any> = string,\n TProps extends {} = any\n> extends HotCellRendererBase<TValue, TProps> {\n static readonly RENDERER_MARKER = Symbol('HotCellRendererAdvancedComponent');\n}\n","import { Directive } from '@angular/core';\nimport { ExtendedEditor } from 'handsontable/editors/factory';\nimport { CellProperties } from 'handsontable/settings';\nimport { KeyboardShortcutConfig } from './models/keyboard-shortcut-config';\nimport { HotCellEditorBase } from './hot-cell-editor-base.directive';\n\n/**\n * Abstract class representing an advanced Handsontable cell editor in Angular.\n *\n * Extend this class and decorate the subclass with `@Component()` to implement a custom editor.\n * Unlike {@link HotCellEditorComponent}, this variant also accepts object and array values\n * and provides additional lifecycle hooks and positioning options.\n */\n@Directive()\nexport abstract class HotCellEditorAdvancedComponent<\n T extends string | number | boolean | Record<string, any> | Array<any>\n> extends HotCellEditorBase<T> {\n static readonly EDITOR_MARKER = Symbol('HotCellEditorAdvancedComponent');\n\n /** Event triggered by Handsontable on focusing the editor. Available in advanced mode. */\n onFocus(editor?: ExtendedEditor<T>): void {}\n\n /** The position of the editor in the DOM. Available in advanced mode. */\n position: 'container' | 'portal' = 'container';\n\n /** The shortcuts available for the editor. Available in advanced mode. */\n shortcuts?: KeyboardShortcutConfig[];\n\n /** The group name for the shortcuts. Available in advanced mode. */\n shortcutsGroup?: string;\n\n /** Configuration object. Available in advanced mode. */\n config?: any;\n\n /** Lifecycle hook called after the editor is opened. Available in advanced mode. */\n afterOpen(editor: ExtendedEditor<T>, event?: Event): void {}\n\n /** Lifecycle hook called after the editor is closed. Available in advanced mode. */\n afterClose(editor: ExtendedEditor<T>): void {}\n\n /** Lifecycle hook called after the editor is initialized. Available in advanced mode. */\n afterInit(editor: ExtendedEditor<T>): void {}\n\n /** Lifecycle hook called before the editor is opened. Available in advanced mode. */\n beforeOpen(\n editor: ExtendedEditor<T>,\n {\n row,\n col,\n prop,\n td,\n originalValue,\n cellProperties,\n }: {\n row: number;\n col: number;\n prop: string | number;\n td: HTMLTableCellElement;\n originalValue: any;\n cellProperties: CellProperties;\n }\n ): void {}\n}\n","import {\n ApplicationRef,\n ComponentRef,\n createComponent,\n EmbeddedViewRef,\n EnvironmentInjector,\n Injectable,\n TemplateRef,\n Type\n} from '@angular/core';\nimport { baseRenderer, BaseRenderer, registerRenderer, rendererFactory } from 'handsontable/renderers';\nimport Handsontable from 'handsontable/base';\nimport { HotCellRendererBase } from './hot-cell-renderer-base.directive';\nimport { HotCellRendererComponent } from './hot-cell-renderer.component';\nimport { HotCellRendererAdvancedComponent } from './hot-cell-renderer-advanced.component';\n\nexport const INVALID_RENDERER_WARNING =\n 'The provided renderer component was not recognized as a valid custom renderer. ' +\n 'It must either extend HotCellRendererComponent or be a valid TemplateRef. ' +\n 'Please ensure that your custom renderer is implemented correctly and imported from the proper source.';\n\nexport const INVALID_ADVANCED_RENDERER_WARNING =\n 'The provided renderer component was not recognized as a valid custom renderer. ' +\n 'It must either extend HotCellRendererAdvancedComponent. ' +\n 'Please ensure that your custom renderer is implemented correctly and imported from the proper source.';\n\n/**\n * An object representing the parameters passed to a Handsontable renderer.\n */\ninterface BaseRendererParametersObject {\n instance: Handsontable.Core;\n td: HTMLTableCellElement;\n row: number;\n col: number;\n prop: string | number;\n value: any;\n cellProperties: Handsontable.CellProperties;\n}\n\n// Renderer component inputs, listed once at module scope so the per-cell render path can set them\n// without allocating a fresh key array (via Object.keys) on every cell of every render frame.\nconst RENDERER_INPUT_KEYS: (keyof BaseRendererParametersObject)[] = [\n 'instance', 'td', 'row', 'col', 'prop', 'value', 'cellProperties',\n];\n\n/**\n * Type guard that checks if the given object is a TemplateRef.\n *\n * @param obj - The object to check.\n * @returns True if the object is a TemplateRef; otherwise, false.\n */\nexport function isTemplateRef<T>(obj: any): obj is TemplateRef<T> {\n return !!obj && typeof obj.createEmbeddedView === 'function';\n}\n\n/**\n * Type guard to check if an object is an instance of HotCellRendererComponent.\n *\n * @param obj - The object to check.\n * @returns True if the object is a HotCellRendererComponent, false otherwise.\n */\nexport function isHotCellRendererComponent(obj: any): obj is Type<HotCellRendererComponent> {\n return obj?.RENDERER_MARKER === HotCellRendererComponent.RENDERER_MARKER;\n}\n\n/**\n * Type guard to check if an object is an instance of HotCellRendererAdvancedComponent.\n *\n * @param obj - The object to check.\n * @returns True if the object is a HotCellRendererAdvancedComponent, false otherwise.\n */\nexport function isAdvancedHotCellRendererComponent(obj: any): obj is Type<HotCellRendererAdvancedComponent> {\n return obj?.RENDERER_MARKER === HotCellRendererAdvancedComponent.RENDERER_MARKER;\n}\n\n/**\n * Service for dynamically creating Angular components or templates as custom renderers for Handsontable.\n *\n * This service allows you to create a renderer function that wraps a given Angular component or TemplateRef\n * so that it can be used as a Handsontable renderer.\n *\n * @example\n * const customRenderer = dynamicComponentService.createRendererFromComponent(MyRendererComponent, { someProp: value });\n * // Use customRenderer in your Handsontable configuration\n */\n@Injectable({\n providedIn: 'root',\n})\nexport class DynamicComponentService {\n // Track Angular component refs and embedded views keyed by the TD element they are attached to.\n // When a cell is re-rendered the previous component is destroyed before a new one is created.\n private readonly _tdComponentRefs = new WeakMap<HTMLTableCellElement, ComponentRef<any>>();\n private readonly _tdEmbeddedViews = new WeakMap<HTMLTableCellElement, EmbeddedViewRef<any>>();\n\n // Per-instance registries of every renderer ref/view currently attached to the application.\n // The WeakMaps above only allow per-TD lookup; these sets let us sweep refs whose TD was dropped\n // from Handsontable's virtual viewport (scrolling, updateData), which would otherwise stay\n // attached to ApplicationRef forever and leak both memory and change-detection work.\n //\n // The registries are scoped per Handsontable instance (not one global set) because this service\n // is a root singleton shared by every <hot-table>. A global sweep would scan the cells of every\n // table on the page on each `afterViewRender`, i.e. on every scroll frame of any one of them.\n // Keying by instance bounds each sweep to the cells of the table that actually re-rendered.\n private readonly _instanceComponentRefs = new WeakMap<Handsontable.Core, Set<ComponentRef<any>>>();\n private readonly _instanceEmbeddedViews = new WeakMap<Handsontable.Core, Set<EmbeddedViewRef<any>>>();\n\n // Instances we already wired the sweep hook into, so each instance is hooked at most once.\n private readonly _hookedInstances = new WeakSet<Handsontable.Core>();\n\n constructor(private appRef: ApplicationRef, private environmentInjector: EnvironmentInjector) {}\n\n /**\n * Creates a custom renderer function for Handsontable from an Angular component or TemplateRef.\n * The generated renderer function will be used by Handsontable to render cell content.\n *\n * @param component - The Angular component type or TemplateRef to use as renderer.\n * @param componentProps - An object containing additional properties to use by the renderer.\n * @param register - If true, registers the renderer with Handsontable using the component's name.\n * @returns A renderer function that can be used in Handsontable's configuration.\n */\n createRendererFromComponent(\n component: Type<HotCellRendererComponent> | TemplateRef<any>,\n componentProps: Record<string, any> = {},\n register: boolean = false\n ) {\n let registered = false;\n\n return (\n instance: Handsontable.Core,\n td: HTMLTableCellElement,\n row: number,\n col: number,\n prop: string | number,\n value: any,\n cellProperties: Handsontable.CellProperties\n ) => {\n const properties: BaseRendererParametersObject = {\n value,\n instance,\n td,\n row,\n col,\n prop,\n cellProperties,\n };\n\n (cellProperties as any).rendererProps = componentProps;\n\n baseRenderer.call(this, instance, td, row, col, prop, value, cellProperties);\n\n this.registerSweepHook(instance);\n\n if (isTemplateRef(component)) {\n // Embedded views carry a per-render context, so they are always rebuilt.\n this.replaceCellContent(instance, td);\n const embeddedView = this.attachTemplateToElement(component, td, properties);\n this.trackEmbeddedView(instance, td, embeddedView);\n } else if (isHotCellRendererComponent(component)) {\n this.renderComponent(td, component, properties);\n } else {\n console.warn(INVALID_RENDERER_WARNING);\n }\n\n if (register && !registered && isHotCellRendererComponent(component)) {\n Handsontable.renderers.registerRenderer((component as Type<any>).name, component as any as BaseRenderer);\n registered = true;\n }\n\n return td;\n };\n }\n\n /**\n * Creates a custom renderer function using rendererFactory from Handsontable.\n * This is an alternative implementation that uses the factory pattern.\n *\n * @param component - The Angular component type to use as renderer.\n * @param componentProps - An object containing additional properties to use by the renderer.\n * @param register - If true, registers the renderer with Handsontable using the component's name.\n * @returns A renderer function that can be used in Handsontable's configuration.\n */\n createRendererWithFactory(\n component: Type<HotCellRendererAdvancedComponent>,\n componentProps: Record<string, any> = {},\n register: boolean = false\n ) {\n let registered = false;\n\n return rendererFactory(({\n instance,\n td,\n row,\n column,\n prop,\n value,\n cellProperties\n }) => {\n const properties: BaseRendererParametersObject = {\n value,\n instance,\n td,\n row,\n col: column,\n prop,\n cellProperties,\n };\n\n (cellProperties as any).rendererProps = componentProps;\n\n // Apply the base renderer so the TD gets the same base classes/attributes as the\n // createRendererFromComponent path (rendererFactory itself does not call it).\n baseRenderer.call(this, instance, td, row, column, prop, value, cellProperties);\n\n this.registerSweepHook(instance);\n\n if (isAdvancedHotCellRendererComponent(component)) {\n this.renderComponent(td, component, properties);\n } else {\n console.warn(INVALID_ADVANCED_RENDERER_WARNING);\n }\n\n if (register && !registered && isAdvancedHotCellRendererComponent(component)) {\n registerRenderer(component.name, component as any as BaseRenderer);\n registered = true;\n }\n });\n }\n\n /**\n * Destroys all renderer components and embedded views attached to cells within a container element.\n * Must be called before destroying the Handsontable instance to prevent Angular component leaks.\n *\n * @param container - The root DOM element of the Handsontable instance.\n * @param instance - The Handsontable instance whose registries should be torn down. When omitted\n * (e.g. test stubs), only refs reachable through TDs still in the container are destroyed.\n */\n cleanupContainer(container: HTMLElement, instance?: Handsontable.Core): void {\n const compRefs = instance ? this._instanceComponentRefs.get(instance) : undefined;\n const embViews = instance ? this._instanceEmbeddedViews.get(instance) : undefined;\n\n container.querySelectorAll<HTMLTableCellElement>('td').forEach((td) => {\n const compRef = this._tdComponentRefs.get(td);\n if (compRef) {\n this.destroyComponent(compRef);\n this._tdComponentRefs.delete(td);\n compRefs?.delete(compRef);\n }\n const embView = this._tdEmbeddedViews.get(td);\n if (embView) {\n this.destroyEmbeddedView(embView);\n this._tdEmbeddedViews.delete(td);\n embViews?.delete(embView);\n }\n });\n\n // The loop above only reaches TDs still present in the container. Refs for cells already\n // dropped from the viewport (but not yet swept) would otherwise be orphaned once this\n // instance's afterViewRender hook is gone after destroy. Tear down whatever is left and drop\n // the per-instance registries so a repeated cleanup call is a no-op. (Entries removed in the\n // loop above are already gone from these sets, so nothing is destroyed twice.)\n compRefs?.forEach((ref) => this.destroyComponent(ref));\n embViews?.forEach((view) => this.destroyEmbeddedView(view));\n\n if (instance) {\n this._instanceComponentRefs.delete(instance);\n this._instanceEmbeddedViews.delete(instance);\n }\n }\n\n /**\n * Registers a one-time `afterViewRender` hook on the given Handsontable instance that sweeps\n * renderer refs whose TD is no longer connected to the document. Handsontable recycles a pool of\n * TD elements while virtualizing rows; cells that leave the viewport are never re-rendered, so\n * without this sweep their Angular components stay attached to ApplicationRef and leak.\n *\n * Guarded against instances that do not expose `addHook` (e.g. test stubs).\n *\n * @param instance - The Handsontable instance whose render cycle drives the sweep.\n */\n private registerSweepHook(instance: Handsontable.Core): void {\n if (this._hookedInstances.has(instance) || typeof (instance as any)?.addHook !== 'function') {\n return;\n }\n\n this._hookedInstances.add(instance);\n instance.addHook('afterViewRender', () => this.sweepDetachedViews(instance));\n }\n\n /**\n * Destroys every renderer ref/view tracked for the given instance whose root node is no longer\n * attached to the document.\n *\n * @param instance - The Handsontable instance whose registries should be swept.\n */\n private sweepDetachedViews(instance: Handsontable.Core): void {\n const compRefs = this._instanceComponentRefs.get(instance);\n compRefs?.forEach((ref) => {\n if (!this.isViewConnected(ref.hostView as EmbeddedViewRef<any>)) {\n this.destroyComponent(ref);\n compRefs.delete(ref);\n }\n });\n\n const embViews = this._instanceEmbeddedViews.get(instance);\n embViews?.forEach((view) => {\n if (!this.isViewConnected(view)) {\n this.destroyEmbeddedView(view);\n embViews.delete(view);\n }\n });\n }\n\n /**\n * @returns True if any of the view's root nodes is still connected to the document.\n */\n private isViewConnected(view: EmbeddedViewRef<any>): boolean {\n return view.rootNodes.some((node: Node) => !!node?.isConnected);\n }\n\n /**\n * Destroys the renderer ref/view previously attached to a cell and clears the cell content,\n * so a fresh renderer can take over the TD without leaking the old one.\n *\n * @param instance - The Handsontable instance owning the cell, used to update its registries.\n * @param td - The table cell whose previous content should be torn down.\n */\n private replaceCellContent(instance: Handsontable.Core, td: HTMLTableCellElement): void {\n const prevRef = this._tdComponentRefs.get(td);\n if (prevRef) {\n this.destroyComponent(prevRef);\n this._tdComponentRefs.delete(td);\n this._instanceComponentRefs.get(instance)?.delete(prevRef);\n }\n\n const prevView = this._tdEmbeddedViews.get(td);\n if (prevView) {\n this.destroyEmbeddedView(prevView);\n this._tdEmbeddedViews.delete(td);\n this._instanceEmbeddedViews.get(instance)?.delete(prevView);\n }\n\n td.innerHTML = '';\n }\n\n /**\n * Tracks a component ref both by its TD (for fast replacement) and in the instance registry\n * (for sweeping and full teardown).\n */\n private trackComponentRef(\n instance: Handsontable.Core, td: HTMLTableCellElement, ref: ComponentRef<any>\n ): void {\n this._tdComponentRefs.set(td, ref);\n this.registryFor(this._instanceComponentRefs, instance).add(ref);\n }\n\n /**\n * Tracks an embedded view both by its TD (for fast replacement) and in the instance registry\n * (for sweeping and full teardown).\n */\n private trackEmbeddedView(\n instance: Handsontable.Core, td: HTMLTableCellElement, view: EmbeddedViewRef<any>\n ): void {\n this._tdEmbeddedViews.set(td, view);\n this.registryFor(this._instanceEmbeddedViews, instance).add(view);\n }\n\n /**\n * Returns the per-instance registry set for the given instance, creating it on first use.\n */\n private registryFor<T>(registry: WeakMap<Handsontable.Core, Set<T>>, instance: Handsontable.Core): Set<T> {\n let set = registry.get(instance);\n if (!set) {\n set = new Set<T>();\n registry.set(instance, set);\n }\n return set;\n }\n\n /**\n * Attaches an embedded view created from a TemplateRef to a given DOM element.\n *\n * @param template - The TemplateRef to create an embedded view from.\n * @param tdEl - The target DOM element (a table cell) to which the view will be appended.\n * @param properties - Context object providing properties to be used within the template.\n * @returns The created EmbeddedViewRef so the caller can track and destroy it later.\n */\n private attachTemplateToElement(\n template: TemplateRef<any>, tdEl: HTMLTableCellElement, properties: BaseRendererParametersObject\n ): EmbeddedViewRef<any> {\n const embeddedView: EmbeddedViewRef<any> = template.createEmbeddedView({\n $implicit: properties.value,\n ...properties,\n });\n embeddedView.detectChanges();\n\n embeddedView.rootNodes.forEach((node) => {\n tdEl.appendChild(node);\n });\n\n return embeddedView;\n }\n\n /**\n * Dynamically creates an Angular component of the given type.\n *\n * @param component - The Angular component type to be created.\n * @param rendererParameters - An object containing input properties to assign to the component instance.\n * @returns The ComponentRef of the dynamically created component.\n */\n private createComponent<T extends HotCellRendererBase>(\n component: Type<T>,\n rendererParameters: BaseRendererParametersObject\n ): ComponentRef<T> {\n const componentRef = createComponent(component, {\n environmentInjector: this.environmentInjector,\n });\n\n this.applyInputs(componentRef, rendererParameters);\n componentRef.changeDetectorRef.detectChanges();\n\n this.appRef.attachView(componentRef.hostView);\n\n return componentRef;\n }\n\n /**\n * Renders an Angular component into the given cell, recycling the component already attached to\n * the TD when it is of the same type. Handsontable recycles its pool of TD elements heavily while\n * virtualizing rows, so recreating an Angular component on every re-render would cause needless\n * teardown/instantiation churn and GC pressure. When the type matches we only refresh the inputs.\n *\n * @param td - The target table cell.\n * @param component - The renderer component type to render.\n * @param properties - The renderer parameters to feed as component inputs.\n */\n private renderComponent<T extends HotCellRendererBase>(\n td: HTMLTableCellElement,\n component: Type<T>,\n properties: BaseRendererParametersObject\n ): void {\n const prevRef = this._tdComponentRefs.get(td);\n\n if (\n prevRef &&\n prevRef.componentType === component &&\n this.isViewConnected(prevRef.hostView as EmbeddedViewRef<any>)\n ) {\n this.applyInputs(prevRef, properties);\n prevRef.changeDetectorRef.detectChanges();\n return;\n }\n\n this.replaceCellContent(properties.instance, td);\n const componentRef = this.createComponent(component, properties);\n this.attachComponentToElement(componentRef, td);\n this.trackComponentRef(properties.instance, td, componentRef);\n }\n\n /**\n * Assigns every renderer parameter as an input on the given component ref.\n *\n * @param componentRef - The component ref whose inputs should be set.\n * @param rendererParameters - The renderer parameters to assign.\n */\n private applyInputs<T>(componentRef: ComponentRef<T>, rendererParameters: BaseRendererParametersObject): void {\n RENDERER_INPUT_KEYS.forEach((key) => {\n componentRef.setInput(key, rendererParameters[key]);\n });\n }\n\n /**\n * Attaches a dynamically created component's view to a specified DOM container element.\n *\n * @param componentRef - The reference to the dynamically created component.\n * @param container - The target DOM element to which the component's root node will be appended.\n */\n private attachComponentToElement<T>(componentRef: ComponentRef<T>, container: HTMLElement): void {\n (componentRef.hostView as EmbeddedViewRef<T>).rootNodes.forEach((node) => {\n container.appendChild(node);\n });\n }\n\n /**\n * Destroys a dynamically created component and detaches its view from the Angular application.\n *\n * Idempotent: a TD recycled after `sweepDetachedViews` already destroyed its ref still maps to that\n * stale ref in `_tdComponentRefs`, so the next render path may reach this with an already-destroyed\n * ref. Guarding on `destroyed` skips the redundant detach/destroy instead of relying on Angular's\n * internal no-op behaviour.\n *\n * @param componentRef - The reference to the component to be destroyed.\n */\n destroyComponent<T>(componentRef: ComponentRef<T>): void {\n const hostView = componentRef.hostView as EmbeddedViewRef<T>;\n if (hostView.destroyed) {\n return;\n }\n this.appRef.detachView(hostView);\n componentRef.destroy();\n }\n\n /**\n * Destroys an embedded view. Idempotent for the same reason as {@link destroyComponent}: a recycled\n * TD can still map to an already-destroyed view in `_tdEmbeddedViews`.\n *\n * @param view - The embedded view to destroy.\n */\n private destroyEmbeddedView(view: EmbeddedViewRef<any>): void {\n if (view.destroyed) {\n return;\n }\n view.destroy();\n }\n}\n","import { ComponentRef, createComponent, EnvironmentInjector, Injectable, NgZone, TemplateRef, Type } from '@angular/core';\nimport { DynamicComponentService } from '../renderer/hot-dynamic-renderer-component.service';\nimport { BaseEditorAdapter } from '../editor/base-editor-adapter';\nimport { GridSettings, GridSettingsInternal } from '../models/grid-settings';\nimport { CustomValidatorFn, ColumnSettings, ColumnSettingsInternal } from '../models/column-settings';\nimport { HotCellRendererComponent } from '../renderer/hot-cell-renderer.component';\nimport { HotCellEditorComponent } from '../editor/hot-cell-editor.component';\nimport Handsontable from 'handsontable/base';\nimport { FactoryEditorAdapter } from '../editor/editor-factory-adapter';\nimport { HotCellRendererAdvancedComponent } from '../renderer/hot-cell-renderer-advanced.component';\nimport { HotCellEditorAdvancedComponent } from '../editor/hot-cell-editor-advanced.component';\n\nconst AVAILABLE_HOOKS_SET = new Set<string>(Handsontable.hooks.getRegistered());\nconst HOT_ZONE_WRAPPED = Symbol('hotZoneWrapped');\n\n/**\n * Service to resolve and apply custom settings for Handsontable settings object.\n */\n@Injectable()\nexport class HotSettingsResolver {\n constructor(\n private readonly dynamicComponentService: DynamicComponentService,\n private readonly environmentInjector: EnvironmentInjector,\n private readonly ngZone: NgZone,\n ) {}\n\n /**\n * Applies custom settings to the provided GridSettings.\n * @param settings The original grid settings.\n * @param previousColumns The previously resolved columns (from the prior settings cycle). When\n * supplied, an editor component already created for a column whose editor type is unchanged is\n * recycled instead of being recreated, avoiding needless Angular component teardown/rebuild.\n * @returns The merged grid settings with custom settings applied.\n */\n applyCustomSettings(settings: GridSettings, previousColumns?: ColumnSettings[]): GridSettingsInternal {\n // Shallow-clone the user settings (and each column) before mutating. Otherwise we would\n // write generated renderers/editors and `_editorComponentReference` straight onto the\n // caller's objects. When the same settings/columns are shared across two <hot-table>\n // instances, the second resolution would overwrite the first instance's editor refs,\n // leaking them and cross-wiring a single editor component between tables.\n const mergedSettings: GridSettings = { ...settings };