@bagelink/vue 1.0.16 → 1.0.22

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.
@@ -1,6 +1,8 @@
1
1
  <script lang="ts" setup>
2
- import { Btn, Icon, CheckInput, TextInput } from '@bagelink/vue'
3
- import { computed, ref, watch, nextTick } from 'vue'
2
+ import { Btn, CheckInput, TextInput } from '@bagelink/vue'
3
+ import { computed, ref, watch, nextTick, onUnmounted } from 'vue'
4
+ import Dropdown from '../Dropdown.vue'
5
+ import SpreadsheetTable from './SpreadsheetTable.vue'
4
6
 
5
7
  // Define column configuration types
6
8
  type ColumnFormat = 'text' | 'number' | 'currency' | 'date' | 'percentage' | 'image' | 'boolean'
@@ -91,6 +93,9 @@ function emitUpdate() {
91
93
  emit('update:modelValue', localRows.value.map(row => unflattenObject({ ...row })))
92
94
  }
93
95
 
96
+ // After other ref declarations but before the columns computed property
97
+ const visibleColumns = ref<string[]>([])
98
+
94
99
  // Sort state
95
100
  const sortColumn = ref<string | null>(null)
96
101
  const sortDirection = ref<'asc' | 'desc'>('asc')
@@ -178,7 +183,7 @@ function isCellEditable(columnKey: string): boolean {
178
183
  return !(column?.locked ?? false)
179
184
  }
180
185
 
181
- // Sort rows by column
186
+ // Update the sortByColumn function to preserve column visibility
182
187
  function sortByColumn(columnKey: string) {
183
188
  const column = columns.value.find(col => col.key === columnKey)
184
189
  if (!column?.sortable) return
@@ -202,8 +207,16 @@ function sortByColumn(columnKey: string) {
202
207
  return aVal < bVal ? -modifier : modifier
203
208
  })
204
209
 
210
+ // Save the current visibleColumns state
211
+ const currentVisibleColumns = [...visibleColumns.value]
212
+
205
213
  localRows.value = sorted
206
214
  emitUpdate()
215
+
216
+ // Restore the visibleColumns state after the update
217
+ nextTick(() => {
218
+ visibleColumns.value = currentVisibleColumns
219
+ })
207
220
  }
208
221
 
209
222
  // Variables to handle cell selection
@@ -232,15 +245,15 @@ function setInputRef(el: any, key: string) {
232
245
  }
233
246
  }
234
247
 
235
- // Determines if the given cell is within the currently selected range
236
- function isCellSelected(row: number, col: number): boolean {
237
- if (!selectionStart.value || !selectionEnd.value) return false
238
- const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
239
- const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
240
- const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
241
- const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
242
- return row >= startRow && row <= endRow && col >= startCol && col <= endCol
243
- }
248
+ // // Determines if the given cell is within the currently selected range
249
+ // function isCellSelected(row: number, col: number): boolean {
250
+ // if (!selectionStart.value || !selectionEnd.value) return false
251
+ // const startRow = Math.min(selectionStart.value.row, selectionEnd.value.row)
252
+ // const endRow = Math.max(selectionStart.value.row, selectionEnd.value.row)
253
+ // const startCol = Math.min(selectionStart.value.col, selectionEnd.value.col)
254
+ // const endCol = Math.max(selectionStart.value.col, selectionEnd.value.col)
255
+ // return row >= startRow && row <= endRow && col >= startCol && col <= endCol
256
+ // }
244
257
 
245
258
  // Mouse event handlers to manage the selection range
