Skip to content

Commit

Permalink
Feat/data type highlighting (#17)
Browse files Browse the repository at this point in the history
* ref: move cell renderers to own components

* feat: add data type highlighting
  • Loading branch information
francisashley authored Jan 18, 2025
1 parent 1269fea commit bf51f13
Show file tree
Hide file tree
Showing 13 changed files with 251 additions and 37 deletions.
3 changes: 3 additions & 0 deletions src/components/VueScreener.vue
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const props = withDefaults(
defaultSortDirection?: 'asc' | 'desc'
columns?: Record<PropertyKey, Partial<Column>>
disableSearchHighlight?: boolean
disableDataTypeHighlight?: boolean
loading?: boolean
title?: string
includeHeader?: boolean
Expand All @@ -81,6 +82,7 @@ const internalScreener = computed(
defaultSortDirection: props.defaultSortDirection,
columns: props.columns,
disableSearchHighlight: props.disableSearchHighlight,
disableDataTypeHighlight: props.disableDataTypeHighlight,
loading: props.loading,
}),
)
Expand All @@ -95,6 +97,7 @@ watch(
defaultSortDirection: props.defaultSortDirection,
columns: props.columns,
disableSearchHighlight: props.disableSearchHighlight,
disableDataTypeHighlight: props.disableDataTypeHighlight,
loading: props.loading,
}),
(options) => internalScreener.value.actions.setOptions(options),
Expand Down
29 changes: 29 additions & 0 deletions src/components/renderers/VueScreenerBooleanRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div
:class="[
twMerge(
'vsc-relative vsc-inset-0 vsc-break-words vsc-py-2 vsc-px-2 vsc-text-[#2196f3]',
truncate && 'vsc-whitespace-nowrap vsc-text-ellipsis vsc-overflow-hidden',
props.class,
),
]"
:title="text"
>
<slot>
<span v-html="text" />
<div v-if="isSearchMatch" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
</slot>
</div>
</template>

<script lang="ts" setup>
import { defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
const props = defineProps<{
truncate?: boolean
text?: string
isSearchMatch?: boolean
class?: HTMLAttributes['class']
}>()
</script>
29 changes: 29 additions & 0 deletions src/components/renderers/VueScreenerDefaultRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div
:class="[
twMerge(
'vsc-relative vsc-inset-0 vsc-break-words vsc-py-2 vsc-px-2',
truncate && 'vsc-whitespace-nowrap vsc-text-ellipsis vsc-overflow-hidden',
props.class,
),
]"
:title="text"
>
<slot>
<span v-html="text" />
<div v-if="isSearchMatch" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
</slot>
</div>
</template>

<script lang="ts" setup>
import { defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
const props = defineProps<{
truncate?: boolean
text?: string
isSearchMatch?: boolean
class?: HTMLAttributes['class']
}>()
</script>
57 changes: 57 additions & 0 deletions src/components/renderers/VueScreenerHeadRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
<template>
<div
:class="[
twMerge(
'vsc-inline-flex vsc-items-center vsc-font-bold vsc-whitespace-nowrap vsc-h-8 vsc-gap-2 vsc-text-xs vsc-py-2 vsc-px-2',
props.class,
),
isSortable && twMerge('vsc-inline-flex vsc-items-center vsc-gap-2 vsc-cursor-pointer', props.sortableClass),
Boolean(column.isSortable && getSortDirection(column.field)) && props.sortingClass,
]"
@click="handleClickColumnHeader(column)"
>
<SortIcon v-if="column.isSortable && getSortDirection(column.field)" :direction="getSortDirection(column.field)" />
<slot>{{ text }}</slot>
<Icon v-if="column.info" icon="codicon:info" class="vsc-ml-auto vsc-min-w-3 vsc-min-h-3" v-tooltip="column.info" />
</div>
</template>

<script lang="ts" setup>
import { computed, HTMLAttributes } from 'vue'
import type { IVueScreener, Column } from '../../interfaces/vue-screener'
import SortIcon from '../icons/SortIcon.vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
import { Icon } from '@iconify/vue'
import { vTooltip } from 'floating-vue'
import 'floating-vue/dist/style.css'
const props = defineProps<{
screener: IVueScreener
column: Column
sortableClass?: string
sortingClass?: string
text?: string | number
class?: HTMLAttributes['class']
}>()
const isSortable = computed(() => props.column.isSortable)
const getSortDirection = (field: string | number): 'asc' | 'desc' | null => {
if (props.screener.searchQuery.value.sortField === field) {
return props.screener.searchQuery.value.sortDirection
}
return null
}
const handleClickColumnHeader = (column: Column) => {
if (column.isSortable) {
props.screener.actions.sort(column.field)
}
}
</script>

