Skip to content

Commit

Permalink
* list: support to set active items in list manaully.
Browse files Browse the repository at this point in the history
  • Loading branch information
catouse committed Dec 5, 2023
1 parent 9a29cc7 commit 0343291
Show file tree
Hide file tree
Showing 5 changed files with 197 additions and 29 deletions.
107 changes: 100 additions & 7 deletions lib/list/src/component/list.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {$, HElement, classes, fetchData, mergeProps, removeUndefinedProps} from '@zui/core';
import {$, Computed, HElement, classes, fetchData, mergeProps, removeUndefinedProps} from '@zui/core';
import {CommonList} from '@zui/common-list/src/component';
import {Listitem} from './listitem';

Expand All @@ -24,10 +24,26 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta

protected declare _hasCheckbox: boolean;

protected _activeSet = new Computed<Set<string>>(() => {
const map = new Set<string>();
const {active} = this.props;
if (Array.isArray(active)) {
active.forEach(x => map.add(x));
} else if (typeof active === 'string') {
map.add(active);
} else if (active) {
Object.keys(active).forEach(x => active[x] && map.add(x));
}
const {activeMap} = this.state;
Object.keys(activeMap).forEach(x => activeMap[x] ? map.add(x) : map.delete(x));
return map;
}, () => [this.state.activeMap, this.props.active]);

constructor(props: P) {
super(props);
this.state = {
checked: {},
activeMap: {},
} as S;
}

Expand Down Expand Up @@ -86,14 +102,14 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta
return this._renderedItems.every(({key}, index) => this.isChecked(key!, index) === true);
}

