Skip to content

Commit c12a01b

Browse files
committed
Search
1 parent 7cddf19 commit c12a01b

File tree

10 files changed

+86
-33
lines changed

10 files changed

+86
-33
lines changed

demo/app.vue

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@
44
<button @click='fetch(1500)'>Load data (1.5s)</button>
55
Selected count: {{ selected.size }}
66
<button @click='selected.clear()'>Clear selection</button>
7+
<input type='search' v-model='search' placeholder='Search...' />
78
</div>
89
<ui-datagrid :data='data' :columns='columns'
910
:selected='selected'
11+
:search='search'
1012
style='flex: 1' />
1113
</template>
1214

1315
<script lang="ts">
14-
import { markNonReactive, ref, shallowReactive } from "vue";
16+
import { markNonReactive, ref, shallowReactive } from 'vue';
17+
import { debouncedRef } from './debounceRef';
1518
1619
export default {
1720
setup() {
@@ -21,7 +24,7 @@ export default {
2124
{ label: 'Name', data: 'name' },
2225
{ label: 'Height', data: 'height', css: 'dg-right', sortable: false, width: 100 },
2326
{ label: 'Weight', data: 'weight', css: 'dg-right', sortable: false, width: 100 },
24-
{ label: 'Spawn chance', data: 'spawn_chance', right: true, width: 100 },
27+
{ label: 'Spawn chance', data: 'spawn_chance', right: true, searchable: false, width: 100 },
2528
],
2629
data: ref<any>([]),
2730
@@ -32,6 +35,8 @@ export default {
3235
},
3336
3437
selected: shallowReactive(new Set()),
38+
39+
search: debouncedRef(400, ''),
3540
};
3641
3742
state.fetch(0);

demo/debounceRef.ts

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { Ref } from 'vue';
2+
import { track, trigger, TrackOpTypes, TriggerOpTypes } from '@vue/reactivity';
3+
4+
export function debouncedRef<T>(delay: number, value?: T) {
5+
let timeout = 0;
6+
return <Ref<T>><any>{
7+
_isRef: true,
8+
get value() {
9+
track(this, TrackOpTypes.GET, 'value');
10+
return value;
11+
},
12+
set value(v) {
13+
value = v;
14+
clearTimeout(timeout);
15+
timeout = setTimeout(<Function>(() => trigger(this, TriggerOpTypes.SET, 'value')), delay);
16+
},
17+
};
18+
}

src/columns/filler.ts

+8-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,14 @@ import { shallowReactive as sreactive, onMounted, watchEffect } from 'vue';
22
import { Column } from './column';
33

44
export function addFiller(columns: Column[], size: { width: number }) {
5-
const filler = sreactive({ key: 'fill', sortable: false, header: () => '', render: () => '', width: 0 });
5+
const filler = sreactive({
6+
key: 'fill',
7+
sortable: false,
8+
searchable: false,
9+
header: () => '',
10+
render: () => '',
11+
width: 0
12+
});
613

714
columns.push(filler);
815

src/datagrid.vue

+6-3
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ import { onMounted, reactive, shallowReactive, shallowRef as sref, watchEffect,
4545
import { AnimBody, AnimColumn, useAnimations } from "./animation";
4646
import { addFiller, autoSize, Column, ColumnDefinition, ColResizer } from './columns';
4747
import { ItemDirective, getItem } from './item';
48-
import ResizeDirective from './resize';
48+
import { ResizeDirective } from './resize';
49+
import { useSearching } from './search';
4950
import { addSelection } from './selection';
5051
import { useSorting, SortIndicator } from './sort';
5152
import { Ctor, view } from './utils';
@@ -70,6 +71,7 @@ export default defineComponent({
7071
columns: { type: Array as Ctor<ColumnDefinition[]>, required: true },
7172
data: [Array as Ctor<object[]>, Promise as Ctor<Promise<object[]>>],
7273
selected: Set as Ctor<Set<object>>,
74+
search: String,
7375
},
7476
7577
setup(props) {
@@ -79,10 +81,10 @@ export default defineComponent({
7981
const table = sref();
8082
const size = shallowReactive({ width: 0, height: 0 });
8183
const headerSize = shallowReactive({ width: 0, height: 0 });
82-
const sorting = useSorting(data);
84+
const sorting = useSorting(useSearching(data, props));
8385
const { scrollToTop } = useVirtual(sorting.data, size);
8486
85-
const columns: Column[] = reactive(props.columns!.map((c, i) => ({
87+
const columns: Column[] = reactive(props.columns.map((c, i) => ({
8688
header: (childProps: any) => c.label + "",
8789
render: (childProps: any) => childProps.data[c.data!] + "",
8890
width: 0,
@@ -97,6 +99,7 @@ export default defineComponent({
9799
const headerPointerDown = useAnimations(columns, autoSized);
98100
99101
onMounted(() => watchEffect(async () => {
102+
// TODO: empty grid because of no props.data or await data == []
100103
loading.value = true;
101104
data.value = await props.data!;
102105
loading.value = false;

src/resize.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { ObjectDirective } from "vue";
22

3-
export default <ObjectDirective>{
3+
export const ResizeDirective = <ObjectDirective>{
44
beforeMount(el, binding) {
55
const size = binding.value as { width: number; height: number };
66
const observer = new ResizeObserver(entries => {

src/search.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { computed, Ref } from 'vue';
2+
import { ColumnDefinition } from './columns';
3+
4+
function escapeRegExp(re: string) {
5+
return re.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
6+
}
7+
8+
export function useSearching(data: Ref<object[]>, props: { search?: string, columns: ColumnDefinition[] }) {
9+
return computed(() => {
10+
const { search, columns } = props;
11+
if (!search) return data.value;
12+
const re = RegExp(escapeRegExp(search), 'i');
13+
const searchables = columns.filter(c => c.searchable !== false).map(c => c.data!);
14+
return data.value.filter(obj => {
15+
for (const s of searchables) {
16+
const val = obj[s];
17+
if (val != null && re.test(val + ""))
18+
return true;
19+
}
20+
return false;
21+
});
22+
});
23+
}

src/selection/index.ts src/selection.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import { computed, Ref, watch, h } from "vue";
2-
import { Column } from '../columns';
1+
import { computed, Ref, watch, h } from 'vue';
2+
import { Column } from './columns';
33

44
export function addSelection(columns: Column[], data: Ref<object[]>, selected: Set<object> | undefined) {
55
if (!selected) return null;
@@ -24,6 +24,7 @@ export function addSelection(columns: Column[], data: Ref<object[]>, selected: S
2424
key: 'select',
2525
resizable: false,
2626
sortable: false,
27+
searchable: false,
2728
defaultWidth: 0,
2829
width: 0,
2930
header: () => h('input', {

src/virtual/body.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<tr :style='{ height: topGap + "px" }' />
44
<tr v-if='(index & 1) === 0' />
55
</tbody>
6-
<slot :items="items" />
6+
<slot :items='items' />
77
<tbody>
88
<tr :style='{ height: bottomGap + "px" }' />
99
</tbody>

src/virtual/index.ts

+19-21
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Ref, provide, shallowReactive, unref, watchEffect } from "vue";
1+
import { computed, Ref, provide, reactive, unref } from "vue";
22
import { injectKey, VirtualState } from './state';
33

44
export { default as VirtualScroller } from './scroller.vue';
@@ -7,34 +7,32 @@ export { default as VirtualBody } from './body.vue';
77
type Val<T> = T | Ref<T>;
88

99
export function useVirtual(data: Val<object[]>, size: { height: number }) {
10-
const state: VirtualState = shallowReactive({
10+
const state: VirtualState = reactive({
1111
scroller: <any>null!, // initialized on mount // FIXME: TS error if declared as Element?
1212
scrollTop: 0,
1313
rowHeight: 25,
1414
buffer: 4,
1515
topGap: 0,
1616
bottomGap: 0,
17-
index: 0,
18-
count: 0,
1917

20-
items: function*() {
21-
const all = unref(data);
22-
const to = state.index + state.count;
23-
for (let i = state.index; i < to; i++)
24-
yield all[i];
25-
},
26-
});
18+
items: computed(() => {
19+
// Technically, size.height is slighty too high because it's the height of the full table,
20+
// including thead, rather than just tbody.
21+
// That's not an issue though, it just means one or two extra rows will be rendered past the bottom.
22+
const length = unref(data).length;
23+
const { buffer, rowHeight, scrollTop } = state;
24+
const index = Math.max((scrollTop / rowHeight | 0) - buffer, 0);
25+
const count = Math.min((size.height / rowHeight | 0) + 1 + buffer + buffer, length - index);
26+
state.topGap = index * rowHeight;
27+
state.bottomGap = (length - count - index) * rowHeight;
2728

28-
watchEffect(() => {
29-
// Technically, size.height is slighty too high because it's the height of the full table,
30-
// including thead, rather than just tbody.
31-
// That's not an issue though, it just means one or two extra rows will be rendered past the bottom.
32-
const length = unref(data).length;
33-
const { buffer, rowHeight, scrollTop } = state;
34-
const index = state.index = Math.max((scrollTop / rowHeight | 0) - buffer, 0);
35-
const count = state.count = Math.min((size.height / rowHeight | 0) + 1 + buffer + buffer, length - index);
36-
state.topGap = index * rowHeight;
37-
state.bottomGap = (length - count - index) * rowHeight;
29+
return function*() {
30+
const all = unref(data);
31+
const to = index + count;
32+
for (let i = index; i < to; i++)
33+
yield all[i];
34+
};
35+
}),
3836
});
3937

4038
provide(injectKey, state);

src/virtual/state.ts

-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,6 @@ export interface VirtualState {
77
buffer: number;
88
topGap: number;
99
bottomGap: number;
10-
index: number;
11-
count: number;
1210

1311
items: () => Iterator<object>;
1412
}

0 commit comments

Comments
 (0)