<style>
.v-popper__popper {
@apply vsc-text-xs;
}
</style>
29 changes: 29 additions & 0 deletions src/components/renderers/VueScreenerNumberRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<template>
<div
:class="[
twMerge(
'vsc-relative vsc-inset-0 vsc-break-words vsc-py-2 vsc-px-2 vsc-text-[#d81b60]',
truncate && 'vsc-whitespace-nowrap vsc-text-ellipsis vsc-overflow-hidden',
props.class,
),
]"
:title="text"
>
<slot>
<span v-html="text" />
<div v-if="isSearchMatch" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
</slot>
</div>
</template>

<script lang="ts" setup>
import { defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
const props = defineProps<{
truncate?: boolean
text?: string
isSearchMatch?: boolean
class?: HTMLAttributes['class']
}>()
</script>
31 changes: 31 additions & 0 deletions src/components/renderers/VueScreenerStringRenderer.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<template>
<div
:class="[
twMerge(
'vsc-relative vsc-inset-0 vsc-break-words vsc-py-2 vsc-px-2 vsc-text-[#4caf50]',
truncate && 'vsc-whitespace-nowrap vsc-text-ellipsis vsc-overflow-hidden',
props.class,
),
]"
:title="text"
>
<slot>
<span v-html="parsedText" />
<div v-if="isSearchMatch" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
</slot>
</div>
</template>

<script lang="ts" setup>
import { computed, defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
const props = defineProps<{
truncate?: boolean
text?: string
isSearchMatch?: boolean
class?: HTMLAttributes['class']
}>()
const parsedText = computed(() => `"${props.text}"`)
</script>
31 changes: 27 additions & 4 deletions src/components/table/VueScreenerTableCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<div
:class="[
twMerge(
'vsc-border-r vsc-border-zinc-700 vsc-py-2 vsc-px-2 vsc-whitespace-inherit last:vsc-border-r-0 vsc-bg-zinc-800 vsc-break-words vsc-relative',
'vsc-border-r vsc-border-zinc-700 vsc-whitespace-inherit last:vsc-border-r-0 vsc-bg-zinc-800 vsc-relative',
column.truncate && 'vsc-whitespace-nowrap vsc-text-ellipsis vsc-overflow-hidden',
props.class,
),
Expand All @@ -12,23 +12,46 @@
:title="column.truncate ? text : ''"
>
<slot>
<span v-html="text" />
<div v-if="isSearchMatch" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
<VueScreenerStringRenderer
v-if="!disableDataTypeHighlight && type === 'string'"
:truncate="column.truncate"
:text="text"
:is-search-match="isSearchMatch"
/>
<VueScreenerNumberRenderer
v-else-if="!disableDataTypeHighlight && type === 'number'"
:truncate="column.truncate"
:text="text"
:is-search-match="isSearchMatch"
/>
<VueScreenerBooleanRenderer
v-else-if="!disableDataTypeHighlight && type === 'boolean'"
:truncate="column.truncate"
:text="text"
:is-search-match="isSearchMatch"
/>
<VueScreenerDefaultRenderer v-else :truncate="column.truncate" :text="text" :is-search-match="isSearchMatch" />
</slot>
</div>
</template>

<script lang="ts" setup>
import { defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
import { Column } from '@/interfaces/vue-screener'
import { Column, DataType } from '@/interfaces/vue-screener'
import VueScreenerDefaultRenderer from '../renderers/VueScreenerDefaultRenderer.vue'
import VueScreenerStringRenderer from '../renderers/VueScreenerStringRenderer.vue'
import VueScreenerNumberRenderer from '../renderers/VueScreenerNumberRenderer.vue'
import VueScreenerBooleanRenderer from '../renderers/VueScreenerBooleanRenderer.vue'
const props = defineProps<{
column: Column
pinnedClass?: string
pinnedOverlappingClass?: string
text?: string
type?: DataType
isSearchMatch?: boolean
class?: HTMLAttributes['class']
disableDataTypeHighlight?: boolean
}>()
</script>
41 changes: 9 additions & 32 deletions src/components/table/VueScreenerTableHeadCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,26 +9,24 @@
'vsc-inline-flex vsc-bg-[#1f1f22] vsc-items-center vsc-font-bold vsc-whitespace-nowrap vsc-h-8 vsc-gap-2 vsc-text-xs last:vsc-border-r-0',
props.class,
),
isSortable && twMerge('vsc-inline-flex vsc-items-center vsc-gap-2 vsc-cursor-pointer', props.sortableClass),
Boolean(column.isSortable && getSortDirection(column.field)) && props.sortingClass,
]"
@click="handleClickColumnHeader(column)"
>
<SortIcon v-if="column.isSortable && getSortDirection(column.field)" :direction="getSortDirection(column.field)" />
<slot>{{ text }}</slot>
<Icon v-if="column.info" icon="codicon:info" class="vsc-ml-auto vsc-min-w-3 vsc-min-h-3" v-tooltip="column.info" />
<VueScreenerTableHeadRenderer
:screener="screener"
:column="column"
:sortable-class="sortableClass"
:sorting-class="sortingClass"
:text="text"
/>
</VueScreenerTableCell>
</template>

<script lang="ts" setup>
import { computed, HTMLAttributes } from 'vue'
import { HTMLAttributes } from 'vue'
import type { IVueScreener, Column } from '../../interfaces/vue-screener'
import VueScreenerTableCell from '../table/VueScreenerTableCell.vue'
import SortIcon from '../icons/SortIcon.vue'
import VueScreenerTableHeadRenderer from '../renderers/VueScreenerHeadRenderer.vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
import { Icon } from '@iconify/vue'
import { vTooltip } from 'floating-vue'
import 'floating-vue/dist/style.css'
const props = defineProps<{
screener: IVueScreener
Expand All @@ -38,25 +36,4 @@ const props = defineProps<{
text?: string | number
class?: HTMLAttributes['class']
}>()
const isSortable = computed(() => props.column.isSortable)
const getSortDirection = (field: string | number): 'asc' | 'desc' | null => {
if (props.screener.searchQuery.value.sortField === field) {
return props.screener.searchQuery.value.sortDirection
}
return null
}
const handleClickColumnHeader = (column: Column) => {
if (column.isSortable) {
props.screener.actions.sort(column.field)
}
}
</script>

<style>
.v-popper__popper {
@apply vsc-text-xs;
}
</style>
2 changes: 2 additions & 0 deletions src/components/viewport/states/VueScreenerTableState.vue
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,10 @@
:screener="screener"
:column="column"
:row="row"
:type="row.cells[column.field]?.type"
:text="row.cells[column.field]?.htmlValue"
:is-search-match="row.cells[column.field]?.isSearchMatch"
:disable-data-type-highlight="screener.options.value.disableDataTypeHighlight"
/>
</slot>
</VueScreenerTableRow>
Expand Down
1 change: 1 addition & 0 deletions src/hooks/use-vue-screener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const useVueScreener = (inputData?: unknown[], defaultOptions: VueScreene
const options = ref<VueScreenerOptions>({
contentHeight: defaultOptions.contentHeight,
disableSearchHighlight: defaultOptions.disableSearchHighlight ?? false,
disableDataTypeHighlight: defaultOptions.disableDataTypeHighlight ?? false,
loading: defaultOptions.loading ?? false,
defaultCurrentPage: defaultOptions.defaultCurrentPage ?? 1,
defaultRowsPerPage: defaultOptions.defaultRowsPerPage ?? 10,
Expand Down
1 change: 1 addition & 0 deletions src/interfaces/vue-screener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export type VueScreenerOptions = {
defaultTruncate?: boolean
columns?: Record<PropertyKey, Partial<Column>>
disableSearchHighlight?: boolean
disableDataTypeHighlight?: boolean
loading?: boolean
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<template>
<Story title="5. Disable data type highlighting" source="-">
<div class="vsc-p-4 vsc-bg-zinc-800">
<VueScreener
title="Array of objects"
:data="baseData"
:columns="{ id: { width: 'minmax(50px, max-content)' } }"
disable-data-type-highlight
/>
</div>
<br />
<div class="vsc-p-4 vsc-bg-zinc-800">
<VueScreener
title="Array of primitives"
:data="primitivesData"
:columns="{ 0: { width: 'minmax(50px, max-content)' } }"
disable-data-type-highlight
/>
</div>
<br />
<div class="vsc-p-4 vsc-bg-zinc-800">
<VueScreener title="Array of mixed objects" :data="mixedObjectsData" disable-data-type-highlight />
</div>
</Story>
</template>

<script lang="ts" setup>
import { VueScreener } from '../../index'
import baseData from '../../fixtures/data.json'
import primitivesData from '../../fixtures/primitives-data.json'
import mixedObjectsData from '../../fixtures/mix-objects-data.json'
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<template>
<Story title="5. Slots" source="-">
<Story title="6. Slots" source="-">
<div class="vsc-p-4 vsc-bg-zinc-800">
<VueScreener title="Override header" :data="baseData">
<template #header>
Expand Down

0 comments on commit bf51f13

Please sign in to comment.