Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/add items expanded #331

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 8 additions & 1 deletion src/components/DataTable.vue
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,8 @@ const {
themeColor,
rowsOfPageSeparatorMessage,
showIndexSymbol,
itemsExpanded,
itemsKey,
preventContextMenuRow
} = toRefs(props);

Expand Down Expand Up @@ -411,7 +413,8 @@ const emits = defineEmits([
'update:serverOptions',
'updatePageItems',
'updateTotalItems',
'selectAll'
'selectAll',
'update:itemsExpanded',
]);

const isMultipleSelectable = computed((): boolean => itemsSelected.value !== null);
Expand Down Expand Up @@ -485,6 +488,7 @@ const {
serverItemsLength,
multiSort,
emits,
itemsKey,
);

const {
Expand Down Expand Up @@ -521,6 +525,7 @@ const {
showIndex,
totalItems,
totalItemsLength,
itemsKey,
);

const prevPageEndIndex = computed(() => {
Expand All @@ -536,6 +541,8 @@ const {
pageItems,
prevPageEndIndex,
emits,
itemsExpanded,
itemsKey,
);

const {
Expand Down
27 changes: 24 additions & 3 deletions src/hooks/useExpandableRow.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import { ref, Ref, ComputedRef } from 'vue';
import type { Item } from '../types/main';
import { ref, Ref, ComputedRef, watchEffect } from 'vue';
import type { Item, ItemKey } from '../types/main';
import type { EmitsEventName } from '../types/internal';
import { getItemIndex } from '../utils';

export default function useExpandableRow(
items: Ref<Item[]>,
prevPageEndIndex: ComputedRef<number>,
emits: (event: EmitsEventName, ...args: any[]) => void,
itemsExpanded: Ref<Item[]>,
itemsKey: Ref<ItemKey>
) {
const expandingItemIndexList = ref<number[]>([]);

Expand All @@ -14,17 +17,35 @@ export default function useExpandableRow(
const index = expandingItemIndexList.value.indexOf(expandingItemIndex);
if (index !== -1) {
expandingItemIndexList.value.splice(index, 1);
emitItemsExpanded();
} else {
const currentPageExpandIndex = items.value.findIndex((item) => JSON.stringify(item) === JSON.stringify(expandingItem));
const currentPageExpandIndex = getItemIndex(items.value, expandingItem, itemsKey.value)
emits('expandRow', prevPageEndIndex.value + currentPageExpandIndex, expandingItem);
expandingItemIndexList.value.push(prevPageEndIndex.value + currentPageExpandIndex);
emitItemsExpanded();
}
};

const clearExpandingItemIndexList = () => {
expandingItemIndexList.value = [];
};

watchEffect(() => {
const indexList = itemsExpanded.value.reduce<number[]>((itemsExpandedIndex, expandedItem) => {
const index = getItemIndex(items.value, expandedItem, itemsKey.value)
if (index !== -1) {
itemsExpandedIndex.push(index + prevPageEndIndex.value)
}
return itemsExpandedIndex
}, [])
expandingItemIndexList.value = indexList
})


function emitItemsExpanded() {
emits('update:itemsExpanded', expandingItemIndexList.value.map(index => items.value[index - prevPageEndIndex.value]))
}

return {
expandingItemIndexList,
updateExpandingItemIndexList,
Expand Down
22 changes: 8 additions & 14 deletions src/hooks/usePageItems.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import {
Ref, computed, ComputedRef, WritableComputedRef,
} from 'vue';
import type { Item } from '../types/main';
import type { Item, ItemKey } from '../types/main';
import type { MultipleSelectStatus } from '../types/internal';
import { areItemsEqual, getItemIndex } from '../utils';

export default function usePageItems(
currentPaginationNumber: Ref<number>,
Expand All @@ -14,6 +15,7 @@ export default function usePageItems(
showIndex: Ref<boolean>,
totalItems: ComputedRef<Item[]>,
totalItemsLength: ComputedRef<number>,
itemsKey: Ref<ItemKey>
) {
const currentPageFirstIndex = computed((): number => (currentPaginationNumber.value - 1)
* rowsPerPageRef.value + 1);
Expand Down Expand Up @@ -45,21 +47,13 @@ export default function usePageItems(
if (selectItemsComputed.value.length === 0) {
return 'noneSelected';
}
const isNoneSelected = selectItemsComputed.value.every((itemSelected) => {
if (totalItems.value.findIndex((item) => JSON.stringify(itemSelected) === JSON.stringify(item)) !== -1) {
return false;
}
return true;
});
const isNoneSelected = selectItemsComputed.value
.every((itemSelected) => getItemIndex(totalItems.value, itemSelected, itemsKey.value) === -1);
if (isNoneSelected) return 'noneSelected';

if (selectItemsComputed.value.length === totalItems.value.length) {
const isAllSelected = selectItemsComputed.value.every((itemSelected) => {
if (totalItems.value.findIndex((item) => JSON.stringify(itemSelected) === JSON.stringify(item)) === -1) {
return false;
}
return true;
});
const isAllSelected = selectItemsComputed.value
.every((itemSelected) => (getItemIndex(totalItems.value, itemSelected, itemsKey.value) !== -1));
return isAllSelected ? 'allSelected' : 'partSelected';
}

Expand All @@ -79,7 +73,7 @@ export default function usePageItems(
const isSelected = selectItemsComputed.value.findIndex((selectItem) => {
const itemDeepCloned = { ...item };
delete itemDeepCloned.index;
return JSON.stringify(selectItem) === JSON.stringify(itemDeepCloned);
return areItemsEqual(selectItem, itemDeepCloned, itemsKey.value)
}) !== -1;
return { checkbox: isSelected, ...item };
});
Expand Down
9 changes: 5 additions & 4 deletions src/hooks/useTotalItems.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import {
Ref, computed, ComputedRef, watch,
} from 'vue';
import type { Item, FilterOption } from '../types/main';
import type { Item, FilterOption, ItemKey } from '../types/main';
import type { ClientSortOptions, EmitsEventName } from '../types/internal';
import { getItemValue } from '../utils';
import { areItemsEqual, getItemValue } from '../utils';

export default function useTotalItems(
clientSortOptions: Ref<ClientSortOptions | null>,
Expand All @@ -16,6 +16,7 @@ export default function useTotalItems(
serverItemsLength: Ref<number>,
multiSort: Ref<boolean>,
emits: (event: EmitsEventName, ...args: any[]) => void,
itemsKey: Ref<ItemKey>
) {
const generateSearchingTarget = (item: Item): string => {
if (typeof searchField.value === 'string' && searchField.value !== '') return getItemValue(searchField.value, item);
Expand Down Expand Up @@ -152,8 +153,8 @@ export default function useTotalItems(
selectItemsComputed.value = selectItemsArr;
emits('selectRow', item);
} else {
selectItemsComputed.value = selectItemsComputed.value.filter((selectedItem) => JSON.stringify(selectedItem)
!== JSON.stringify(item));
selectItemsComputed.value = selectItemsComputed.value
.filter((selectedItem) => !areItemsEqual(selectedItem, item, itemsKey.value));
emits('deselectRow', item);
}
};
Expand Down
47 changes: 30 additions & 17 deletions src/modes/Client.vue
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
<span>search value: </span>
<input type="text" v-model="searchValue">
<div>
<button @click="refresh">Refresh</button>
<DataTable
table-node-id="my-table"
v-model:items-selected="itemsSelected"
Expand Down Expand Up @@ -44,6 +45,8 @@
@update-page-items="updateItems"
@update-total-items="updateTotalItems"
show-index-symbol="$"
items-key="id"
v-model:items-expanded="expandedItems"
>
<!-- <template #customize-headers>
<thead class="my-static-header">
Expand Down Expand Up @@ -138,6 +141,7 @@ const switchToNested = () => {
items.value = mockClientNestedItems(100);
};
const headers: Header[] = [
{ text: "ID", value: "id" },
{ text: "Name", value: "name" },
{ text: "TEAM", value: "team"},
{ text: "NUMBER", value: "number", sortable: true},
Expand All @@ -146,6 +150,7 @@ const headers: Header[] = [
{ text: "WEIGHT (lbs)", value: "indicator.weight", sortable: true},
{ text: "LAST ATTENDED", value: "lastAttended", width: 200},
{ text: "COUNTRY", value: "country"},
{ text: "LAST UPDATE", value: "updated"},
];

// const headers: Header[] = headersMocked;
Expand All @@ -164,24 +169,31 @@ const updateTotalItems = (items: Item[]) => {
console.log('total items');
console.log(JSON.stringify(items));
};
const initialItems = [
{ id: '1', name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ id: '2', name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ id: '3', name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ id: '4', name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ id: '5', name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
{ id: '6', name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ id: '7', name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ id: '8', name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ id: '9', name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ id: '10', name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
{ id: '11', name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ id: '12', name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ id: '13', name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ id: '14', name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ id: '15', name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
]
function getItems() {
return initialItems.map(item => ({...item, updated: new Date() }))
}
const items = ref<Item[]>(getItems());
async function refresh() {
items.value = getItems()
}

const items = ref<Item[]>([
{ name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
{ name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
{ name: "Stephen Curry", firstName: "GSW", number: 30, position: 'G', indicator: {"height": '6-2', "weight": 185}, lastAttended: "Davidson", country: "USA"},
{ name: "Kevin Durant", firstName: "BKN", number: 7, position: 'F', indicator: {"height": '6-10', "weight": 240}, lastAttended: "Texas-Austin", country: "USA"},
{ name: "Lebron James", firstName: "LAL", number: 7, position: 'F', indicator: {"height": '6-9', "weight": 185}, lastAttended: "St. Vincent-St. Mary HS (OH)", country: "USA"},
{ name: "Giannis Antetokounmpo", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 242}, lastAttended: "Filathlitikos", country: "Greece"},
{ name: "HC", firstName: "MIL", number: 34, position: 'F', indicator: {"height": '6-11', "weight": 243}, lastAttended: "Filathlitikos", country: "Greece"},
]);

// const items = ref<Item[]>(mockClientItems());

Expand All @@ -196,6 +208,7 @@ const items = ref<Item[]>([
// ];

const itemsSelected = ref<Item[]>([items.value[1]]);
const expandedItems = ref<Item[]>([items.value[1]]);

const showItem = (item: ClickRowArgument) => {
console.log('item 111');
Expand Down
15 changes: 14 additions & 1 deletion src/propsWithDefault.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type {
SortType, Item, ServerOptions, FilterOption,
HeaderItemClassNameFunction, BodyItemClassNameFunction, BodyRowClassNameFunction,
TextDirection,
ItemKeyFn,
} from './types/main';
import type { ClickEventType } from './types/internal';

Expand Down Expand Up @@ -195,8 +196,20 @@ export default {
type: String,
default: '#',
},
itemsExpanded: {
type: Array as PropType<Item[]>,
default: [],
},
/** String object key('id' it's the most common)
* or Function that returns a unique value for a row.
* It improves the tracking of expanded and checked rows.
*/
itemsKey: {
type: [Function as PropType<ItemKeyFn>, String],
default: undefined,
},
preventContextMenuRow: {
type: Boolean,
default: true
}
},
};
2 changes: 1 addition & 1 deletion src/types/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,4 @@ export type ClickEventType = 'single' | 'double'
export type MultipleSelectStatus = 'allSelected' | 'noneSelected' | 'partSelected'

// eslint-disable-next-line max-len
export type EmitsEventName = 'clickRow' | 'selectRow' | 'deselectRow' | 'expandRow' | 'updateSort' | 'update:itemsSelected' | 'update:serverOptions' | 'updateFilter' | 'updatePageItems' | 'updateTotalItems' | 'selectAll'
export type EmitsEventName = 'clickRow' | 'selectRow' | 'deselectRow' | 'expandRow' | 'updateSort' | 'update:itemsSelected' | 'update:serverOptions' | 'updateFilter' | 'updatePageItems' | 'updateTotalItems' | 'selectAll' | 'update:itemsExpanded'
3 changes: 3 additions & 0 deletions src/types/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,6 @@ export type BodyRowClassNameFunction = (item: Item, rowNumber: number) => string
export type BodyItemClassNameFunction = (column: string, rowNumber: number) => string

export type TextDirection = 'center' | 'left' | 'right'

export type ItemKeyFn = (item: Item) => string | number
export type ItemKey = ItemKeyFn | string | undefined
23 changes: 22 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Item } from './types/main';
import type { Item, ItemKey } from './types/main';

export function getItemValue(column: string, item: Item) {
if (column.includes('.')) {
Expand Down Expand Up @@ -28,3 +28,24 @@ export function generateColumnContent(column: string, item: Item) {
const content = getItemValue(column, item);
return Array.isArray(content) ? content.join(',') : content;
}

function getComparatorFn(itemToCompare: Item, key: ItemKey) {
let comparatorFunction = (item: Item) => JSON.stringify(item) === JSON.stringify(itemToCompare)

if (key && typeof key === 'function') {
comparatorFunction = (item: Item) => key(item) === key(itemToCompare)
}
if (key && typeof key === 'string') {
comparatorFunction = (item: Item) => getItemValue(key, item) === getItemValue(key, itemToCompare)
}
return comparatorFunction
}

export function getItemIndex(allItems: Item[], item: Item, key: ItemKey) {
return allItems.findIndex(getComparatorFn(item, key));
}

export function areItemsEqual(item: Item, itemToCompare: Item, key: ItemKey) {
const areEqualsFn = getComparatorFn(item, key)
return areEqualsFn(itemToCompare);
}
Loading