246
259
  function handleMouseDown(row: number, col: number) {
@@ -356,13 +369,25 @@ function updateCell(rowIndex: number, key: string, newValue: string | boolean) {
356
369
  emitUpdate()
357
370
  }
358
371
 
359
- // Update the fixed and scrollable columns computed properties to filter out hidden columns
372
+ // After other ref declarations but before computed properties
373
+ // const visibleColumns = ref<string[]>([])
374
+
375
+ // After the columns computed property
376
+ // Initialize visibleColumns with all columns when columns change
377
+ watch(() => columns.value, (newColumns) => {
378
+ if (newColumns.length > 0 && visibleColumns.value.length === 0) {
379
+ visibleColumns.value = newColumns.filter(col => !col.hidden).map(col => col.key)
380
+ }
381
+ })
382
+
383
+ // Update the fixedColumns computed property
360
384
  const fixedColumns = computed(() => {
361
- return columns.value.filter(col => col.fixed && !col.hidden)
385
+ return columns.value.filter(col => col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
362
386
  })
363
387
 
388
+ // Update the scrollableColumns computed property
364
389
  const scrollableColumns = computed(() => {
365
- return columns.value.filter(col => !col.fixed && !col.hidden)
390
+ return columns.value.filter(col => !col.fixed && !col.hidden && visibleColumns.value.includes(col.key))
366
391
  })
367
392
 
368
393
  // Update createEmptyRow to use defaultValue from column config
@@ -418,7 +443,16 @@ async function copySelection() {
418
443
  }
419
444
  }
420
445
 
421
- // Paste function using Navigator Clipboard API
446
+ // Add a ref for the search input
447
+ const searchInputRef = ref<any>(null)
448
+
449
+ // Add a method to check if the search input is focused
450
+ function isSearchFocused(): boolean {
451
+ const inputElement = searchInputRef.value?.$el?.querySelector('input')
452
+ return document.activeElement === inputElement
453
+ }
454
+
455
+ // Update the pasteSelection function to handle search focus
422
456
  async function pasteSelection() {
423
457
  if (!selectionStart.value) return
424
458
 
@@ -543,8 +577,8 @@ function handleCellKeyDown(event: KeyboardEvent, row: number, col: number) {
543
577
  // Update keyboard shortcuts to include undo/redo
544
578
  function handleSpreadsheetKeyDown(event: KeyboardEvent) {
545
579
  // Don't intercept keyboard shortcuts when editing a cell
546
- if (editingCell.value) return
547
-
580
+ if (editingCell.value || isSearchFocused()) return
581
+ console.log('handleSpreadsheetKeyDown', event)
548
582
  const isCtrlOrCmd = event.ctrlKey || event.metaKey
549
583
 
550
584
  if (isCtrlOrCmd) {
@@ -593,6 +627,64 @@ const filteredRows = computed(() => {
593
627
  })
594
628
  })
595
629
  })
630
+
631
+ // Add after other ref declarations
632
+ const columnWidths = ref<Map<string, number>>(new Map())
633
+ const isResizing = ref(false)
634
+ const resizingColumn = ref<string | null>(null)
635
+ const startX = ref<number>(0)
636
+ const startWidth = ref<number>(0)
637
+
638
+ // Add after other function declarations
639
+ function handleResizeStart(e: MouseEvent, columnKey: string) {
640
+ isResizing.value = true
641
+ resizingColumn.value = columnKey
642
+ startX.value = e.pageX
643
+
644
+ // Find the column header element
645
+ const columnHeader = (e.target as HTMLElement).closest('th')
646
+ if (!columnHeader) return
647
+
648
+ // Get the actual computed width of the column
649
+ const computedWidth = columnHeader.getBoundingClientRect().width
650
+ const currentWidth = columnWidths.value.get(columnKey) || computedWidth
651
+ startWidth.value = currentWidth
652
+
653
+ // Add event listeners for mousemove and mouseup
654
+ document.addEventListener('mousemove', handleResizeMove)
655
+ document.addEventListener('mouseup', handleResizeEnd)
656
+ }
657
+
658
+ function handleResizeMove(e: MouseEvent) {
659
+ if (!isResizing.value || !resizingColumn.value) return
660
+
661
+ e.preventDefault()
662
+
663
+ const diff = e.pageX - startX.value
664
+ const newWidth = Math.max(80, startWidth.value + diff) // Keep minimum width of 80px
665
+
666
+ columnWidths.value.set(resizingColumn.value, newWidth)
667
+ }
668
+
669
+ function handleResizeEnd() {
670
+ isResizing.value = false
671
+ resizingColumn.value = null
672
+
673
+ // Remove event listeners
674
+ document.removeEventListener('mousemove', handleResizeMove)
675
+ document.removeEventListener('mouseup', handleResizeEnd)
676
+ }
677
+
678
+ // Clean up event listeners when component is unmounted
679
+ onUnmounted(() => {
680
+ document.removeEventListener('mousemove', handleResizeMove)
681
+ document.removeEventListener('mouseup', handleResizeEnd)
682
+ })
683
+
684
+ // Add a computed property for visible column options
685
+ const columnOptions = computed(() => {
686
+ return columns.value.filter(col => !col.hidden)
687
+ })
596
688
  </script>
597
689
 
598
690
  <template>
@@ -600,7 +692,22 @@ const filteredRows = computed(() => {
600
692
  <div class="flex gap-05 py-05 justify-content-end m_flex-wrap">
601
693
  <label v-if="label" class="label me-auto">{{ label }}</label>
602
694
  <div class="flex gap-075">
603
- <TextInput v-model="search" icon="search" placeholder="Search" class="m-0 max-w200px" />
695
+ <Dropdown flat thin icon="view_column">
696
+ <div class="p-05">
697
+ <div class="flex space-between">
698
+ <Btn flat thin small value="Select All" @click="visibleColumns = columnOptions.map(col => col.key)" />
699
+ <Btn flat thin small value="Clear All" @click="visibleColumns = []" />
700
+ </div>
701
+ <CheckInput
702
+ v-for="col in columnOptions"
703
+ :key="col.key"
704
+ v-model="visibleColumns"
705
+ :value="col.key"
706
+ :label="col.label || col.key"
707
+ />
708
+ </div>
709
+ </Dropdown>
710
+ <TextInput ref="searchInputRef" v-model="search" icon="search" placeholder="Search" class="m-0 max-w200px" />
604
711
  <Btn v-tooltip="'Paste'" flat thin round icon="paste" @click="pasteSelection" />
605
712
  <Btn v-tooltip="'copy'" flat thin round icon="copy" @click="copySelection" />
606
713
  <Btn v-tooltip="'Undo'" flat thin round icon="undo" :disabled="!canUndo" @click="undo" />
@@ -610,156 +717,57 @@ const filteredRows = computed(() => {
610
717
  <div class="spreadsheet" @mouseup="handleMouseUp">
611
718
  <div class="flex w-100p relative">
612
719
  <!-- Fixed Columns -->
613
- <table v-if="fixedColumns.length" class="fixed-columns sticky z-2 start-0 bg-white">
614
- <thead>
615
- <tr>
616
- <th class="row-number-header bg-white" />
617
- <th
618
- v-for="col in fixedColumns"
619
- :key="col.key"
620
- >
621
- <span @click="col.sortable && sortByColumn(col.key)">
622
- {{ col.label || col.key }}
623
- </span>
624
- <Icon
625
- v-if="sortColumn === col.key"
626
- class="line-height-0 transition-400"
627
- name="keyboard_arrow_down" :class="{ 'rotate-180': sortDirection === 'desc' }"
628
- />
629
- </th>
630
- </tr>
631
- </thead>
632
- <tbody>
633
- <tr v-for="(row, rowIndex) in filteredRows" :key="rowIndex">
634
- <td class="row-number txt-center hover user-select-none pointer txt12 regular" @click="selectEntireRow(rowIndex)">
635
- {{ rowIndex + 1 }}
636
- </td>
637
- <td
638
- v-for="col in fixedColumns"
639
- :key="col.key"
640
- :class="{
641
- selected: isCellSelected(rowIndex, fixedColumns.indexOf(col)),
642
- locked: !isCellEditable(col.key),
643
- }"
644
- :style="{ width: col.width }"
645
- :tabindex="col.hidden ? undefined : 0"
646
- @mousedown="handleMouseDown(rowIndex, fixedColumns.indexOf(col))"
647
- @mouseover="handleMouseOver(rowIndex, fixedColumns.indexOf(col))"
648
- @focusin="handleMouseOver(rowIndex, fixedColumns.indexOf(col))"
649
- @dblclick="startEditing(rowIndex, fixedColumns.indexOf(col))"
650
- @keydown="handleCellKeyDown($event, rowIndex, fixedColumns.indexOf(col))"
651
- >
652
- <template v-if="editingCell && editingCell.row === rowIndex && editingCell.col === fixedColumns.indexOf(col)">
653
- <input
654
- :ref="el => setInputRef(el, `cell-${rowIndex}-${fixedColumns.indexOf(col)}`)"
655
- :value="row[col.key]"
656
- type="text"
657
- class="spreadsheet-input"
658
- @input="(e: Event) => updateCell(rowIndex, col.key, (e.target as HTMLInputElement).value)"
659
- @blur="handleStopEditingAndBlur(false)"
660
- @keydown.enter.prevent="handleStopEditingAndBlur(false)"
661
- @keydown.esc.prevent="handleStopEditingAndBlur(true)"
662
- @mousedown.stop
663
- >
664
- <span class="spreadsheet-cell spreadsheetCellPlaceHolder">{{ formatCellValue(row[col.key], col.format) }}</span>
665
- </template>
666
- <template v-else>
667
- <template v-if="col.format === 'image'">
668
- <div class="h40px w-100p flex align-items-center justify-content-center overflow-hidden">
669
- <img class=" w-100p h-100p contain radius-05" :src="row[col.key]" :alt="col.label || col.key">
670
- </div>
671
- </template>
672
- <template v-else-if="col.format === 'boolean'">
673
- <CheckInput
674
- :modelValue="!!row[col.key]"
675
- :disabled="!isCellEditable(col.key)"
676
- @update:modelValue="(value: boolean | any[] | undefined) => updateCell(rowIndex, col.key, !!value)"
677
- @mousedown.stop
678
- />
679
- </template>
680
- <template v-else>
681
- <span class="spreadsheet-cell">{{ formatCellValue(row[col.key], col.format) }}</span>
682
- </template>
683
- </template>
684
- </td>
685
- </tr>
686
- </tbody>
687
- </table>
720
+ <SpreadsheetTable
721
+ v-if="fixedColumns.length"
722
+ :columns="fixedColumns"
723
+ :rows="filteredRows"
724
+ :is-fixed="true"
725
+ :show-row-numbers="true"
726
+ :column-widths="columnWidths"
727
+ :sort-column="sortColumn"
728
+ :sort-direction="sortDirection"
729
+ :selection-start="selectionStart"
730
+ :selection-end="selectionEnd"
731
+ :editing-cell="editingCell"
732
+ :base-column-index="0"
733
+ class="sticky z-2 start-0 bg-white"
734
+ @sortColumn="sortByColumn"
735
+ @resizeStart="handleResizeStart"
736
+ @selectRow="selectEntireRow"
737
+ @cellMouseDown="handleMouseDown"
738
+ @cellMouseOver="handleMouseOver"
739
+ @cellEditStart="startEditing"
740
+ @cellKeyDown="handleCellKeyDown"
741
+ @updateCell="updateCell"
742
+ @stopEditing="handleStopEditingAndBlur"
743
+ @setInputRef="setInputRef"
744
+ />
688
745
 
689
746
  <!-- Scrollable Columns -->
690
747
  <div class="flex-shrink flex-grow overflow-x">
691
- <table class="w-100p">
692
- <thead>
693
- <tr>
694
- <th v-if="!fixedColumns.length" class="row-number-header bg-white" />
695
- <th
696
- v-for="col in scrollableColumns"
697
- :key="col.key"
698
- >
699
- <span @click="col.sortable && sortByColumn(col.key)">
700
- {{ col.label || col.key }}
701
- </span>
702
-
703
- <Icon v-if="sortColumn === col.key" name="keyboard_arrow_down" class="line-height-0 inline-block" :class="{ 'rotate-180': sortColumn === col.key && sortDirection === 'desc' }" />
704
- </th>
705
- </tr>
706
- </thead>
707
- <tbody>
708
- <tr v-for="(row, rowIndex) in filteredRows" :key="rowIndex">
709
- <td v-if="!fixedColumns.length" class="row-number txt-center hover user-select-none pointer txt12 regular" @click="selectEntireRow(rowIndex)">
710
- {{ rowIndex + 1 }}
711
- </td>
712
- <td
713
- v-for="(col, colIndex) in scrollableColumns"
714
- :key="col.key"
715
- :class="{
716
- selected: isCellSelected(rowIndex, fixedColumns.length + colIndex),
717
- locked: !isCellEditable(col.key),
718
- }"
719
- :style="{ width: col.width }"
720
- :tabindex="col.hidden ? undefined : 0"
721
- @mousedown="handleMouseDown(rowIndex, fixedColumns.length + colIndex)"
722
- @mouseover="handleMouseOver(rowIndex, fixedColumns.length + colIndex)"
723
- @focusin="handleMouseOver(rowIndex, fixedColumns.length + colIndex)"
724
- @dblclick="startEditing(rowIndex, fixedColumns.length + colIndex)"
725
- @keydown="handleCellKeyDown($event, rowIndex, fixedColumns.length + colIndex)"
726
- >
727
- <template v-if="editingCell && editingCell.row === rowIndex && editingCell.col === (fixedColumns.length + colIndex)">
728
- <input
729
- :ref="el => setInputRef(el, `cell-${rowIndex}-${fixedColumns.length + colIndex}`)"
730
- :value="row[col.key]"
731
- type="text"
732
- class="spreadsheet-input"
733
- @input="(e: Event) => updateCell(rowIndex, col.key, (e.target as HTMLInputElement).value)"
734
- @blur="handleStopEditingAndBlur(false)"
735
- @keydown.enter.prevent="handleStopEditingAndBlur(false)"
736
- @keydown.esc.prevent="handleStopEditingAndBlur(true)"
737
- @mousedown.stop
738
- >
739
- <span class="spreadsheet-cell spreadsheetCellPlaceHolder">{{ formatCellValue(row[col.key], col.format) }}</span>
740
- </template>
741
- <template v-else>
742
- <template v-if="col.format === 'image'">
743
- <div v-if="row[col.key]" class="h40px w-100p flex align-items-center justify-content-center overflow-hidden">
744
- <img class=" w-100p h-100p contain radius-05" :src="row[col.key]" :alt="col.label || col.key">
745
- </div>
746
- </template>
747
- <template v-else-if="col.format === 'boolean'">
748
- <CheckInput
749
- :modelValue="!!row[col.key]"
750
- :disabled="!isCellEditable(col.key)"
751
- @update:modelValue="(value: boolean | any[] | undefined) => updateCell(rowIndex, col.key, !!value)"
752
- @mousedown.stop
753
- />
754
- </template>
755
- <template v-else>
756
- <span class="spreadsheet-cell">{{ formatCellValue(row[col.key], col.format) }}</span>
757
- </template>
758
- </template>
759
- </td>
760
- </tr>
761
- </tbody>
762
- </table>
748
+ <SpreadsheetTable
749
+ :columns="scrollableColumns"
750
+ :rows="filteredRows"
751
+ :is-fixed="false"
752
+ :show-row-numbers="!fixedColumns.length"
753
+ :column-widths="columnWidths"
754
+ :sort-column="sortColumn"
755
+ :sort-direction="sortDirection"
756
+ :selection-start="selectionStart"
757
+ :selection-end="selectionEnd"
758
+ :editing-cell="editingCell"
759
+ :base-column-index="fixedColumns.length"
760
+ @sortColumn="sortByColumn"
761
+ @resizeStart="handleResizeStart"
762
+ @selectRow="selectEntireRow"
763
+ @cellMouseDown="handleMouseDown"
764
+ @cellMouseOver="handleMouseOver"
765
+ @cellEditStart="startEditing"
766
+ @cellKeyDown="handleCellKeyDown"
767
+ @updateCell="updateCell"
768
+ @stopEditing="handleStopEditingAndBlur"
769
+ @setInputRef="setInputRef"
770
+ />
763
771
  </div>
764
772
  </div>
765
773
  <Btn v-if="allowAddRow" outline thin round icon="add" value="Add Row" class="mt-05" @click="addRow" />
@@ -768,97 +776,8 @@ const filteredRows = computed(() => {
768
776
  </template>
769
777
 
770
778
  <style scoped>
771
- .fixed-columns {
772
- border-right: 2px solid var(--border-color);
773
- }
774
- .spreadsheet table {
775
- border-collapse: collapse;
776
- }
777
- .spreadsheet th, .spreadsheet td {
778
- border: 1px solid var(--border-color);
779
- padding: 0.1rem 0.5rem;
780
- min-width: 80px;
781
- background: var(--bgl-white);
782
- user-select: none;
783
- }
784
- .spreadsheet th {
785
- background: var(--input-bg);
786
- white-space: nowrap;
787
- position: relative;
788
- padding: 0.25rem 0.5rem;
789
- font-weight: 500;
790
- text-align: start;
791
- }
792
- .spreadsheet th .bgl_icon-font{
793
- vertical-align: middle;
794
- }
795
- .spreadsheet td.selected {
796
- background: var(--bgl-primary-light);
797
- }
798
- .spreadsheet td.locked {
799
- background: var(--bgl-gray-light);
800
- cursor: default;
801
- }
802
- .spreadsheet td.locked.selected {
803
- background: var(--bgl-primary-light);
804
- }
805
- .spreadsheet td {
806
- height: 40px;
807
- vertical-align: middle;
808
- }
809
- .spreadsheet td:has(img){
810
- padding: 0;
811
- }
812
- .spreadsheet td span{
813
- display: block;
814
- display: -webkit-box;
815
- max-width: 100%;
816
- -webkit-box-orient: vertical;
817
- overflow: hidden;
818
- text-overflow: ellipsis;
819
- -webkit-line-clamp: 1;
820
- word-break: break-all;
821
- }
822
- .spreadsheet input {
823
- width: 100%;
824
- border: none;
825
- background: transparent;
826
- padding: 0;
827
- margin: 0;
828
- min-height: 0;
829
- min-width: 0;
830
- }
831
- .spreadsheet input:focus {
832
- outline: 2px solid var(--bgl-primary);
833
- outline-offset: 6px;
834
- }
835
- .spreadsheet th.sortable {
836
- cursor: pointer;
837
- }
838
- .row-number-header, .row-number {
839
- width: fit-content;
840
- min-width: fit-content !important;
841
- padding: 0.1rem 0.7rem !important;
842
- }
843
- .spreadsheet td .bgl-checkbox{
844
- margin: 0;
845
- text-align: center;
846
- justify-content: center;
847
-
848
- }
849
- .spreadsheet td:has(.bgl-checkbox){
850
- text-align: center;
851
- background: var(--input-bg);
852
- }
853
- .spreadsheet td:has(:checked){
854
- background: var(--bgl-primary-light);
855
- }
856
-
857
- .spreadsheetCellPlaceHolder{
858
- height: 0px;
859
- overflow: hidden;
860
- opacity: 0;
861
- pointer-events: none;
779
+ /* Spreadsheet container styles */
780
+ .spreadsheet {
862
781
  user-select: none;
863
782
  }
864
783
  </style>