Created
June 27, 2025 14:37
-
-
Save scbj/2aba180f0ed71bfe052f3d74e9ea1aea to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <template> | |
| <table class="jk-table" :aria-label="ariaLabel" :aria-rowcount="items.length"> | |
| <thead> | |
| <tr> | |
| <th | |
| v-for="(column, index) in columns" | |
| :key="index" | |
| class="jk-table__header-cell-container" | |
| > | |
| <div | |
| class="jk-table__header-cell" | |
| :class=" | |
| determineCellJustificationModifiers( | |
| 'jk-table__header-cell', | |
| column | |
| ) | |
| " | |
| > | |
| <div class="jk-table__header-cell-content"> | |
| <span v-text="column.label" /> | |
| <JkButton | |
| icon="sort-ascending" | |
| no-background | |
| @click="onSortClick(column)" | |
| /> | |
| </div> | |
| <div | |
| class="jk-table__header-cell-resize-handle" | |
| :class="determineResizeHandleModifiers(column)" | |
| @mousedown="onResizeHandleMouseDown($event, column)" | |
| /> | |
| </div> | |
| </th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr | |
| v-for="(item, index) in displayedItems" | |
| :key="index" | |
| :class="determineRowModifiers(item)" | |
| @click="onRowClick(item)" | |
| > | |
| <td | |
| v-for="(column, columnIndex) in columns" | |
| :key="columnIndex" | |
| :class="{ | |
| ...determineCellJustificationModifiers( | |
| 'jk-table__row-cell', | |
| column | |
| ), | |
| ...determineRowCellModifiers(index) | |
| }" | |
| > | |
| <slot | |
| v-if="isColumnWithSlot(column)" | |
| :name="`cell:${column.slotName}`" | |
| :item="item" | |
| /> | |
| <span v-else>{{ item[column.property] }}</span> | |
| </td> | |
| </tr> | |
| <tr v-if="hasMoreItems" class="jk-table__load-more"> | |
| <LoadMore | |
| :has-more-items="hasMoreItems" | |
| @load-more-requested="loadNextPage" | |
| /> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </template> | |
| <script lang="ts"> | |
| import { defineComponent, PropType, toRef } from 'vue' | |
| import JkButton from '@/components/jk/button/JkButton.vue' | |
| import { | |
| Column, | |
| ColumnProps, | |
| ColumnWithSlot | |
| } from '@/components/jk/table-2/types' | |
| import { useResizableTableColumns } from '@/components/jk/table-2/use-resizable-table-columns' | |
| import LoadMore from '@/components/LoadMore.vue' | |
| import { Modifiers } from '@/components/types' | |
| import { useInfiniteScroll } from '@/composables/use-infinite-scroll' | |
| enum Event { | |
| CurrentChange = 'current-change', | |
| Sort = 'sort' | |
| } | |
| export default defineComponent({ | |
| name: 'JkTable2', | |
| components: { | |
| JkButton, | |
| LoadMore | |
| }, | |
| props: { | |
| ariaLabel: { | |
| type: String, | |
| required: true | |
| }, | |
| columns: { | |
| type: Array as PropType<Column[]>, | |
| required: true | |
| }, | |
| items: { | |
| type: Array as PropType<unknown[]>, | |
| required: true | |
| }, | |
| rowHeight: { | |
| type: String, | |
| required: true | |
| } | |
| }, | |
| setup(props) { | |
| const columns = toRef(props, 'columns') | |
| const { | |
| columnBeingResizedLabel, | |
| columnsTemplate, | |
| onResizeHandleMouseDown | |
| } = useResizableTableColumns(columns) | |
| const items = toRef(props, 'items') | |
| const { displayedItems, hasMoreItems, loadNextPage } = useInfiniteScroll( | |
| items, | |
| { pageLength: 40 } | |
| ) | |
| return { | |
| columnBeingResizedLabel, | |
| columnsTemplate, | |
| onResizeHandleMouseDown, | |
| displayedItems, | |
| hasMoreItems, | |
| loadNextPage | |
| } | |
| }, | |
| data() { | |
| return { | |
| currentItem: undefined as unknown | |
| } | |
| }, | |
| watch: { | |
| currentItem(item: unknown, oldItem: unknown) { | |
| this.$emit(Event.CurrentChange, item, oldItem) | |
| } | |
| }, | |
| methods: { | |
| determineCellJustificationModifiers( | |
| className: string, | |
| column: ColumnProps | |
| ): Modifiers { | |
| return { | |
| [`${className}--center`]: column.justify === 'center', | |
| [`${className}--right`]: column.justify === 'right', | |
| [`${className}--stretch`]: column.justify === 'stretch', | |
| [`${className}--monospace`]: column.fontFamily === 'monospace' | |
| } | |
| }, | |
| determineResizeHandleModifiers(column: ColumnProps): Modifiers { | |
| return { | |
| 'jk-table__header-cell-resize-handle--active': | |
| column.label === this.columnBeingResizedLabel | |
| } | |
| }, | |
| determineRowCellModifiers(index: number): Modifiers { | |
| return { | |
| 'jk-table__row-cell--last': index === this.items.length - 1 | |
| } | |
| }, | |
| determineRowModifiers(item: unknown): Modifiers { | |
| return { | |
| 'jk-table__row--current': this.currentItem === item | |
| } | |
| }, | |
| isColumnWithSlot(column: Column): column is ColumnWithSlot { | |
| return 'slotName' in column | |
| }, | |
| onRowClick(item: unknown) { | |
| this.currentItem = this.currentItem === item ? null : item | |
| }, | |
| onSortClick(column: Column) { | |
| const field = this.isColumnWithSlot(column) | |
| ? column.slotName | |
| : column.property | |
| this.$emit(Event.Sort, field) | |
| } | |
| } | |
| }) | |
| </script> | |
| <style lang="scss" scoped> | |
| $border-color: var(--grayscale-medium-high-color); | |
| $border-top-radius: $jk-table-2-border-top-radius; | |
| $header-background-color: var(--grayscale-high-color); | |
| $cell-padding: 2rem; | |
| $resize-handle-background-color: $jk-table-2-resize-handle-background-color; | |
| $resize-handle-active-box-shadow-background-color: $jk-table-2-resize-handle-active-box-shadow-background-color; | |
| $resize-handle-width: 1px; | |
| $row-hover-background-color: $jk-table-2-row-hover-background-color; | |
| $row-active-background-color: $jk-table-2-row-active-background-color; | |
| .jk-table { | |
| color: var(--grayscale-medium-low-color); | |
| border: 1px solid $border-color; | |
| border-radius: $border-top-radius $border-top-radius 0 0; | |
| min-width: 100%; | |
| width: auto; | |
| overflow: auto; | |
| border-collapse: collapse; | |
| display: grid; | |
| grid-template-columns: v-bind(columnsTemplate); | |
| grid-auto-rows: v-bind(rowHeight); | |
| text-align: left; | |
| } | |
| thead, | |
| tbody, | |
| tr { | |
| display: contents; | |
| } | |
| td { | |
| display: flex; | |
| align-items: center; | |
| padding: $cell-padding; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| transition: background-color $animation-duration-enter $animation-easing; | |
| &:not(.jk-table__row-cell--last) { | |
| border-bottom: 1px solid $border-color; | |
| } | |
| &.jk-table__row-cell--center { | |
| justify-content: center; | |
| } | |
| &.jk-table__row-cell--right { | |
| justify-content: right; | |
| } | |
| &.jk-table__row-cell--stretch { | |
| justify-content: stretch; | |
| } | |
| &.jk-table__row-cell--monospace { | |
| font-family: monospace; | |
| } | |
| &:hover, | |
| &:hover ~ td, | |
| &:has(~ td:hover) { | |
| background: $row-hover-background-color; | |
| transition: none; | |
| } | |
| } | |
| th { | |
| position: relative; | |
| } | |
| .jk-table__header-cell-container { | |
| z-index: 1; | |
| position: sticky; | |
| top: 0; | |
| left: 0; | |
| background: var(--grayscale-high-color); | |
| } | |
| .jk-table__header-cell { | |
| --resize-handle-opacity: 0; | |
| --resize-handle-transition: none; | |
| --resize-handle-box-shadow: none; | |
| position: relative; | |
| user-select: none; | |
| display: flex; | |
| align-items: center; | |
| height: v-bind(rowHeight); | |
| &.jk-table__header-cell--center { | |
| text-align: center; | |
| } | |
| &.jk-table__header-cell--right { | |
| justify-content: right; | |
| text-align: right; | |
| } | |
| &:last-of-type .jk-table__header-cell-resize-handle { | |
| transform: none; | |
| &::after { | |
| left: calc(100% - $resize-handle-width); | |
| } | |
| } | |
| } | |
| .jk-table__header-cell-content { | |
| padding: $cell-padding; | |
| overflow: hidden; | |
| text-overflow: ellipsis; | |
| white-space: nowrap; | |
| } | |
| .jk-table__header-cell-resize-handle { | |
| position: absolute; | |
| top: 0; | |
| right: 0; | |
| bottom: 0; | |
| background: transparent; | |
| width: 16px; | |
| cursor: col-resize; | |
| transform: translateX(50%); | |
| z-index: 1; | |
| &.jk-table__header-cell-resize-handle--active { | |
| --resize-handle-opacity: 1; | |
| --resize-handle-transition: opacity 0.3s linear 0s; | |
| --resize-handle-box-shadow: #{$resize-handle-active-box-shadow-background-color}; | |
| } | |
| &::after { | |
| content: ''; | |
| position: absolute; | |
| top: 0px; | |
| bottom: 0px; | |
| width: $resize-handle-width; | |
| background: $resize-handle-background-color; | |
| opacity: var(--resize-handle-opacity); | |
| left: 50%; | |
| transition: var(--resize-handle-transition); | |
| box-shadow: var(--resize-handle-box-shadow); | |
| } | |
| &:hover::after { | |
| opacity: 1; | |
| } | |
| } | |
| .jk-table__row--current td, | |
| .jk-table__row--current td:hover, | |
| .jk-table__row--current td:hover ~ td, | |
| .jk-table__row--current td:has(~ td:hover) { | |
| background-color: $row-active-background-color; | |
| } | |
| .jk-table__load-more { | |
| grid-column: 1 / -1; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| </style> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment