Skip to content

Commit

Permalink
ref: elevate search highlighting and add Cell type
Browse files Browse the repository at this point in the history
  • Loading branch information
francisashley committed Jan 5, 2025
1 parent 40fe3a5 commit a9000cd
Show file tree
Hide file tree
Showing 12 changed files with 271 additions and 132 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "vue-screener",
"version": "0.15.16",
"version": "0.15.17",
"type": "module",
"description": "Easily search and filter data in Vue3.",
"author": "Francis Ashley",
Expand Down
8 changes: 4 additions & 4 deletions src/components/pagination/VueScreenerPagination.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@
>
<slot :screener="screener">
<VueScreenerPaginationResults
:total="screener.queriedRows.value.length ?? 0"
:total="screener.searchedRows.value.length ?? 0"
:current-page="props.screener.searchQuery.value.page"
:per-page="props.screener.searchQuery.value.rowsPerPage"
/>

<VueScreenerPaginationButtons
:total="props.screener.queriedRows.value.length"
:total="props.screener.searchedRows.value.length"
:per-page="props.screener.searchQuery.value.rowsPerPage"
:current-page="props.screener.searchQuery.value.page"
@go-to="screener.actions.goToPage"
Expand Down Expand Up @@ -44,7 +44,7 @@ const props = defineProps<{
}>()
const totalPages = computed((): number => {
return Math.ceil(props.screener.queriedRows.value.length / props.screener.searchQuery.value.rowsPerPage) || 0
return Math.ceil(props.screener.searchedRows.value.length / props.screener.searchQuery.value.rowsPerPage) || 0
})
const currentPageIsInRange = computed((): boolean => {
Expand All @@ -56,7 +56,7 @@ onMounted(() => {
})
watch(
() => props.screener.queriedRows.value.length,
() => props.screener.searchedRows.value.length,
() => ensureCurrentPageIsValid(),
)
Expand Down
47 changes: 6 additions & 41 deletions src/components/table/VueScreenerTableCell.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,61 +9,26 @@
column.isPinned && twMerge('vsc-sticky vsc-right-0 vsc-border-l vsc-ml-[-1px] vsc-shadow-[0px_0px_0px_rgba(0,0,0,0)] vsc-transition-shadow vsc-duration-300 vsc-ease-out', props.pinnedClass), // eslint-disable-line
column.isOverlayingColumns && twMerge('!vsc-shadow-[-3px_0px_2px_rgba(0,0,0,0.11)]', props.pinnedOverlappingClass), // eslint-disable-line
]"
:title="column.truncate && value"
:title="column.truncate ? text : ''"
>
<slot>
<span v-html="formattedValue.value" />
<div v-if="formattedValue.isHighlighted" class="vsc-absolute vsc-inset-0 vsc-bg-yellow-400/5" />
<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 { computed, defineProps, HTMLAttributes } from 'vue'
import { defineProps, HTMLAttributes } from 'vue'
import { twMerge } from '../../utils/tailwind-merge.utils'
import { Column, Row, IVueScreener } from '@/interfaces/vue-screener'
import { highlightMatches } from '../../utils/text.utils'
import { Column } from '@/interfaces/vue-screener'
const props = defineProps<{
screener?: IVueScreener
column: Column
row?: Row
pinnedClass?: string
pinnedOverlappingClass?: string
text?: string
isSearchMatch?: boolean
class?: HTMLAttributes['class']
}>()
const value = computed(() => {
return props.row ? props.row.data[props.column.field] : undefined
})
const formattedValue = computed(() => {
let formattedValue = {
isHighlighted: false,
value: value.value,
}
if (!props.row || !props.screener) return formattedValue
// allow the user to format the value
if (props.column.format) {
formattedValue = {
isHighlighted: false,
value: props.column.format(formattedValue, props.row),
}
}
// highlight search matches
const disableSearchHighlight = props.screener.options.value.disableSearchHighlight
const text = props.screener.searchQuery.value.text
if (!disableSearchHighlight && text && formattedValue !== undefined) {
const updatedFormattedValue = highlightMatches(String(formattedValue.value), text)
formattedValue = {
isHighlighted: formattedValue.value != updatedFormattedValue,
value: updatedFormattedValue,
}
}
return formattedValue
})
</script>
11 changes: 10 additions & 1 deletion src/components/viewport/states/VueScreenerTableState.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@
:screener="screener"
:column="column"
:row="row"
:cell="row.cells[column.field]"
:text="row.cells[column.field]?.htmlValue"
:is-search-match="row.cells[column.field]?.isSearchMatch"
>
<VueScreenerTableCell :screener="screener" :column="column" :row="row" />
<VueScreenerTableCell
:screener="screener"
:column="column"
:row="row"
:text="row.cells[column.field]?.htmlValue"
:is-search-match="row.cells[column.field]?.isSearchMatch"
/>
</slot>
</VueScreenerTableRow>
</template>
Expand Down
49 changes: 38 additions & 11 deletions src/hooks/use-vue-screener.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Column, Row, IVueScreener, SearchQuery, VueScreenerOptions } from '@/interfaces/vue-screener'
import { createColumn, getFields, getPaginated, isValidInput, convertToRows, sortRows } from '../utils/data.utils'
import { computed, ref } from 'vue'
import { search } from '../utils/search.utils'
import Search from '../utils/search'

export const useVueScreener = (inputData?: unknown[], defaultOptions: VueScreenerOptions = {}): IVueScreener => {
const options = ref<VueScreenerOptions>({
Expand Down Expand Up @@ -38,19 +38,46 @@ export const useVueScreener = (inputData?: unknown[], defaultOptions: VueScreene
sortDirection: options.value.defaultSortDirection ?? 'desc', // Sort direction
})

const queriedRows = computed((): Row[] => {
return search({
rows: allRows.value,
columns: columns.value,
text: searchQuery.value.text,
const parsedRows = computed((): Row[] => {
// If no columns defined, return unmodified rows
if (!options.value.columns) return allRows.value

// Filter to only columns with formatters
const columnsWithFormatters = Object.entries(options.value.columns).filter(([, column]) => column?.format)

// If no formatters, return original rows
if (columnsWithFormatters.length === 0) return allRows.value

// Map over each row
return allRows.value.map((row) => {
// Create a deep copy of the row
const formattedRow = {
...row,
cells: Object.fromEntries(Object.entries(row.cells).map(([key, cell]) => [key, { ...cell }])),
}

// Only iterate over columns that have formatters
columnsWithFormatters.forEach(([key, column]) => {
if (row.cells[key] !== undefined) {
formattedRow.cells[key].value = column!.format!(row.cells[key], row)
}
})

return formattedRow
})
})

const searchedRows = computed((): Row[] => {
return new Search(parsedRows.value, {
regex: searchQuery.value.regex,
caseSensitive: searchQuery.value.caseSensitive,
wholeWord: searchQuery.value.wholeWord,
})
disableSearchHighlight: options.value.disableSearchHighlight,
}).execute(searchQuery.value.text)
})

const sortedRows = computed((): Row[] => {
const sortedRows = searchQuery.value.text ? queriedRows.value : allRows.value
const sortedRows = searchQuery.value.text ? searchedRows.value : allRows.value

const _sortField = searchQuery.value.sortField

Expand All @@ -74,7 +101,7 @@ export const useVueScreener = (inputData?: unknown[], defaultOptions: VueScreene
})
})

const totalSearchedRows = computed(() => queriedRows.value.length ?? 0)
const totalSearchedRows = computed(() => searchedRows.value.length ?? 0)
const currentPage = computed(() => searchQuery.value.page)
const rowsPerPage = computed(() => searchQuery.value.rowsPerPage)

Expand Down Expand Up @@ -181,8 +208,8 @@ export const useVueScreener = (inputData?: unknown[], defaultOptions: VueScreene
options, // user options
searchQuery, // search options (text, pagination, sort)
allRows, // all data
queriedRows, // filtered data (after search query)
paginatedRows, // paginated data (cut from queriedRows)
searchedRows, // filtered data (after search query)
paginatedRows, // paginated data (cut from searchedRows)
hasError, // boolean indicating if the data is valid
columns, // columns (field, label, width, isPinned, isSortable, defaultSortDirection)
dimensions, // screener dimensions
Expand Down
14 changes: 11 additions & 3 deletions src/interfaces/vue-screener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { ComputedRef, Ref } from 'vue'

export type DataType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'null' | 'array' | 'object'

export type VueScreenerOptions = {
contentHeight?: string // a css height
defaultCurrentPage?: number
Expand All @@ -17,7 +19,7 @@ export interface IVueScreener {
searchQuery: Ref<SearchQuery>
hasError: ComputedRef<boolean>
allRows: Ref<Row[]>
queriedRows: ComputedRef<Row[]>
searchedRows: ComputedRef<Row[]>
paginatedRows: ComputedRef<Row[]>
columns: ComputedRef<Column[]>
dimensions: Ref<{ width: number; height: number } | null>
Expand Down Expand Up @@ -74,7 +76,13 @@ export type Column = {

export type Row = {
id: string // A unique identifier for internal tracking and updating of the row.
data: { [key: string | number]: any } // The original data for the row.
cells: { [key: string | number]: Cell } // The original data for the row.
}

export type DataType = 'string' | 'number' | 'boolean' | 'symbol' | 'undefined' | 'object' | 'null' | 'array' | 'object'
export type Cell = {
value: any
stringValue: string
htmlValue: string
type: DataType
isSearchMatch?: boolean
}
4 changes: 2 additions & 2 deletions src/stories/1-basic-usage/5-slots.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@
<VueScreener title="Override Table Head Cell" :data="baseData">
<template #head-cell:id="props">
<VueScreenerTableHeadCell v-bind="props">
<div class="vsc-text-yellow-400">Overridden Table Head Cell</div>
<div class="vsc-text-yellow-400" v-html="props.column.field ?? props.column.label" />
</VueScreenerTableHeadCell>
</template>
</VueScreener>
Expand All @@ -62,7 +62,7 @@
<VueScreener title="Override Table Cell" :data="baseData">
<template #cell:id="props">
<VueScreenerTableCell v-bind="props">
<div class="vsc-text-yellow-400">Overridden Table Cell</div>
<div class="vsc-text-yellow-400" v-html="props.cell.htmlValue" />
</VueScreenerTableCell>
</template>
</VueScreener>
Expand Down
4 changes: 2 additions & 2 deletions src/stories/3-theming/1-space.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@
</VueScreener>
<VueScreenerPagination :screener="screener">
<VueScreenerPaginationResults
:total="screener.queriedRows.value.length ?? 0"
:total="screener.searchedRows.value.length ?? 0"
:current-page="screener.searchQuery.value.page"
:per-page="screener.searchQuery.value.rowsPerPage"
/>
<VueScreenerPaginationButtons
:total="screener.queriedRows.value.length"
:total="screener.searchedRows.value.length"
:per-page="screener.searchQuery.value.rowsPerPage"
:current-page="screener.searchQuery.value.page"
@go-to="screener.actions.goToPage"
Expand Down
4 changes: 2 additions & 2 deletions src/stories/3-theming/2-cavern.story.vue
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,13 @@
<div class="vsc-p-4">
<VueScreenerPagination :screener="screener">
<VueScreenerPaginationResults
:total="screener.queriedRows.value.length ?? 0"
:total="screener.searchedRows.value.length ?? 0"
:current-page="screener.searchQuery.value.page"
:per-page="screener.searchQuery.value.rowsPerPage"
class="vsc-text-xs vsc-min-w-[150px]"
/>
<VueScreenerPaginationButtons
:total="screener.queriedRows.value.length"
:total="screener.searchedRows.value.length"
:per-page="screener.searchQuery.value.rowsPerPage"
:current-page="screener.searchQuery.value.page"
@go-to="screener.actions.goToPage"
Expand Down
28 changes: 19 additions & 9 deletions src/utils/data.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { DataType, Row, Column } from '@/interfaces/vue-screener'
import { DataType, Row, Column, Cell } from '@/interfaces/vue-screener'
import { orderBy } from 'natural-orderby'
import { v4 as uuidv4 } from 'uuid'

Expand All @@ -17,12 +17,22 @@ export function isValidInput(data: unknown): data is Row[] {
* @param {Row[]} data - The input data.
* @returns {Row[]} The normalised data.
*/
export function convertToRows(data: Row[]): Row[] {
return data.map((row) => ({
id: uuidv4(),
// Handle both array and object inputs in one step
data: Array.isArray(row) ? Object.fromEntries(row.entries()) : row,
}))
export function convertToRows(data: any[]): Row[] {
return data.map((row) => {
const rowData = Array.isArray(row) ? Object.fromEntries(row.entries()) : row
const cells: Record<string | number, Cell> = {}

Object.entries(rowData).forEach(([key, value]) => {
cells[key] = {
value: value,
stringValue: String(value),
htmlValue: String(value),
type: getTypeOf(value),
}
})

return { id: uuidv4(), cells }
})
}

/**
Expand All @@ -31,7 +41,7 @@ export function convertToRows(data: Row[]): Row[] {
* @returns {string[]} Unique field keys.
*/
export function getFields(rows: Row[]): string[] {
const fields = new Set<string>(rows.flatMap((row) => Object.keys(row.data)))
const fields = new Set<string>(rows.flatMap((row) => Object.keys(row.cells)))
return Array.from(fields)
}

Expand Down Expand Up @@ -84,7 +94,7 @@ export const sortRows = (
const sortField = options.sortField
const sortDirection = options.invertSort ? (options.sortDirection === 'asc' ? 'desc' : 'asc') : options.sortDirection
if (sortField && sortDirection) {
return [...orderBy(data, [(row: Row) => row.data[sortField]], [sortDirection])]
return [...orderBy(data, [(row: Row) => row.cells[sortField]], [sortDirection])]
}
return data
}
Expand Down
Loading

0 comments on commit a9000cd

Please sign in to comment.