From 9c13de61c608bb0786d5d455f27ede967caace8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=A1s=20C=C3=A1mera?= <81186472+ncamera@users.noreply.github.com> Date: Wed, 3 Jul 2024 18:06:15 -0300 Subject: [PATCH] Add methods and callbacks in the Action List control (#355) * Add support to removeItem method in the Action List control * Add "fixItemCallback" Prop Callback that is executed when and item requests to be fixed/unfixed. * Add support to "addItem" method in the Action List control * Don't fire itemClick if the group's caption is not interactable * Show additional actions on item focus * Check if the item still exists before applying removeElement * Fix addItem implementation --- .../action-list/action-list-render.tsx | 133 ++++++++++++++++-- .../action-list-item/action-list-item.scss | 2 +- 2 files changed, 125 insertions(+), 10 deletions(-) diff --git a/src/components/action-list/action-list-render.tsx b/src/components/action-list/action-list-render.tsx index d6710bd9..ba4b577a 100644 --- a/src/components/action-list/action-list-render.tsx +++ b/src/components/action-list/action-list-render.tsx @@ -166,8 +166,6 @@ const defaultSortItemsCallback = (subModel: ActionListItemModel[]): void => { }); }; -// type ImmediateFilter = "immediate" | "debounced" | undefined; - @Component({ tag: "ch-action-list-render", styleUrl: "action-list-render.scss", @@ -206,6 +204,16 @@ export class ChActionListRender { */ @Prop() readonly editableItems: boolean = DEFAULT_EDITABLE_ITEMS_VALUE; + /** + * Callback that is executed when and item requests to be fixed/unfixed. + * If the callback is not defined, the item will be fixed/unfixed without + * further confirmation. + */ + @Prop() readonly fixItemCallback?: ( + itemInfo: ActionListItemActionable, + newFixedValue: boolean + ) => Promise; + /** * This property lets you define the model of the control. */ @@ -377,9 +385,66 @@ export class ChActionListRender { /** * Fired when an item is clicked and `selection === "none"`. + * Applies for items that have `type === "actionable"` or + * (`type === "group"` and `expandable === true`) */ @Event() itemClick: EventEmitter; + /** + * Adds an item in the control. + * + * If the item already exists, the operation is canceled. + * + * If the `groupParentId` property is specified the item is added in the + * group determined by `groupParentId`. It only works if the item to add + * has `type === "actionable"` + */ + @Method() + async addItem( + itemInfo: ActionListItemModel, + groupParentId?: string + ): Promise { + // Already exists + if (this.#flattenedModel.get(itemInfo.id)) { + return; + } + + if (groupParentId) { + const parentGroup = this.#flattenedModel.get(groupParentId); + + // The parent group does not exists or it isn't a group + if ( + !parentGroup || + parentGroup.item.type !== "group" || + itemInfo.type !== "actionable" + ) { + return; + } + + parentGroup.item.items.push(itemInfo); + this.#flattenedModel.set(itemInfo.id, { + item: itemInfo, + parentItem: parentGroup.item + }); + + // Sort items in parent model + this.#sortModel(parentGroup.item.items); + } + // Item is placed at the root + else { + this.model.push(itemInfo); + this.#flattenedModel.set(itemInfo.id, { + item: itemInfo, + root: this.model + }); + + // Sort items in parent model + this.#sortModel(this.model); + } + + forceUpdate(this); + } + /** * Given a list of ids, it returns an array of the items that exists in the * given list. @@ -391,6 +456,29 @@ export class ChActionListRender { return this.#getItemsInfo(itemsId); } + /** + * Remove the item and all its descendants from the control. + */ + @Method() + async removeItem(itemId: string) { + const itemUIModel = this.#flattenedModel.get(itemId); + + if (!itemUIModel) { + return; + } + + // Remove all descendants + if (itemUIModel.item.type === "group") { + const items = itemUIModel.item.items; + + items.forEach(item => { + this.#flattenedModel.delete(item.id); + }); + } + + this.#removeItem(itemUIModel); + } + #getItemsInfo = (itemsId: string[]): ActionListItemModelExtended[] => { const actionListItemsInfo: ActionListItemModelExtended[] = []; @@ -413,7 +501,25 @@ export class ChActionListRender { const itemUIModel = this.#flattenedModel.get(detail.itemId); const itemInfo = itemUIModel.item as ActionListItemActionable; - itemInfo.fixed = detail.value; + + if (!this.fixItemCallback) { + this.#updateItemFix(itemUIModel, itemInfo, detail.value); + return; + } + + this.fixItemCallback(itemInfo, detail.value).then(acceptChange => { + if (acceptChange) { + this.#updateItemFix(itemUIModel, itemInfo, detail.value); + } + }); + } + + #updateItemFix = ( + itemUIModel: ActionListItemModelExtended, + itemInfo: ActionListItemActionable, + newFixedValue: boolean + ) => { + itemInfo.fixed = newFixedValue; // Sort items in parent model this.#sortModel( @@ -423,7 +529,7 @@ export class ChActionListRender { // Queue a re-render to update the fixed binding and the order of the items forceUpdate(this); - } + }; @Listen("remove") onRemove(event: ChActionListItemCustomEvent) { @@ -491,6 +597,9 @@ export class ChActionListRender { const itemInfo = this.#getItemOrGroupInfo(actionListItemOrGroup.id); this.#checkIfMustExpandCollapseGroup(itemInfo); + if (itemInfo.type === "group" && !itemInfo.expandable) { + return; + } this.itemClick.emit(this.#flattenedModel.get(itemInfo.id)); }; @@ -594,15 +703,21 @@ export class ChActionListRender { const parentArray = (itemUIModel as ActionListItemModelExtendedRoot).root ?? (itemUIModel as ActionListItemModelExtendedGroup).parentItem.items; - const itemInfo = itemUIModel.item as ActionListItemActionable; + const itemToRemoveId = itemUIModel.item.id; const itemToRemoveIndex = parentArray.findIndex( - el => (el as ActionListItemActionable).id === itemInfo.id + el => el.id === itemToRemoveId ); - // Remove the UI model from the previous parent. The equality function - // must be by index, not by object reference - removeElement(parentArray, itemToRemoveIndex); + // In some situations, the user could remove the item before the + // "removeItemCallback" promise is resolved + if (itemToRemoveIndex > -1) { + // Remove the UI model from the previous parent. The equality function + // must be by index, not by object reference + removeElement(parentArray, itemToRemoveIndex); + } + + this.#flattenedModel.delete(itemToRemoveId); // Queue a re-render forceUpdate(this); diff --git a/src/components/action-list/internal/action-list-item/action-list-item.scss b/src/components/action-list/internal/action-list-item/action-list-item.scss index b10dc896..5ce8237a 100644 --- a/src/components/action-list/internal/action-list-item/action-list-item.scss +++ b/src/components/action-list/internal/action-list-item/action-list-item.scss @@ -86,7 +86,7 @@ // - - - - - - - - - - - - - - - - - - - - // Additional items // - - - - - - - - - - - - - - - - - - - - -:host(:not(:hover)) .show-on-mouse-hover { +:host(:not(:hover):not(:focus-within)) .show-on-mouse-hover { display: none; }