toggleAllChecked(checked?: boolean): void {
toggleAllChecked(checked?: boolean) {
if (checked === undefined) {
checked = !this.isAllChecked();
}
this.toggleChecked(this._renderedItems.map(x => x.key!), checked);
return this.toggleChecked(this._renderedItems.map(x => x.key!), checked);
}

toggleChecked(keyOrChange: ItemKey | ItemKey[] | Record<ItemKey, CheckedType>, checked?: boolean): void {
async toggleChecked(keyOrChange: ItemKey | ItemKey[] | Record<ItemKey, CheckedType>, checked?: boolean) {
let change: Record<ItemKey, CheckedType>;
if (Array.isArray(keyOrChange)) {
if (!keyOrChange.length) {
Expand All @@ -119,12 +135,12 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta
return;
}

this.setState(prevState => ({
await this.changeState(prevState => ({
checked: {
...prevState.checked,
...change,
},
}), () => {
} as Partial<S>), () => {
const checkState = this.state.checked;
this.props.onCheck?.call(this, change, Object.keys(checkState).filter(x => checkState[x] === true));
});
Expand All @@ -139,6 +155,77 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta
}, []);
}

isActive(key: string | Item) {
if (typeof key === 'object') {
key = key.key!;
}
return this._activeSet.cache.has(key);
}

getActiveKeys() {
return [...this._activeSet.value];
}

getActiveKey() {
return this.getActiveKeys()[0];
}

async toggleActive(keys: string | string[], active?: boolean) {
if (typeof keys === 'string') {
keys = [keys];
}
if (!keys.length) {
return;
}
active = active ?? !this.isActive(keys[0]);
await this.changeState(prevState => {
const activeMap = this.props.multipleActive ? (keys as string[]).reduce<Record<string, boolean>>((map, key) => {
map[key] = active!;
return map;
}, {...prevState.activeMap}) : {[keys[0]]: active!};
return {activeMap} as Partial<S>;
}, () => {
this.props.onActive?.call(this, keys as string[], active!);
});
}

getNextItem(key: string | undefined, condition?: (item: Item, index: number) => boolean, step = 1, items: Item[] | undefined = undefined): Item | undefined {
items = items || this._renderedItems;
if (key === undefined) {
return items.at(step ? 0 : -1);
}
const count = items.length;
let index = items.findIndex(x => x.key === key);
if (index < 0 || count < 2) {
return items.at(step ? 0 : -1);
}
let checkCount = 0;
condition = condition || ((x) => x.type === 'item' && !x.disabled);
while (checkCount < count) {
index = (index + step + count) % count;
const nextItem = items[index];
if (nextItem && condition.call(this, nextItem, index)) {
return nextItem;
}
checkCount++;
}
}

getPrevItem(key: string | undefined, condition?: (item: Item, index: number) => boolean): Item | undefined {
return this.getNextItem(key, condition, -1);
}

activeNext(condition?: (item: Item, index: number) => boolean, step = 1) {
const nextItem = this.getNextItem(this.getActiveKey(), condition, step);
if (nextItem) {
this.toggleActive(nextItem.key!);
}
}

activePrev(condition?: (item: Item, index: number) => boolean) {
this.activeNext(condition, -1);
}

protected _afterRender(firstRender: boolean) {
this.props.afterRender?.call(this, firstRender);
}
Expand Down Expand Up @@ -168,10 +255,13 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta
if (typeof checkbox === 'object') {
renderedItem.checkbox = renderedItem.checkbox ? $.extend({}, checkbox, renderedItem.checkbox) : checkbox;
}
if (props.activeOnChecked && renderedItem.checked) {
if (props.activeOnChecked && renderedItem.checked === true) {
renderedItem.active = true;
}
}
if (renderedItem.active === undefined && this.isActive(renderedItem)) {
renderedItem.active = true;
}
}

if (renderedItem.icon) {
Expand Down Expand Up @@ -229,6 +319,9 @@ export class List<P extends ListProps = ListProps, S extends ListState = ListSta
}

protected _getChildren(props: RenderableProps<P>): ComponentChildren {
this._hasIcons = false;
this._hasCheckbox = false;
this._activeSet.compute();
const children = super._getChildren(props) as ComponentChild[];
const {loadFailed} = this.state;
if (loadFailed) {
Expand Down
111 changes: 89 additions & 22 deletions lib/list/src/component/nested-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,8 +58,9 @@ function parentKeys(keyPath: string) {
}, []);
}

function initItemMap(items: Item[], itemKey: string | undefined, map: Map<string, ItemInfo> = new Map(), level = 0, parent?: ItemInfo) {
items.forEach((item, index) => {

function reduceNestedItems<T>(items: Item[], itemKey: string | undefined, reducer: (previousValue: T, info: ItemInfo) => T, initialValue: T, level = 0, parent?: ItemInfo): T {
return items.reduce((currentValue, item, index) => {
const key = String((itemKey ? item[itemKey] : item.key) ?? (item.key ?? index));
const keyPath = parent ? `${parent.keyPath}:${key}` : key;
const itemInfo = {
Expand All @@ -74,12 +75,19 @@ function initItemMap(items: Item[], itemKey: string | undefined, map: Map<string
if (parent) {
parent.children.push(itemInfo);
}
map.set(keyPath, itemInfo);
currentValue = reducer(currentValue, itemInfo);
if (Array.isArray(item.items)) {
initItemMap(item.items as Item[], itemKey, map, level + 1, itemInfo);
return reduceNestedItems(item.items as Item[], itemKey, reducer, currentValue, level + 1, itemInfo);
}
});
return map;
return currentValue;
}, initialValue);
}

function initItemMap(items: Item[], itemKey: string | undefined, map: Map<string, ItemInfo> = new Map()) {
return reduceNestedItems(items, itemKey, (currentMap, info) => {
currentMap.set(info.keyPath, info);
return currentMap;
}, map);
}

export class NestedList<P extends NestedListProps = NestedListProps, S extends NestedListState = NestedListState> extends List<P, S> {
Expand Down Expand Up @@ -191,7 +199,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
return !!(nestedShow[keyPath] ?? this.state.defaultShow);
}

toggle(keyPath: string, toggle?: boolean) {
async toggle(keyPath: string, toggle?: boolean) {
const isExpanded = this.isExpanded(keyPath);
if (toggle === isExpanded) {
return;
Expand All @@ -206,7 +214,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
if (nestedShow !== undefined) {
return;
}
return this.setState(prevState => {
await this.changeState(prevState => {
const newNestedShow: Record<ItemKey, boolean> = {
...prevState.nestedShow,
[keyPath]: toggle!,
Expand All @@ -227,7 +235,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
map[key] = toggle!;
return map;
}, newNestedShow) : newNestedShow,
};
} as Partial<S>;
}, this._preserveState);
}

Expand Down Expand Up @@ -255,7 +263,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
return this.props.checkedState![`${this.props.parentKey}:${key}`] ?? (item.checked as CheckedType) ?? defaultChecked;
}

toggleChecked(keyOrChange: ItemKey | ItemKey[] | Record<ItemKey, CheckedType>, checked?: CheckedType): void {
async toggleChecked(keyOrChange: ItemKey | ItemKey[] | Record<ItemKey, CheckedType>, checked?: CheckedType) {
let change: Record<ItemKey, CheckedType>;
if (Array.isArray(keyOrChange)) {
if (!keyOrChange.length) {
Expand All @@ -281,7 +289,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
}
if (this.isRoot) {
const map = this.getItemMap();
this.setState(({checked: prevChecked}) => {
await this.changeState(({checked: prevChecked}) => {
const isChecked = (item: ItemInfo) => {
return change[item.keyPath] ?? prevChecked[item.keyPath] ?? item.data.checked ?? false;
};
Expand Down Expand Up @@ -313,19 +321,76 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
...prevChecked,
...change,
},
};
} as Partial<S>;
}, () => {
const checkState = this.state.checked;
this.props.onCheck?.call(this, change, Object.keys(checkState).filter(x => checkState[x] === true));
});
} else {
const {parentKey, onCheck} = this.props;
const nestedChange = Object.keys(change).reduce<Record<string, CheckedType>>((map, key) => {
map[`${parentKey !== undefined ? `${parentKey}:` : ''}${key}`] = change[key];
return map;
}, {});
onCheck!.call(this, nestedChange, []);
return;
}

const {parentKey, onCheck} = this.props;
const nestedChange = Object.keys(change).reduce<Record<string, CheckedType>>((map, key) => {
map[`${parentKey !== undefined ? `${parentKey}:` : ''}${key}`] = change[key];
return map;
}, {});
onCheck!.call(this, nestedChange, []);
}

isActive(keyPath: string | Item) {
if (typeof keyPath === 'object') {
const keyOrKeyPath = (keyPath._keyPath ?? keyPath.key) as (string | undefined);
if (keyOrKeyPath === undefined) {
return false;
}
keyPath = keyOrKeyPath;
}
const parentKey = this.props.parentKey!;
if (!this.isRoot && !keyPath.startsWith(parentKey + ':')) {
keyPath = `${parentKey}:${keyPath}`;
}
return this._activeSet.cache.has(keyPath);
}

async toggleActive(keys: string | string[], active?: boolean) {
if (typeof keys === 'string') {
keys = [keys];
}
if (this.isRoot) {
await super.toggleActive(keys, active);
if (this.props.toggleOnActive) {
(keys as string[]).forEach(key => {
console.log('> active', key, this.isExpanded(key));
if (this.isActive(key) && !this.isExpanded(key)) {
this.toggle(key, true);
}
});
return;
}
}

this.props.onActive?.call(this, keys, active ?? !this.isActive(keys[0]));
}

activeNext(condition?: (item: Item, index: number) => boolean, step = 1) {
const nextItem = this.getNextItem(this.getActiveKey(), condition, step);
if (nextItem) {
this.toggleActive(nextItem._keyPath as string);
}
}

getNextItem(key: string | undefined, condition?: (item: Item, index: number) => boolean, step = 1, items: Item[] | undefined = undefined): Item | undefined {
items = items || reduceNestedItems<Item[]>(this._items, this.props.itemKey, (list, info) => {
list.push({
_keyPath: info.keyPath,
type: 'item',
...info.data,
...this._renderedItemMap.get(info.keyPath),
key: info.keyPath,
});
return list;
}, []);
return super.getNextItem(key, condition, step, items);
}

protected _afterRender(firstRender: boolean): void {
Expand Down Expand Up @@ -358,6 +423,7 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
parentKey,
level = 0,
} = props;
const {isRoot} = this;
return mergeProps(((this.constructor as typeof NestedList).inheritNestedProps.reduce<Record<string, unknown>>((propMap, key) => {
propMap[key] = props[key as keyof P];
return propMap;
Expand All @@ -370,9 +436,10 @@ export class NestedList<P extends NestedListProps = NestedListProps, S extends N
nestedShow: this.nestedShow,
defaultNestedShow: this.state.defaultShow,
checkedState: props.checkedState || this.state.checked,
onCheck: this.isRoot ? this._handleNestedCheck : props.onCheck,
onToggle: this.isRoot ? this._handleNestedToggle : props.onToggle,
beforeRenderItem: this.isRoot ? this._beforeRenderNestedItem : props.beforeRenderItem,
onCheck: isRoot ? this._handleNestedCheck : props.onCheck,
onToggle: isRoot ? this._handleNestedToggle : props.onToggle,
beforeRenderItem: isRoot ? this._beforeRenderNestedItem : props.beforeRenderItem,
active: isRoot ? this.getActiveKeys() : props.active,
}, item.listProps);
}

Expand Down
3 changes: 3 additions & 0 deletions lib/list/src/style/listitem.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
.listitem {
@apply -flex -flex-row -flex-nowrap -py-1 -gap-2 -items-center;
}
.listitem.active {
@apply -bg-primary-50;
}

.item-icon,
.item-trailing-icon,
Expand Down
4 changes: 4 additions & 0 deletions lib/list/src/types/list-props.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ export interface ListProps<T extends Item = ListItem> extends CommonListProps<T>
checkbox?: boolean | CheckboxProps;
checkOnClick?: boolean | 'any' | (string & {});
activeOnChecked?: boolean;
active?: string | string[] | Record<string, boolean>;
multipleActive?: boolean;
activeOnHover?: boolean;
onActive?: (keys: string[], active: boolean) => void;
onCheck?: (change: Record<ItemKey, CheckedType>, checks: ItemKey[]) => void;
onLoad?: (items: T[]) => void | T[];
onLoadFail?: CustomContentType | ((error: Error) => CustomContentType | void);
Expand Down
1 change: 1 addition & 0 deletions lib/list/src/types/list-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ export interface ListState<T extends Item = ListItem> {
loading?: boolean;
loadFailed?: CustomContentType;
checked: Record<string, CheckedType>;
activeMap: Record<string, boolean>;
}

0 comments on commit 0343291

Please sign in to comment.