@aquera/ngx-smart-table 0.0.15-alpha → 0.0.17-alpha

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3602,6 +3602,72 @@ class NileInputEditor {
3602
3602
  * Custom editor using NileSelect from @aquera/nile-elements
3603
3603
  * This demonstrates how to create dropdown/select editors for ngx-smart-table
3604
3604
  */
3605
+ /**
3606
+ * Inject global styles for nile-select dropdown height limit
3607
+ */
3608
+ let dropdownStylesInjected = false;
3609
+ function injectDropdownStyles() {
3610
+ if (dropdownStylesInjected)
3611
+ return;
3612
+ dropdownStylesInjected = true;
3613
+ const styleId = 'nile-select-dropdown-styles';
3614
+ if (document.getElementById(styleId))
3615
+ return;
3616
+ const style = document.createElement('style');
3617
+ style.id = styleId;
3618
+ style.textContent = `
3619
+ /* Limit nile-select dropdown height and enable scrolling */
3620
+ .nile-select-portal-append {
3621
+ max-height: 300px !important;
3622
+ }
3623
+ .nile-select-portal-append .select__listbox {
3624
+ min-width: 220px !important;
3625
+ max-height: 260px !important;
3626
+ overflow-y: auto !important;
3627
+ width: auto !important;
3628
+ }
3629
+ .nile-select-portal-append .select__footer {
3630
+ height: 35px !important;
3631
+ }
3632
+ /* Prevent option text truncation - target the options container */
3633
+ .nile-select-portal-append .select__options {
3634
+ width: auto !important;
3635
+ }
3636
+ .nile-select-portal-append nile-option {
3637
+ width: auto !important;
3638
+ max-width: none !important;
3639
+ }
3640
+ .nile-select-portal-append nile-option::part(base) {
3641
+ white-space: nowrap !important;
3642
+ text-overflow: clip !important;
3643
+ overflow: visible !important;
3644
+ max-width: none !important;
3645
+ }
3646
+ /* Fix option text truncation by changing flex direction */
3647
+ .nile-select-portal-append nile-option::part(option_label_container) {
3648
+ flex-direction: row !important;
3649
+ }
3650
+ /* Combobox styling using ::part() selector */
3651
+ nile-select.st-cell-editor::part(combobox) {
3652
+ background-color: var(--nile-colors-white-base, var(--ng-colors-bg-primary)) !important;
3653
+ border: solid 1px transparent !important;
3654
+ margin: 1px 4px !important;
3655
+ }
3656
+ nile-select.st-cell-editor::part(combobox):hover {
3657
+ border: solid 1px transparent !important;
3658
+ }
3659
+ .st-cell-editor::part(combobox) {
3660
+ background-color: var(--nile-colors-white-base, var(--ng-colors-bg-primary)) !important;
3661
+ border: solid 1px transparent !important;
3662
+ margin: 1px 4px !important;
3663
+ }
3664
+ /* Search input full width */
3665
+ .nile-select-portal-append .select__search {
3666
+ width: 100% !important;
3667
+ }
3668
+ `;
3669
+ document.head.appendChild(style);
3670
+ }
3605
3671
  /**
3606
3672
  * Custom editor that uses NileSelect component
3607
3673
  * @template T The value type (string for single selection, string[] for multiple)
@@ -3612,9 +3678,7 @@ class NileSelectEditor {
3612
3678
  this.acceptsInitialKeypress = false;
3613
3679
  this.eventListeners = [];
3614
3680
  this.currentOptions = [];
3615
- this.isInitializing = false; // Flag to prevent immediate close during initialization
3616
- this.hasSaved = false; // Flag to prevent double-save
3617
- this.lastSelectedValue = ''; // Track the last selected value
3681
+ this.trackedValues = []; // Track selected values for virtual scroll
3618
3682
  // Handle Observable options
3619
3683
  if (isObservable(options.options)) {
3620
3684
  this.optionsSubscription = options.options.subscribe(opts => {
@@ -3633,101 +3697,20 @@ class NileSelectEditor {
3633
3697
  }
3634
3698
  }
3635
3699
  }
3636
- /**
3637
- * Inject global styles to remove border from nile-select combobox
3638
- * Uses ::part selector which works across shadow DOM boundaries
3639
- */
3640
- injectBorderlessStyles() {
3641
- if (NileSelectEditor.stylesInjected)
3642
- return;
3643
- const styleId = 'nile-select-borderless-styles';
3644
- if (document.getElementById(styleId)) {
3645
- NileSelectEditor.stylesInjected = true;
3646
- return;
3647
- }
3648
- const style = document.createElement('style');
3649
- style.id = styleId;
3650
- style.textContent = `
3651
- .st-cell-editor::part(combobox) {
3652
- border: none !important;
3653
- outline: none !important;
3654
- box-shadow: none !important;
3655
- min-height: unset !important;
3656
- height: 100% !important;
3657
- padding: 0 !important;
3658
- }
3659
- .st-cell-editor::part(combobox):hover {
3660
- border: none !important;
3661
- outline: none !important;
3662
- box-shadow: none !important;
3663
- }
3664
- .st-cell-editor::part(combobox):focus {
3665
- border: none !important;
3666
- outline: none !important;
3667
- box-shadow: none !important;
3668
- }
3669
- // .st-cell-editor::part(tag) {
3670
- // margin: 1px 2px !important;
3671
- // border-radius: 3px !important;
3672
- // background-color: #e2e8f0 !important;
3673
- // font-size: 12px !important;
3674
- // height: auto !important;
3675
- // }
3676
- // .st-cell-editor::part(tag__base) {
3677
- // border: none !important;
3678
- // outline: none !important;
3679
- // padding: 0 2px !important;
3680
- // height: auto !important;
3681
- // }
3682
- // .st-cell-editor::part(tag__content) {
3683
- // padding: 0 !important;
3684
- // }
3685
- .st-cell-editor::part(tags) {
3686
- gap: 2px !important;
3687
- align-items: center !important;
3688
- }
3689
- .st-cell-editor::part(tags-count) {
3690
- margin: 0 !important;
3691
- }
3692
- .st-cell-editor::part(footer) {
3693
- height: 35px !important;
3694
- min-height: 35px !important;
3695
- padding: 8px !important;
3696
- padding-bottom: 30px !important;
3697
- box-sizing: border-box !important;
3698
- }
3699
- .st-cell-editor::part(listbox) {
3700
- margin-bottom: 0 !important;
3701
- padding-bottom: 0 !important;
3702
- }
3703
- `;
3704
- document.head.appendChild(style);
3705
- NileSelectEditor.stylesInjected = true;
3706
- }
3707
3700
  edit(context) {
3708
3701
  if (!context.container) {
3709
3702
  console.warn('NileSelectEditor requires a container element');
3710
3703
  return;
3711
3704
  }
3712
- // Set initializing flag to prevent immediate close
3713
- this.isInitializing = true;
3714
- this.hasSaved = false;
3715
- this.lastSelectedValue = '';
3705
+ // Inject dropdown height styles once
3706
+ injectDropdownStyles();
3716
3707
  // Create NileSelect custom element using document.createElement
3717
3708
  this.select = document.createElement('nile-select');
3718
3709
  this.select.className = 'st-cell-editor';
3719
- // Apply inline styles for proper cell fitting - prevent height changes
3720
- this.select.style.width = 'calc(100% - 4px)';
3721
- this.select.style.height = '100%';
3722
- this.select.style.maxHeight = '100%';
3710
+ // Apply inline styles for proper cell fitting
3711
+ this.select.style.width = '100%';
3712
+ this.select.style.height = 'inherit';
3723
3713
  this.select.style.boxSizing = 'border-box';
3724
- this.select.style.margin = '0 2px';
3725
- this.select.style.overflow = 'hidden';
3726
- // Inject styles to remove border using ::part selector
3727
- this.injectBorderlessStyles();
3728
- // Also try removing outline on host element
3729
- this.select.style.outline = 'none';
3730
- this.select.style.border = 'none';
3731
3714
  // Set initial value
3732
3715
  this.setInitialValue(context.value);
3733
3716
  // Apply all configuration options
@@ -3739,22 +3722,15 @@ class NileSelectEditor {
3739
3722
  // Clear container and append select
3740
3723
  context.container.innerHTML = '';
3741
3724
  context.container.appendChild(this.select);
3742
- // Focus and auto-open after render
3725
+ // Focus the select after render
3743
3726
  setTimeout(() => {
3744
- this.select?.focus();
3745
- // Auto-open the dropdown
3746
- if (this.select) {
3747
- this.select.open = true;
3727
+ try {
3728
+ this.select?.focus();
3748
3729
  }
3749
- // Clear initializing flag after dropdown has opened
3750
- setTimeout(() => {
3751
- this.isInitializing = false;
3752
- // For multi-select, find the portal div for click detection
3753
- if (this.options.multiple) {
3754
- this.portalDiv = document.querySelector('.nile-select-portal-append');
3755
- }
3756
- }, 100);
3757
- }, 0);
3730
+ catch (e) {
3731
+ // Ignore errors
3732
+ }
3733
+ }, 50);
3758
3734
  }
3759
3735
  /**
3760
3736
  * Set the initial value for the select
@@ -3766,17 +3742,21 @@ class NileSelectEditor {
3766
3742
  // Handle multiple selection - value should be array
3767
3743
  if (Array.isArray(value)) {
3768
3744
  this.select.value = value;
3745
+ this.trackedValues = [...value]; // Initialize tracked values
3769
3746
  }
3770
3747
  else if (value) {
3771
3748
  this.select.value = [String(value)];
3749
+ this.trackedValues = [String(value)];
3772
3750
  }
3773
3751
  else {
3774
3752
  this.select.value = [];
3753
+ this.trackedValues = [];
3775
3754
  }
3776
3755
  }
3777
3756
  else {
3778
3757
  // Handle single selection
3779
3758
  this.select.value = value ? String(value) : '';
3759
+ this.trackedValues = value ? [String(value)] : [];
3780
3760
  }
3781
3761
  }
3782
3762
  /**
@@ -3837,13 +3817,9 @@ class NileSelectEditor {
3837
3817
  if (this.options.pill) {
3838
3818
  this.select.pill = this.options.pill;
3839
3819
  }
3840
- if (this.options.hoist !== undefined) {
3820
+ if (this.options.hoist) {
3841
3821
  this.select.hoist = this.options.hoist;
3842
3822
  }
3843
- else {
3844
- // Default hoist to true for all selects to prevent clipping issues in table cells
3845
- this.select.hoist = true;
3846
- }
3847
3823
  if (this.options.placement) {
3848
3824
  this.select.placement = this.options.placement;
3849
3825
  }
@@ -3853,12 +3829,8 @@ class NileSelectEditor {
3853
3829
  if (this.options.disableLocalSearch) {
3854
3830
  this.select.disableLocalSearch = this.options.disableLocalSearch;
3855
3831
  }
3856
- if (this.options.portal !== undefined) {
3857
- this.select.portal = this.options.portal;
3858
- }
3859
- else {
3860
- this.select.portal = true; // Default to portal mode for better positioning
3861
- }
3832
+ // Enable portal mode by default to prevent dropdown clipping in table cells
3833
+ this.select.portal = this.options.portal !== false;
3862
3834
  // Prevent width syncing issues in table cells
3863
3835
  this.select.noWidthSync = true;
3864
3836
  }
@@ -3868,12 +3840,17 @@ class NileSelectEditor {
3868
3840
  updateOptions() {
3869
3841
  if (!this.select)
3870
3842
  return;
3871
- // Clear existing options
3872
- while (this.select.firstChild) {
3873
- this.select.removeChild(this.select.firstChild);
3843
+ // For virtual scroll mode, just update the data property (no DOM children to clear)
3844
+ if (this.options.enableVirtualScroll) {
3845
+ this.createAndAppendOptions(this.currentOptions);
3846
+ }
3847
+ else {
3848
+ // Standard mode: clear existing nile-option elements first
3849
+ while (this.select.firstChild) {
3850
+ this.select.removeChild(this.select.firstChild);
3851
+ }
3852
+ this.createAndAppendOptions(this.currentOptions);
3874
3853
  }
3875
- // Re-append with new options
3876
- this.createAndAppendOptions(this.currentOptions);
3877
3854
  // Trigger update on the web component
3878
3855
  if (this.select.requestUpdate) {
3879
3856
  this.select.requestUpdate();
@@ -3881,11 +3858,22 @@ class NileSelectEditor {
3881
3858
  }
3882
3859
  /**
3883
3860
  * Create NileOption elements and append them to the select
3861
+ * When enableVirtualScroll is true, uses data property instead of DOM elements
3862
+ * @see https://nile.aqueralabs.com/select-virtual?theme=enterprise
3884
3863
  * @param options - The options to render
3885
3864
  */
3886
3865
  createAndAppendOptions(options) {
3887
3866
  if (!this.select)
3888
3867
  return;
3868
+ // Virtual scroll mode: use data property instead of DOM elements
3869
+ if (this.options.enableVirtualScroll) {
3870
+ const virtualData = options.map(opt => typeof opt === 'string'
3871
+ ? { value: opt, label: opt }
3872
+ : { value: opt.value, label: opt.label, disabled: opt.disabled });
3873
+ this.select.data = virtualData;
3874
+ return;
3875
+ }
3876
+ // Standard mode: create nile-option elements
3889
3877
  options.forEach(opt => {
3890
3878
  const nileOption = document.createElement('nile-option');
3891
3879
  // Auto-detect format: string or SelectOption object
@@ -3918,30 +3906,6 @@ class NileSelectEditor {
3918
3906
  if (!this.select)
3919
3907
  return;
3920
3908
  const validateOnSave = this.options.validateOnSave !== false;
3921
- // Prevent click events from bubbling up to the cell container
3922
- const selectClickHandler = (e) => {
3923
- e.stopPropagation();
3924
- };
3925
- this.select.addEventListener('click', selectClickHandler);
3926
- this.eventListeners.push({ event: 'click', handler: selectClickHandler });
3927
- // For single-select with portal, listen to document mousedown to detect option selection
3928
- // Use mousedown because it fires before blur
3929
- if (!this.options.multiple) {
3930
- const documentMouseDownHandler = (e) => {
3931
- const composedPath = e.composedPath?.() || [];
3932
- // Look for nile-option in the event path
3933
- for (const el of composedPath) {
3934
- if (el instanceof HTMLElement && el.tagName?.toLowerCase() === 'nile-option') {
3935
- const optionValue = el.getAttribute('value') || el.value || '';
3936
- this.lastSelectedValue = optionValue;
3937
- return;
3938
- }
3939
- }
3940
- };
3941
- // Add listener immediately (mousedown fires before blur, so no need for delay)
3942
- document.addEventListener('mousedown', documentMouseDownHandler, true); // capture phase
3943
- this.eventListeners.push({ event: 'document-mousedown', handler: documentMouseDownHandler });
3944
- }
3945
3909
  // Handle keyboard events (keydown is not replaced by custom events)
3946
3910
  const keydownHandler = (e) => {
3947
3911
  const keyEvent = e;
@@ -3953,45 +3917,100 @@ class NileSelectEditor {
3953
3917
  };
3954
3918
  this.select.addEventListener('keydown', keydownHandler);
3955
3919
  this.eventListeners.push({ event: 'keydown', handler: keydownHandler });
3956
- // Use nile-blur event as save trigger for single-select
3957
- // Add a small delay to allow the value to update before reading
3958
- const singleSelectSaveHandler = (e) => {
3959
- // Don't save again if already saved
3960
- // Ignore events that fire during initialization (e.g. when setting initial value)
3961
- if (this.hasSaved || this.isInitializing) {
3962
- return;
3963
- }
3964
- // Only save for single-select
3965
- if (!this.options.multiple) {
3966
- // Add a small delay to let the value update after selection
3967
- setTimeout(() => {
3968
- // Check again after timeout in case saved by another handler
3969
- if (this.hasSaved) {
3920
+ // Use nile-change custom event as save trigger - ONLY for single-select
3921
+ // Multi-select should stay open until user clicks outside
3922
+ if (!this.options.multiple) {
3923
+ const changeHandler = (e) => {
3924
+ const customEvent = e;
3925
+ try {
3926
+ if (validateOnSave && this.select && !this.select.checkValidity()) {
3927
+ this.select.reportValidity();
3970
3928
  return;
3971
3929
  }
3972
- if (validateOnSave && !this.select?.checkValidity()) {
3973
- this.select?.reportValidity();
3974
- return;
3930
+ }
3931
+ catch (err) {
3932
+ // Ignore validation errors - nile-select internals may not be ready
3933
+ }
3934
+ context.onSave(this.parseValue(customEvent.detail.value));
3935
+ };
3936
+ this.select.addEventListener('nile-change', changeHandler);
3937
+ this.eventListeners.push({ event: 'nile-change', handler: changeHandler });
3938
+ }
3939
+ // For multi-select, track value changes (needed for virtual scroll where value property doesn't update)
3940
+ if (this.options.multiple) {
3941
+ const trackValueHandler = (e) => {
3942
+ const customEvent = e;
3943
+ const newValue = customEvent.detail?.value;
3944
+ if (newValue !== undefined) {
3945
+ if (Array.isArray(newValue)) {
3946
+ this.trackedValues = [...newValue];
3947
+ }
3948
+ else {
3949
+ this.trackedValues = newValue ? [newValue] : [];
3975
3950
  }
3976
- // Use lastSelectedValue if available, otherwise get from element
3977
- const target = e.target;
3978
- const value = this.lastSelectedValue || target?.value || this.select?.value || '';
3979
- this.hasSaved = true;
3980
- context.onSave(this.parseValue(value));
3981
- }, 50); // 50ms delay to allow value to update
3951
+ }
3952
+ };
3953
+ this.select.addEventListener('nile-change', trackValueHandler);
3954
+ this.eventListeners.push({ event: 'nile-change', handler: trackValueHandler });
3955
+ }
3956
+ // Track if save was already triggered (to prevent double save)
3957
+ let saveTriggered = false;
3958
+ // Use mousedown on document to detect clicks outside the select and portal
3959
+ const documentMousedownHandler = (e) => {
3960
+ if (saveTriggered)
3961
+ return;
3962
+ if (!this.select)
3963
+ return;
3964
+ // Use composedPath to traverse shadow DOM boundaries
3965
+ const path = e.composedPath();
3966
+ // Check if click is inside the select element or any nile-select related elements
3967
+ for (const element of path) {
3968
+ if (!(element instanceof HTMLElement))
3969
+ continue;
3970
+ // Check if it's our select element
3971
+ if (element === this.select) {
3972
+ return;
3973
+ }
3974
+ // Check for nile-select portals (various class names)
3975
+ if (element.classList?.contains('nile-select-portal-append') ||
3976
+ element.classList?.contains('select__listbox') ||
3977
+ element.classList?.contains('select__options') ||
3978
+ element.tagName?.toLowerCase() === 'nile-option' ||
3979
+ element.tagName?.toLowerCase() === 'nile-select') {
3980
+ return;
3981
+ }
3982
+ // Check for any element with nile-select in its class
3983
+ if (element.className && typeof element.className === 'string' &&
3984
+ element.className.includes('nile-select')) {
3985
+ return;
3986
+ }
3982
3987
  }
3988
+ // Also check portal containers directly
3989
+ const portals = document.querySelectorAll('.nile-select-portal-append, [class*="select__listbox"]');
3990
+ for (let i = 0; i < portals.length; i++) {
3991
+ if (portals[i].contains(e.target)) {
3992
+ return;
3993
+ }
3994
+ }
3995
+ // Click is outside - save and exit
3996
+ saveTriggered = true;
3997
+ try {
3998
+ if (validateOnSave && this.select && !this.select.checkValidity()) {
3999
+ this.select.reportValidity();
4000
+ return;
4001
+ }
4002
+ }
4003
+ catch (err) {
4004
+ // Ignore validation errors - nile-select internals may not be ready
4005
+ }
4006
+ context.onSave(this.getCurrentValue());
3983
4007
  };
3984
- // Listen to blur and nile-blur events for focus loss detection
3985
- // Nile web components emit nile-blur custom event instead of native blur
3986
- this.select.addEventListener('blur', singleSelectSaveHandler);
3987
- this.eventListeners.push({ event: 'blur', handler: singleSelectSaveHandler });
3988
- this.select.addEventListener('nile-blur', singleSelectSaveHandler);
3989
- this.eventListeners.push({ event: 'nile-blur', handler: singleSelectSaveHandler });
3990
- // Also listen to change events as fallback
3991
- this.select.addEventListener('nile-change', singleSelectSaveHandler);
3992
- this.eventListeners.push({ event: 'nile-change', handler: singleSelectSaveHandler });
3993
- this.select.addEventListener('change', singleSelectSaveHandler);
3994
- this.eventListeners.push({ event: 'change', handler: singleSelectSaveHandler });
4008
+ // Add listener after a short delay to avoid capturing the initial click that opened the editor
4009
+ setTimeout(() => {
4010
+ document.addEventListener('mousedown', documentMousedownHandler, true);
4011
+ }, 50);
4012
+ // Store for cleanup
4013
+ this._documentMousedownHandler = documentMousedownHandler;
3995
4014
  // Handle clear button if enabled
3996
4015
  if (this.options.clearable) {
3997
4016
  const clearHandler = (e) => {
@@ -4010,43 +4029,6 @@ class NileSelectEditor {
4010
4029
  this.select.addEventListener('nile-search', searchHandler);
4011
4030
  this.eventListeners.push({ event: 'nile-search', handler: searchHandler });
4012
4031
  }
4013
- // For multi-select: detect clicks outside portal and select
4014
- if (this.options.multiple) {
4015
- const documentClickHandler = (e) => {
4016
- const mouseEvent = e;
4017
- const target = mouseEvent.target;
4018
- // Check if portal div exists (might take a moment to render)
4019
- if (!this.portalDiv) {
4020
- this.portalDiv = document.querySelector('.nile-select-portal-append');
4021
- }
4022
- // Determine if click is inside the select or portal
4023
- const clickedInsideSelect = this.select?.contains(target);
4024
- const clickedInsidePortal = this.portalDiv?.contains(target);
4025
- // If clicked outside both select and portal, save and close
4026
- if (!clickedInsideSelect && !clickedInsidePortal) {
4027
- if (validateOnSave && !this.select?.checkValidity()) {
4028
- this.select?.reportValidity();
4029
- return;
4030
- }
4031
- context.onSave(this.getCurrentValue());
4032
- }
4033
- // Otherwise, click is inside - do nothing (dropdown stays open)
4034
- };
4035
- // Delay adding the document click handler to prevent the initial click
4036
- // (that opened the editor) from immediately triggering it and closing the dropdown
4037
- // Use 200ms to ensure the dropdown has fully opened
4038
- setTimeout(() => {
4039
- // Only add if select still exists (editor not destroyed)
4040
- if (this.select) {
4041
- // Use bubbling phase (false) instead of capture to not interfere with nile-select's toggle
4042
- document.addEventListener('click', documentClickHandler, false);
4043
- this.eventListeners.push({
4044
- event: 'document-click',
4045
- handler: documentClickHandler
4046
- });
4047
- }
4048
- }, 200);
4049
- }
4050
4032
  }
4051
4033
  /**
4052
4034
  * Parse value based on selection mode
@@ -4066,54 +4048,52 @@ class NileSelectEditor {
4066
4048
  return value;
4067
4049
  }
4068
4050
  destroy() {
4069
- // Reset flags
4070
- this.isInitializing = false;
4071
- this.hasSaved = false;
4072
- this.lastSelectedValue = '';
4073
4051
  // Unsubscribe from options Observable
4074
4052
  if (this.optionsSubscription) {
4075
4053
  this.optionsSubscription.unsubscribe();
4076
4054
  this.optionsSubscription = undefined;
4077
4055
  }
4056
+ // Remove document mousedown handler if exists
4057
+ if (this._documentMousedownHandler) {
4058
+ document.removeEventListener('mousedown', this._documentMousedownHandler, true);
4059
+ delete this._documentMousedownHandler;
4060
+ }
4078
4061
  // Remove all event listeners
4079
4062
  if (this.select) {
4080
4063
  this.eventListeners.forEach(({ event, handler }) => {
4081
- if (event === 'document-click') {
4082
- // Remove from document for multi-select click handler (bubbling phase)
4083
- document.removeEventListener('click', handler, false);
4084
- }
4085
- else if (event === 'document-mousedown') {
4086
- // Remove from document for single-select option mousedown handler (capture phase)
4087
- document.removeEventListener('mousedown', handler, true);
4088
- }
4089
- else {
4090
- // Remove from select element
4091
- this.select?.removeEventListener(event, handler);
4092
- }
4064
+ this.select?.removeEventListener(event, handler);
4093
4065
  });
4094
4066
  this.eventListeners = [];
4095
4067
  this.select.remove();
4096
4068
  this.select = undefined;
4097
- this.portalDiv = undefined; // Clean up portal reference
4098
4069
  }
4099
4070
  }
4100
4071
  focus() {
4101
- this.select?.focus();
4072
+ try {
4073
+ this.select?.focus();
4074
+ }
4075
+ catch (e) {
4076
+ // Ignore focus errors - nile-select internals may not be ready
4077
+ }
4102
4078
  }
4103
4079
  getCurrentValue() {
4104
4080
  if (!this.select) {
4105
4081
  return (this.options.multiple ? [] : '');
4106
4082
  }
4107
- const value = this.select.value;
4108
- // Handle multiple selection
4083
+ // For multi-select, prefer tracked values (more reliable for virtual scroll)
4109
4084
  if (this.options.multiple) {
4110
- return (Array.isArray(value) ? value : [value]);
4085
+ // Use tracked values if available, otherwise try component value
4086
+ let values = this.trackedValues.length > 0 ? this.trackedValues : this.select.value;
4087
+ if (Array.isArray(values)) {
4088
+ return values.filter((v) => v !== undefined && v !== null && v !== '');
4089
+ }
4090
+ return (values ? [values] : []);
4111
4091
  }
4112
- // Handle single selection
4113
- return (Array.isArray(value) ? (value.length > 0 ? value[0] : '') : value);
4092
+ // For single-select, use tracked values or component value
4093
+ let value = this.trackedValues.length > 0 ? this.trackedValues[0] : this.select.value;
4094
+ return (Array.isArray(value) ? (value.length > 0 ? value[0] : '') : (value || ''));
4114
4095
  }
4115
4096
  }
4116
- NileSelectEditor.stylesInjected = false;
4117
4097
 
4118
4098
  /**
4119
4099
  * Custom editor using NileAutoComplete from @aquera/nile-elements
@@ -8828,6 +8808,15 @@ class StTableComponent {
8828
8808
  focusTdElement(rowIndex, colIndex) {
8829
8809
  // Use setTimeout to ensure DOM has updated
8830
8810
  setTimeout(() => {
8811
+ // Don't steal focus from editors - if a cell is being edited,
8812
+ // the editor should keep focus
8813
+ const editingPosition = this.getActiveTableState().getEditingPosition();
8814
+ if (editingPosition &&
8815
+ editingPosition.rowIndex === rowIndex &&
8816
+ editingPosition.columnIndex === colIndex) {
8817
+ // Cell is being edited, don't steal focus from the editor
8818
+ return;
8819
+ }
8831
8820
  // Use absolute row index for virtual scroll compatibility
8832
8821
  const absoluteRowIndex = this.getAbsoluteRowIndex(rowIndex);
8833
8822
  // Query within this component's host element to avoid selecting rows from other tables
@@ -9088,7 +9077,21 @@ class StTableComponent {
9088
9077
  return;
9089
9078
  const target = event.target;
9090
9079
  const tableElement = target.closest('.st-table');
9091
- if (!tableElement) {
9080
+ // Also check if click is inside a portal (e.g., nile-select dropdown)
9081
+ // These are rendered outside the table but are part of the editing experience
9082
+ const portalElement = target.closest('.nile-select-portal-append');
9083
+ // Check composedPath for shadow DOM elements (e.g., nile-option, nile-input inside shadow root)
9084
+ const composedPath = event.composedPath?.() || [];
9085
+ const isInsideNileElement = composedPath.some((el) => {
9086
+ if (el instanceof HTMLElement) {
9087
+ const tagName = el.tagName?.toLowerCase();
9088
+ // Check for any nile-* custom elements used in editors
9089
+ return tagName?.startsWith('nile-') ||
9090
+ el.classList?.contains('nile-select-portal-append');
9091
+ }
9092
+ return false;
9093
+ });
9094
+ if (!tableElement && !portalElement && !isInsideNileElement) {
9092
9095
  this.getActiveTableState().clearFocus();
9093
9096
  this.getActiveTableState().clearEditingCell();
9094
9097
  }
@@ -10046,9 +10049,13 @@ class StWorkbookComponent {
10046
10049
  this.workbookActionsOpen = false;
10047
10050
  this.workbookActionsPosition = {};
10048
10051
  /**
10049
- * Visible workbook actions (filtered by hidden)
10052
+ * Visible workbook actions for dropdown (filtered by hidden and showInToolbar)
10050
10053
  */
10051
10054
  this.visibleWorkbookActions = [];
10055
+ /**
10056
+ * Workbook actions to show directly in toolbar
10057
+ */
10058
+ this.toolbarWorkbookActions = [];
10052
10059
  /**
10053
10060
  * Destroy subject for cleanup
10054
10061
  */
@@ -10285,14 +10292,18 @@ class StWorkbookComponent {
10285
10292
  updateVisibleWorkbookActions() {
10286
10293
  if (!this.config.workbookActions) {
10287
10294
  this.visibleWorkbookActions = [];
10295
+ this.toolbarWorkbookActions = [];
10288
10296
  return;
10289
10297
  }
10290
- this.visibleWorkbookActions = this.config.workbookActions.filter(action => {
10298
+ const visibleActions = this.config.workbookActions.filter(action => {
10291
10299
  if (typeof action.hidden === 'function') {
10292
10300
  return !action.hidden();
10293
10301
  }
10294
10302
  return !action.hidden;
10295
10303
  });
10304
+ // Separate toolbar actions from dropdown actions
10305
+ this.toolbarWorkbookActions = visibleActions.filter(action => action.showInToolbar === true);
10306
+ this.visibleWorkbookActions = visibleActions.filter(action => action.showInToolbar !== true);
10296
10307
  }
10297
10308
  /**
10298
10309
  * Check if workbook action is disabled
@@ -10489,10 +10500,10 @@ class StWorkbookComponent {
10489
10500
  }
10490
10501
  }
10491
10502
  StWorkbookComponent.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: StWorkbookComponent, deps: [{ token: AutosaveService }, { token: i0.ChangeDetectorRef }], target: i0.ɵɵFactoryTarget.Component });
10492
- StWorkbookComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: StWorkbookComponent, selector: "st-workbook", inputs: { config: "config", sheetsData: "sheetsData", state: "state" }, outputs: { sheetChanged: "sheetChanged", addSheet: "addSheet", sheetTabAction: "sheetTabAction", workbookAction: "workbookAction", cellChange: "cellChange", cellSave: "cellSave", tableStateChange: "tableStateChange", fullscreenToggle: "fullscreenToggle", requestAddRow: "requestAddRow" }, viewQueries: [{ propertyName: "tableComponent", first: true, predicate: StTableComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"workbook-container\" [class.fullscreen]=\"isFullscreen$ | async\">\n <nile-tab-group [activeIndex]=\"activeSheetIndex\">\n \n <!-- Sheet Tabs (one per sheet) -->\n <nile-tab *ngFor=\"let sheet of sheets; let i = index\"\n slot=\"nav\" \n panel=\"shared-panel\"\n [class.active]=\"i === activeSheetIndex\"\n (click)=\"onTabChange(i)\">\n <div class=\"sheet-tab-content\">\n <span class=\"sheet-name\">{{ sheet.name }}</span>\n \n <!-- Tab actions dropdown button -->\n <button class=\"tab-actions-button\"\n (click)=\"openTabActions($event, sheet, i)\"\n *ngIf=\"hasTabActions(sheet)\">\n <nile-icon name=\"arrowdown\" size=\"14\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Toolbar Tab (for workbook controls) -->\n <nile-tab slot=\"nav\" \n panel=\"shared-panel\"\n class=\"workbook-toolbar-tab\"\n [disabled]=\"true\">\n <div class=\"workbook-toolbar-content\">\n <!-- Autosave Indicator -->\n <div class=\"autosave-indicator\" *ngIf=\"autosaveEnabled\">\n <nile-icon \n *ngIf=\"!isSaving && lastSaveTime\" \n name=\"save\" \n size=\"14\"\n [title]=\"'Saved at ' + (lastSaveTime | date:'HH:mm:ss')\">\n </nile-icon>\n <nile-icon \n *ngIf=\"isSaving\" \n name=\"loader\" \n size=\"14\"\n title=\"Saving...\">\n </nile-icon>\n </div>\n\n <!-- Workbook Actions Dropdown -->\n <button class=\"workbook-actions-button\"\n *ngIf=\"config.workbookActions && config.workbookActions.length > 0\"\n (click)=\"toggleWorkbookActions($event)\"\n title=\"Workbook Actions\">\n <nile-icon name=\"settings\"></nile-icon>\n </button>\n \n <!-- Add Sheet Button -->\n <button class=\"add-sheet-button\"\n *ngIf=\"canAddSheet\"\n (click)=\"onAddSheet()\"\n title=\"Add Sheet\">\n <nile-icon name=\"plus\"></nile-icon>\n </button>\n \n <!-- Fullscreen Button -->\n <button class=\"fullscreen-button\"\n *ngIf=\"config.display?.allowFullscreen !== false\"\n (click)=\"toggleFullscreen()\"\n [title]=\"(isFullscreen$ | async) ? 'Exit Fullscreen' : 'Fullscreen'\">\n <nile-icon [name]=\"(isFullscreen$ | async) ? 'collapse' : 'expand-06'\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Single Shared Tab Panel -->\n <nile-tab-panel name=\"shared-panel\">\n <!-- Lazy loading strategy: table is destroyed and recreated with new config/state when sheet changes -->\n <!-- Using ngFor with trackBy to force complete reinitialization when tableComponentKey changes -->\n <ng-container *ngFor=\"let key of [tableComponentKey]; trackBy: trackByKey\">\n <st-table *ngIf=\"currentTableConfig && currentTableState\"\n [attr.data-sheet-key]=\"key\"\n [tableConfig]=\"currentTableConfig\"\n [data$]=\"currentTableData$\"\n [tableState]=\"currentTableState\"\n (cellChange)=\"onCellChange($event)\"\n (cellSave)=\"onCellSave($event)\"\n (stateChange)=\"onTableStateChange($event)\"\n (requestAddRow)=\"onRequestAddRow($event)\">\n </st-table>\n </ng-container>\n </nile-tab-panel>\n \n </nile-tab-group>\n</div>\n\n<!-- Tab Actions Dropdown -->\n<div class=\"tab-actions-dropdown\" \n *ngIf=\"tabActionsOpen\"\n [ngStyle]=\"tabActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeTabActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of selectedSheet?.tabActions\" \n (click)=\"onTabActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Workbook Actions Dropdown -->\n<div class=\"workbook-actions-dropdown\"\n *ngIf=\"workbookActionsOpen\"\n [ngStyle]=\"workbookActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeWorkbookActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of visibleWorkbookActions\"\n [class.disabled]=\"isActionDisabled(action)\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Fullscreen Backdrop -->\n<div class=\"fullscreen-backdrop\" \n *ngIf=\"isFullscreen$ | async\"\n (click)=\"toggleFullscreen()\">\n</div>\n\n", styles: ["@import\"@aquera/nile/lib/styles/variables.css\";:host{display:block;width:100%;height:100%}.workbook-container{display:flex;flex-direction:column;height:100%;background:white;border:1px solid var(--nile-color-neutral-200);border-radius:4px;overflow:hidden}.workbook-container.fullscreen{position:fixed;inset:0;z-index:2000;border:none;border-radius:0}.workbook-container nile-tab-group{height:100%;display:flex;flex-direction:column}.sheet-tab-content{display:flex;align-items:center;gap:8px;padding:0 4px}.sheet-tab-content .sheet-name{font-size:12px;font-weight:500;font-family:var(--nile-font-family-sans-serif);color:#000}.sheet-tab-content .tab-actions-button{width:20px;height:20px;padding:0;background:transparent;border:none;cursor:pointer}.workbook-toolbar-tab{margin-left:auto!important;pointer-events:auto!important;border-left:1px solid var(--nile-color-neutral-200)}.workbook-toolbar-tab .workbook-toolbar-content{display:flex;gap:4px;align-items:center;padding:0 8px}.workbook-toolbar-tab .workbook-toolbar-content .autosave-indicator{display:flex;align-items:center;margin-right:8px;color:var(--nile-color-success-500)}.workbook-toolbar-tab button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s;color:var(--nile-color-neutral-600)}.workbook-toolbar-tab button:hover{background-color:var(--nile-color-neutral-100);color:var(--nile-color-neutral-900)}.workbook-toolbar-tab button:active{background-color:var(--nile-color-neutral-200)}.workbook-toolbar-tab button nile-icon{font-size:16px}.tab-actions-dropdown{position:fixed;z-index:1001}.tab-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.tab-actions-dropdown .dropdown-menu{position:relative;min-width:180px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.tab-actions-dropdown .dropdown-menu nile-menu{display:block}.workbook-actions-dropdown{position:fixed;z-index:1001}.workbook-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.workbook-actions-dropdown .dropdown-menu{position:relative;min-width:200px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.workbook-actions-dropdown .dropdown-menu nile-menu{display:block}.fullscreen-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1999;cursor:pointer}nile-tab-group::part(nav){background-color:#fafafa}nile-tab-group::part(active-tab-indicator-path){background:none}nile-tab-group::part(active-tab-indicator){border-bottom:none}nile-tab-group::part(tabs){gap:0}nile-tab-group nile-tab{border:1px solid #e0e0e0;border-top-left-radius:6px;border-top-right-radius:6px}nile-tab-group nile-tab::part(base){padding-left:.5rem;padding-top:.5rem;padding-bottom:.5rem;border-bottom-left-radius:0;border-bottom-right-radius:0}nile-tab-group nile-tab.active{background-color:#fff;color:#000}nile-tab-group nile-tab.active .sheet-name{font-weight:600}nile-tab-group nile-tab-panel::part(base){padding:0;max-height:30rem}\n"], components: [{ type: StTableComponent, selector: "st-table", inputs: ["tableConfig", "data", "data$", "tableState", "enableSorting", "enableFiltering", "validateConfig"], outputs: ["stateChange", "dataChange", "cellEdit", "cellSave", "cellCancel", "cellChange", "columnResized", "columnMoved", "configValidationErrors", "columnAdded", "rowAction", "validationStateChange", "requestAddRow"] }], directives: [{ type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], pipes: { "async": i1.AsyncPipe, "date": i1.DatePipe } });
10503
+ StWorkbookComponent.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "12.0.0", version: "13.4.0", type: StWorkbookComponent, selector: "st-workbook", inputs: { config: "config", sheetsData: "sheetsData", state: "state" }, outputs: { sheetChanged: "sheetChanged", addSheet: "addSheet", sheetTabAction: "sheetTabAction", workbookAction: "workbookAction", cellChange: "cellChange", cellSave: "cellSave", tableStateChange: "tableStateChange", fullscreenToggle: "fullscreenToggle", requestAddRow: "requestAddRow" }, viewQueries: [{ propertyName: "tableComponent", first: true, predicate: StTableComponent, descendants: true }], usesOnChanges: true, ngImport: i0, template: "<div class=\"workbook-container\" [class.fullscreen]=\"isFullscreen$ | async\">\n <nile-tab-group [activeIndex]=\"activeSheetIndex\">\n \n <!-- Sheet Tabs (one per sheet) -->\n <nile-tab *ngFor=\"let sheet of sheets; let i = index\"\n slot=\"nav\" \n panel=\"shared-panel\"\n [class.active]=\"i === activeSheetIndex\"\n (click)=\"onTabChange(i)\">\n <div class=\"sheet-tab-content\">\n <span class=\"sheet-name\">{{ sheet.name }}</span>\n \n <!-- Tab actions dropdown button -->\n <button class=\"tab-actions-button\"\n (click)=\"openTabActions($event, sheet, i)\"\n *ngIf=\"hasTabActions(sheet)\">\n <nile-icon name=\"arrowdown\" size=\"14\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Toolbar Tab (for workbook controls) -->\n <nile-tab slot=\"nav\" \n panel=\"shared-panel\"\n class=\"workbook-toolbar-tab\"\n [disabled]=\"true\">\n <div class=\"workbook-toolbar-content\">\n <!-- Autosave Indicator -->\n <div class=\"autosave-indicator\" *ngIf=\"autosaveEnabled\">\n <nile-icon \n *ngIf=\"!isSaving && lastSaveTime\" \n name=\"save\" \n size=\"14\"\n [title]=\"'Saved at ' + (lastSaveTime | date:'HH:mm:ss')\">\n </nile-icon>\n <nile-icon \n *ngIf=\"isSaving\" \n name=\"loader\" \n size=\"14\"\n title=\"Saving...\">\n </nile-icon>\n </div>\n\n <!-- Toolbar Workbook Actions (shown as individual buttons) -->\n <button *ngFor=\"let action of toolbarWorkbookActions\"\n class=\"toolbar-action-button\"\n [class.disabled]=\"isActionDisabled(action)\"\n [title]=\"action.label\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" [name]=\"action.icon\"></nile-icon>\n <span *ngIf=\"!action.icon\">{{ action.label }}</span>\n </button>\n\n <!-- Workbook Actions Dropdown -->\n <button class=\"workbook-actions-button\"\n *ngIf=\"visibleWorkbookActions.length > 0\"\n (click)=\"toggleWorkbookActions($event)\"\n title=\"Workbook Actions\">\n <nile-icon name=\"settings\"></nile-icon>\n </button>\n \n <!-- Add Sheet Button -->\n <button class=\"add-sheet-button\"\n *ngIf=\"canAddSheet\"\n (click)=\"onAddSheet()\"\n title=\"Add Sheet\">\n <nile-icon name=\"plus\"></nile-icon>\n </button>\n \n <!-- Fullscreen Button -->\n <button class=\"fullscreen-button\"\n *ngIf=\"config.display?.allowFullscreen !== false\"\n (click)=\"toggleFullscreen()\"\n [title]=\"(isFullscreen$ | async) ? 'Exit Fullscreen' : 'Fullscreen'\">\n <nile-icon [name]=\"(isFullscreen$ | async) ? 'collapse' : 'expand-06'\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Single Shared Tab Panel -->\n <nile-tab-panel name=\"shared-panel\">\n <!-- Lazy loading strategy: table is destroyed and recreated with new config/state when sheet changes -->\n <!-- Using ngFor with trackBy to force complete reinitialization when tableComponentKey changes -->\n <ng-container *ngFor=\"let key of [tableComponentKey]; trackBy: trackByKey\">\n <st-table *ngIf=\"currentTableConfig && currentTableState\"\n [attr.data-sheet-key]=\"key\"\n [tableConfig]=\"currentTableConfig\"\n [data$]=\"currentTableData$\"\n [tableState]=\"currentTableState\"\n (cellChange)=\"onCellChange($event)\"\n (cellSave)=\"onCellSave($event)\"\n (stateChange)=\"onTableStateChange($event)\"\n (requestAddRow)=\"onRequestAddRow($event)\">\n </st-table>\n </ng-container>\n </nile-tab-panel>\n \n </nile-tab-group>\n</div>\n\n<!-- Tab Actions Dropdown -->\n<div class=\"tab-actions-dropdown\" \n *ngIf=\"tabActionsOpen\"\n [ngStyle]=\"tabActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeTabActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of selectedSheet?.tabActions\" \n (click)=\"onTabActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Workbook Actions Dropdown -->\n<div class=\"workbook-actions-dropdown\"\n *ngIf=\"workbookActionsOpen\"\n [ngStyle]=\"workbookActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeWorkbookActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of visibleWorkbookActions\"\n [class.disabled]=\"isActionDisabled(action)\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Fullscreen Backdrop -->\n<div class=\"fullscreen-backdrop\" \n *ngIf=\"isFullscreen$ | async\"\n (click)=\"toggleFullscreen()\">\n</div>\n\n", styles: ["@import\"@aquera/nile/lib/styles/variables.css\";:host{display:block;width:100%;height:100%}.workbook-container{display:flex;flex-direction:column;height:100%;background:white;border:1px solid var(--nile-color-neutral-200);border-radius:4px;overflow:hidden}.workbook-container.fullscreen{position:fixed;inset:0;z-index:2000;border:none;border-radius:0}.workbook-container nile-tab-group{height:100%;display:flex;flex-direction:column}.sheet-tab-content{display:flex;align-items:center;gap:8px;padding:0 4px}.sheet-tab-content .sheet-name{font-size:12px;font-weight:500;font-family:var(--nile-font-family-sans-serif);color:#000}.sheet-tab-content .tab-actions-button{width:20px;height:20px;padding:0;background:transparent;border:none;cursor:pointer}.workbook-toolbar-tab{margin-left:auto!important;pointer-events:auto!important;border-left:1px solid var(--nile-color-neutral-200)}.workbook-toolbar-tab .workbook-toolbar-content{display:flex;gap:4px;align-items:center;padding:0 8px}.workbook-toolbar-tab .workbook-toolbar-content .autosave-indicator{display:flex;align-items:center;margin-right:8px;color:var(--nile-color-success-500)}.workbook-toolbar-tab button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s;color:var(--nile-color-neutral-600)}.workbook-toolbar-tab button:hover{background-color:var(--nile-color-neutral-100);color:var(--nile-color-neutral-900)}.workbook-toolbar-tab button:active{background-color:var(--nile-color-neutral-200)}.workbook-toolbar-tab button nile-icon{font-size:16px}.workbook-toolbar-tab button.disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.tab-actions-dropdown{position:fixed;z-index:1001}.tab-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.tab-actions-dropdown .dropdown-menu{position:relative;min-width:180px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.tab-actions-dropdown .dropdown-menu nile-menu{display:block}.workbook-actions-dropdown{position:fixed;z-index:1001}.workbook-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.workbook-actions-dropdown .dropdown-menu{position:relative;min-width:200px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.workbook-actions-dropdown .dropdown-menu nile-menu{display:block}.fullscreen-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1999;cursor:pointer}nile-tab-group::part(nav){background-color:#fafafa}nile-tab-group::part(active-tab-indicator-path){background:none}nile-tab-group::part(active-tab-indicator){border-bottom:none}nile-tab-group::part(tabs){gap:0}nile-tab-group nile-tab{border:1px solid #e0e0e0;border-top-left-radius:6px;border-top-right-radius:6px}nile-tab-group nile-tab::part(base){padding-left:.5rem;padding-top:.5rem;padding-bottom:.5rem;border-bottom-left-radius:0;border-bottom-right-radius:0}nile-tab-group nile-tab.active{background-color:#fff;color:#000}nile-tab-group nile-tab.active .sheet-name{font-weight:600}nile-tab-group nile-tab-panel::part(base){padding:0;max-height:30rem}\n"], components: [{ type: StTableComponent, selector: "st-table", inputs: ["tableConfig", "data", "data$", "tableState", "enableSorting", "enableFiltering", "validateConfig"], outputs: ["stateChange", "dataChange", "cellEdit", "cellSave", "cellCancel", "cellChange", "columnResized", "columnMoved", "configValidationErrors", "columnAdded", "rowAction", "validationStateChange", "requestAddRow"] }], directives: [{ type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { type: i1.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }], pipes: { "async": i1.AsyncPipe, "date": i1.DatePipe } });
10493
10504
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "13.4.0", ngImport: i0, type: StWorkbookComponent, decorators: [{
10494
10505
  type: Component,
10495
- args: [{ selector: 'st-workbook', template: "<div class=\"workbook-container\" [class.fullscreen]=\"isFullscreen$ | async\">\n <nile-tab-group [activeIndex]=\"activeSheetIndex\">\n \n <!-- Sheet Tabs (one per sheet) -->\n <nile-tab *ngFor=\"let sheet of sheets; let i = index\"\n slot=\"nav\" \n panel=\"shared-panel\"\n [class.active]=\"i === activeSheetIndex\"\n (click)=\"onTabChange(i)\">\n <div class=\"sheet-tab-content\">\n <span class=\"sheet-name\">{{ sheet.name }}</span>\n \n <!-- Tab actions dropdown button -->\n <button class=\"tab-actions-button\"\n (click)=\"openTabActions($event, sheet, i)\"\n *ngIf=\"hasTabActions(sheet)\">\n <nile-icon name=\"arrowdown\" size=\"14\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Toolbar Tab (for workbook controls) -->\n <nile-tab slot=\"nav\" \n panel=\"shared-panel\"\n class=\"workbook-toolbar-tab\"\n [disabled]=\"true\">\n <div class=\"workbook-toolbar-content\">\n <!-- Autosave Indicator -->\n <div class=\"autosave-indicator\" *ngIf=\"autosaveEnabled\">\n <nile-icon \n *ngIf=\"!isSaving && lastSaveTime\" \n name=\"save\" \n size=\"14\"\n [title]=\"'Saved at ' + (lastSaveTime | date:'HH:mm:ss')\">\n </nile-icon>\n <nile-icon \n *ngIf=\"isSaving\" \n name=\"loader\" \n size=\"14\"\n title=\"Saving...\">\n </nile-icon>\n </div>\n\n <!-- Workbook Actions Dropdown -->\n <button class=\"workbook-actions-button\"\n *ngIf=\"config.workbookActions && config.workbookActions.length > 0\"\n (click)=\"toggleWorkbookActions($event)\"\n title=\"Workbook Actions\">\n <nile-icon name=\"settings\"></nile-icon>\n </button>\n \n <!-- Add Sheet Button -->\n <button class=\"add-sheet-button\"\n *ngIf=\"canAddSheet\"\n (click)=\"onAddSheet()\"\n title=\"Add Sheet\">\n <nile-icon name=\"plus\"></nile-icon>\n </button>\n \n <!-- Fullscreen Button -->\n <button class=\"fullscreen-button\"\n *ngIf=\"config.display?.allowFullscreen !== false\"\n (click)=\"toggleFullscreen()\"\n [title]=\"(isFullscreen$ | async) ? 'Exit Fullscreen' : 'Fullscreen'\">\n <nile-icon [name]=\"(isFullscreen$ | async) ? 'collapse' : 'expand-06'\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Single Shared Tab Panel -->\n <nile-tab-panel name=\"shared-panel\">\n <!-- Lazy loading strategy: table is destroyed and recreated with new config/state when sheet changes -->\n <!-- Using ngFor with trackBy to force complete reinitialization when tableComponentKey changes -->\n <ng-container *ngFor=\"let key of [tableComponentKey]; trackBy: trackByKey\">\n <st-table *ngIf=\"currentTableConfig && currentTableState\"\n [attr.data-sheet-key]=\"key\"\n [tableConfig]=\"currentTableConfig\"\n [data$]=\"currentTableData$\"\n [tableState]=\"currentTableState\"\n (cellChange)=\"onCellChange($event)\"\n (cellSave)=\"onCellSave($event)\"\n (stateChange)=\"onTableStateChange($event)\"\n (requestAddRow)=\"onRequestAddRow($event)\">\n </st-table>\n </ng-container>\n </nile-tab-panel>\n \n </nile-tab-group>\n</div>\n\n<!-- Tab Actions Dropdown -->\n<div class=\"tab-actions-dropdown\" \n *ngIf=\"tabActionsOpen\"\n [ngStyle]=\"tabActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeTabActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of selectedSheet?.tabActions\" \n (click)=\"onTabActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Workbook Actions Dropdown -->\n<div class=\"workbook-actions-dropdown\"\n *ngIf=\"workbookActionsOpen\"\n [ngStyle]=\"workbookActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeWorkbookActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of visibleWorkbookActions\"\n [class.disabled]=\"isActionDisabled(action)\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Fullscreen Backdrop -->\n<div class=\"fullscreen-backdrop\" \n *ngIf=\"isFullscreen$ | async\"\n (click)=\"toggleFullscreen()\">\n</div>\n\n", styles: ["@import\"@aquera/nile/lib/styles/variables.css\";:host{display:block;width:100%;height:100%}.workbook-container{display:flex;flex-direction:column;height:100%;background:white;border:1px solid var(--nile-color-neutral-200);border-radius:4px;overflow:hidden}.workbook-container.fullscreen{position:fixed;inset:0;z-index:2000;border:none;border-radius:0}.workbook-container nile-tab-group{height:100%;display:flex;flex-direction:column}.sheet-tab-content{display:flex;align-items:center;gap:8px;padding:0 4px}.sheet-tab-content .sheet-name{font-size:12px;font-weight:500;font-family:var(--nile-font-family-sans-serif);color:#000}.sheet-tab-content .tab-actions-button{width:20px;height:20px;padding:0;background:transparent;border:none;cursor:pointer}.workbook-toolbar-tab{margin-left:auto!important;pointer-events:auto!important;border-left:1px solid var(--nile-color-neutral-200)}.workbook-toolbar-tab .workbook-toolbar-content{display:flex;gap:4px;align-items:center;padding:0 8px}.workbook-toolbar-tab .workbook-toolbar-content .autosave-indicator{display:flex;align-items:center;margin-right:8px;color:var(--nile-color-success-500)}.workbook-toolbar-tab button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s;color:var(--nile-color-neutral-600)}.workbook-toolbar-tab button:hover{background-color:var(--nile-color-neutral-100);color:var(--nile-color-neutral-900)}.workbook-toolbar-tab button:active{background-color:var(--nile-color-neutral-200)}.workbook-toolbar-tab button nile-icon{font-size:16px}.tab-actions-dropdown{position:fixed;z-index:1001}.tab-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.tab-actions-dropdown .dropdown-menu{position:relative;min-width:180px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.tab-actions-dropdown .dropdown-menu nile-menu{display:block}.workbook-actions-dropdown{position:fixed;z-index:1001}.workbook-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.workbook-actions-dropdown .dropdown-menu{position:relative;min-width:200px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.workbook-actions-dropdown .dropdown-menu nile-menu{display:block}.fullscreen-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1999;cursor:pointer}nile-tab-group::part(nav){background-color:#fafafa}nile-tab-group::part(active-tab-indicator-path){background:none}nile-tab-group::part(active-tab-indicator){border-bottom:none}nile-tab-group::part(tabs){gap:0}nile-tab-group nile-tab{border:1px solid #e0e0e0;border-top-left-radius:6px;border-top-right-radius:6px}nile-tab-group nile-tab::part(base){padding-left:.5rem;padding-top:.5rem;padding-bottom:.5rem;border-bottom-left-radius:0;border-bottom-right-radius:0}nile-tab-group nile-tab.active{background-color:#fff;color:#000}nile-tab-group nile-tab.active .sheet-name{font-weight:600}nile-tab-group nile-tab-panel::part(base){padding:0;max-height:30rem}\n"] }]
10506
+ args: [{ selector: 'st-workbook', template: "<div class=\"workbook-container\" [class.fullscreen]=\"isFullscreen$ | async\">\n <nile-tab-group [activeIndex]=\"activeSheetIndex\">\n \n <!-- Sheet Tabs (one per sheet) -->\n <nile-tab *ngFor=\"let sheet of sheets; let i = index\"\n slot=\"nav\" \n panel=\"shared-panel\"\n [class.active]=\"i === activeSheetIndex\"\n (click)=\"onTabChange(i)\">\n <div class=\"sheet-tab-content\">\n <span class=\"sheet-name\">{{ sheet.name }}</span>\n \n <!-- Tab actions dropdown button -->\n <button class=\"tab-actions-button\"\n (click)=\"openTabActions($event, sheet, i)\"\n *ngIf=\"hasTabActions(sheet)\">\n <nile-icon name=\"arrowdown\" size=\"14\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Toolbar Tab (for workbook controls) -->\n <nile-tab slot=\"nav\" \n panel=\"shared-panel\"\n class=\"workbook-toolbar-tab\"\n [disabled]=\"true\">\n <div class=\"workbook-toolbar-content\">\n <!-- Autosave Indicator -->\n <div class=\"autosave-indicator\" *ngIf=\"autosaveEnabled\">\n <nile-icon \n *ngIf=\"!isSaving && lastSaveTime\" \n name=\"save\" \n size=\"14\"\n [title]=\"'Saved at ' + (lastSaveTime | date:'HH:mm:ss')\">\n </nile-icon>\n <nile-icon \n *ngIf=\"isSaving\" \n name=\"loader\" \n size=\"14\"\n title=\"Saving...\">\n </nile-icon>\n </div>\n\n <!-- Toolbar Workbook Actions (shown as individual buttons) -->\n <button *ngFor=\"let action of toolbarWorkbookActions\"\n class=\"toolbar-action-button\"\n [class.disabled]=\"isActionDisabled(action)\"\n [title]=\"action.label\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" [name]=\"action.icon\"></nile-icon>\n <span *ngIf=\"!action.icon\">{{ action.label }}</span>\n </button>\n\n <!-- Workbook Actions Dropdown -->\n <button class=\"workbook-actions-button\"\n *ngIf=\"visibleWorkbookActions.length > 0\"\n (click)=\"toggleWorkbookActions($event)\"\n title=\"Workbook Actions\">\n <nile-icon name=\"settings\"></nile-icon>\n </button>\n \n <!-- Add Sheet Button -->\n <button class=\"add-sheet-button\"\n *ngIf=\"canAddSheet\"\n (click)=\"onAddSheet()\"\n title=\"Add Sheet\">\n <nile-icon name=\"plus\"></nile-icon>\n </button>\n \n <!-- Fullscreen Button -->\n <button class=\"fullscreen-button\"\n *ngIf=\"config.display?.allowFullscreen !== false\"\n (click)=\"toggleFullscreen()\"\n [title]=\"(isFullscreen$ | async) ? 'Exit Fullscreen' : 'Fullscreen'\">\n <nile-icon [name]=\"(isFullscreen$ | async) ? 'collapse' : 'expand-06'\"></nile-icon>\n </button>\n </div>\n </nile-tab>\n \n <!-- Single Shared Tab Panel -->\n <nile-tab-panel name=\"shared-panel\">\n <!-- Lazy loading strategy: table is destroyed and recreated with new config/state when sheet changes -->\n <!-- Using ngFor with trackBy to force complete reinitialization when tableComponentKey changes -->\n <ng-container *ngFor=\"let key of [tableComponentKey]; trackBy: trackByKey\">\n <st-table *ngIf=\"currentTableConfig && currentTableState\"\n [attr.data-sheet-key]=\"key\"\n [tableConfig]=\"currentTableConfig\"\n [data$]=\"currentTableData$\"\n [tableState]=\"currentTableState\"\n (cellChange)=\"onCellChange($event)\"\n (cellSave)=\"onCellSave($event)\"\n (stateChange)=\"onTableStateChange($event)\"\n (requestAddRow)=\"onRequestAddRow($event)\">\n </st-table>\n </ng-container>\n </nile-tab-panel>\n \n </nile-tab-group>\n</div>\n\n<!-- Tab Actions Dropdown -->\n<div class=\"tab-actions-dropdown\" \n *ngIf=\"tabActionsOpen\"\n [ngStyle]=\"tabActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeTabActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of selectedSheet?.tabActions\" \n (click)=\"onTabActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Workbook Actions Dropdown -->\n<div class=\"workbook-actions-dropdown\"\n *ngIf=\"workbookActionsOpen\"\n [ngStyle]=\"workbookActionsPosition\">\n <div class=\"dropdown-backdrop\" (click)=\"closeWorkbookActions()\"></div>\n <div class=\"dropdown-menu\">\n <nile-menu>\n <nile-menu-item *ngFor=\"let action of visibleWorkbookActions\"\n [class.disabled]=\"isActionDisabled(action)\"\n (click)=\"onWorkbookActionClick(action, $event)\">\n <nile-icon *ngIf=\"action.icon\" slot=\"prefix\" size=\"14\" [name]=\"action.icon\"></nile-icon>\n {{ action.label }}\n </nile-menu-item>\n </nile-menu>\n </div>\n</div>\n\n<!-- Fullscreen Backdrop -->\n<div class=\"fullscreen-backdrop\" \n *ngIf=\"isFullscreen$ | async\"\n (click)=\"toggleFullscreen()\">\n</div>\n\n", styles: ["@import\"@aquera/nile/lib/styles/variables.css\";:host{display:block;width:100%;height:100%}.workbook-container{display:flex;flex-direction:column;height:100%;background:white;border:1px solid var(--nile-color-neutral-200);border-radius:4px;overflow:hidden}.workbook-container.fullscreen{position:fixed;inset:0;z-index:2000;border:none;border-radius:0}.workbook-container nile-tab-group{height:100%;display:flex;flex-direction:column}.sheet-tab-content{display:flex;align-items:center;gap:8px;padding:0 4px}.sheet-tab-content .sheet-name{font-size:12px;font-weight:500;font-family:var(--nile-font-family-sans-serif);color:#000}.sheet-tab-content .tab-actions-button{width:20px;height:20px;padding:0;background:transparent;border:none;cursor:pointer}.workbook-toolbar-tab{margin-left:auto!important;pointer-events:auto!important;border-left:1px solid var(--nile-color-neutral-200)}.workbook-toolbar-tab .workbook-toolbar-content{display:flex;gap:4px;align-items:center;padding:0 8px}.workbook-toolbar-tab .workbook-toolbar-content .autosave-indicator{display:flex;align-items:center;margin-right:8px;color:var(--nile-color-success-500)}.workbook-toolbar-tab button{display:flex;align-items:center;justify-content:center;width:28px;height:28px;padding:0;background:transparent;border:none;border-radius:4px;cursor:pointer;transition:background-color .2s;color:var(--nile-color-neutral-600)}.workbook-toolbar-tab button:hover{background-color:var(--nile-color-neutral-100);color:var(--nile-color-neutral-900)}.workbook-toolbar-tab button:active{background-color:var(--nile-color-neutral-200)}.workbook-toolbar-tab button nile-icon{font-size:16px}.workbook-toolbar-tab button.disabled{opacity:.5;cursor:not-allowed;pointer-events:none}.tab-actions-dropdown{position:fixed;z-index:1001}.tab-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.tab-actions-dropdown .dropdown-menu{position:relative;min-width:180px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.tab-actions-dropdown .dropdown-menu nile-menu{display:block}.workbook-actions-dropdown{position:fixed;z-index:1001}.workbook-actions-dropdown .dropdown-backdrop{position:fixed;top:0;left:0;width:100%;height:100%;background:transparent;z-index:1000}.workbook-actions-dropdown .dropdown-menu{position:relative;min-width:200px;background:white;border-radius:8px;box-shadow:0 4px 12px #00000026;z-index:1001;overflow:hidden}.workbook-actions-dropdown .dropdown-menu nile-menu{display:block}.fullscreen-backdrop{position:fixed;inset:0;background:rgba(0,0,0,.5);z-index:1999;cursor:pointer}nile-tab-group::part(nav){background-color:#fafafa}nile-tab-group::part(active-tab-indicator-path){background:none}nile-tab-group::part(active-tab-indicator){border-bottom:none}nile-tab-group::part(tabs){gap:0}nile-tab-group nile-tab{border:1px solid #e0e0e0;border-top-left-radius:6px;border-top-right-radius:6px}nile-tab-group nile-tab::part(base){padding-left:.5rem;padding-top:.5rem;padding-bottom:.5rem;border-bottom-left-radius:0;border-bottom-right-radius:0}nile-tab-group nile-tab.active{background-color:#fff;color:#000}nile-tab-group nile-tab.active .sheet-name{font-weight:600}nile-tab-group nile-tab-panel::part(base){padding:0;max-height:30rem}\n"] }]
10496
10507
  }], ctorParameters: function () { return [{ type: AutosaveService }, { type: i0.ChangeDetectorRef }]; }, propDecorators: { config: [{
10497
10508
  type: Input
10498
10509
  }], sheetsData: [{