diff --git a/client/src/app/core/ui-services/tree.service.ts b/client/src/app/core/ui-services/tree.service.ts index 25e766573..d4f6c21eb 100644 --- a/client/src/app/core/ui-services/tree.service.ts +++ b/client/src/app/core/ui-services/tree.service.ts @@ -31,21 +31,23 @@ export interface OSTreeNode extends TreeNodeWithoutItem { * Interface which defines the nodes for the sorting trees. * * Contains information like - * name: The name of the node. + * item: The base item the node is created from. * level: The level of the node. The higher, the deeper the level. * position: The position in the array of the node. * isExpanded: Boolean if the node is expanded. * expandable: Boolean if the node is expandable. * id: The id of the node. + * filtered: Optional boolean to check, if the node is filtered. */ -export interface FlatNode { - name: string; +export interface FlatNode { + item: T; level: number; position?: number; isExpanded?: boolean; isSeen: boolean; expandable: boolean; id: number; + filtered?: boolean; } /** @@ -98,11 +100,11 @@ export class TreeService { items: T[], weightKey: keyof T, parentKey: keyof T - ): FlatNode[] { + ): FlatNode[] { const tree = this.makeTree(items, weightKey, parentKey); - const flatNodes: FlatNode[] = []; + const flatNodes: FlatNode[] = []; for (const node of tree) { - flatNodes.push(...this.makePartialFlatTree(node, 0, [])); + flatNodes.push(...this.makePartialFlatTree(node, 0)); } for (let i = 0; i < flatNodes.length; ++i) { flatNodes[i].position = i; @@ -117,7 +119,7 @@ export class TreeService { * * @returns The tree with nested information. */ - public makeTreeFromFlatTree(nodes: FlatNode[]): TreeIdNode[] { + public makeTreeFromFlatTree(nodes: FlatNode[]): TreeIdNode[] { const basicTree: TreeIdNode[] = []; for (let i = 0; i < nodes.length; ) { @@ -294,30 +296,29 @@ export class TreeService { /** * Helper function to go recursively through the children of given node. * - * @param item - * @param level + * @param item The current item from which the flat node will be created. + * @param level The level the flat node will be. + * @param additionalTag Optional: A key of the items. If this parameter is set, the nodes will have a tag for filtering them. * * @returns An array containing the parent node with all its children. */ private makePartialFlatTree( item: OSTreeNode, - level: number, - parents: FlatNode[] - ): FlatNode[] { + level: number + ): FlatNode[] { const children = item.children; - const node: FlatNode = { + const node: FlatNode = { id: item.id, - name: item.name, + item: item.item, expandable: !!children, isExpanded: !!children, level: level, isSeen: true }; - const flatNodes: FlatNode[] = [node]; + const flatNodes: FlatNode[] = [node]; if (children) { - parents.push(node); for (const child of children) { - flatNodes.push(...this.makePartialFlatTree(child, level + 1, parents)); + flatNodes.push(...this.makePartialFlatTree(child, level + 1)); } } return flatNodes; @@ -333,9 +334,9 @@ export class TreeService { * * @returns `OSTreeNodeWithOutItem` */ - private buildBranchFromFlatTree( - node: FlatNode, - nodes: FlatNode[], + private buildBranchFromFlatTree( + node: FlatNode, + nodes: FlatNode[], length: number ): { node: TreeIdNode; length: number } { const children = []; diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.html b/client/src/app/shared/components/sorting-tree/sorting-tree.component.html index 2dddcf986..f1cbe3584 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.html +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.html @@ -36,10 +36,12 @@ chevron_right - {{ node.name }} +
+ [style.margin-left]="placeholderLevel * 40 + 'px'" + *cdkDragPlaceholder> diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.scss b/client/src/app/shared/components/sorting-tree/sorting-tree.component.scss index bb254c0b9..9f13ad79b 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.scss +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.scss @@ -1,43 +1,55 @@ @import '../../../../assets/styles/drag.scss'; +@import '~@angular/material/theming'; -cdk-tree-node { - margin-bottom: 5px; - display: flex; - align-items: center; - cursor: pointer; +@mixin os-sorting-tree-style($theme) { + $background: map-get($theme, background); - div { - width: 100%; + cdk-tree-node { + margin: 3px 0; + display: flex; + align-items: center; + cursor: pointer; - mat-icon { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + div { + display: inherit; + align-items: inherit; + width: 100%; + + mat-icon { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + } } } -} -// Overwrite the preview -.cdk-drag-preview { - box-shadow: none; - background-color: unset; + // Overwrite the preview + .cdk-drag-preview { + box-shadow: none !important; + background-color: unset !important; + border-radius: 6px !important; - div { - box-sizing: border-box; - background-color: white; - border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), - 0 3px 14px 2px rgba(0, 0, 0, 0.12); + div { + box-sizing: border-box; + background-color: mat-color($background, background); + border-radius: 4px; + box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), 0 8px 10px 1px rgba(0, 0, 0, 0.14), + 0 3px 14px 2px rgba(0, 0, 0, 0.12); + } + + .mat-icon-button { + visibility: hidden !important; + } + } + + // Overwrite the placeholder + .cdk-drag-placeholder { + opacity: 1 !important; + background: #ccc; + border: dotted 3px #999; + min-height: 42px; + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); + } + + .cdk-drop-list.cdk-drop-list-dragging .cdk-tree-node:not(.cdk-drag-placeholder) { + transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); } } - -// Overwrite the placeholder -.cdk-drag-placeholder { - opacity: 1; - background: #ccc; - border: dotted 3px #999; - min-height: 42px; - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} - -.cdk-drop-list.cdk-drop-list-dragging .cdk-tree-node:not(.cdk-drag-placeholder) { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} diff --git a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts index db795eb34..43c619bce 100644 --- a/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts +++ b/client/src/app/shared/components/sorting-tree/sorting-tree.component.ts @@ -1,8 +1,8 @@ -import { Component, OnInit, Input, OnDestroy, Output, EventEmitter } from '@angular/core'; - +import { Component, OnInit, Input, OnDestroy, Output, EventEmitter, ContentChild, TemplateRef } from '@angular/core'; import { FlatTreeControl } from '@angular/cdk/tree'; import { ArrayDataSource } from '@angular/cdk/collections'; import { CdkDragMove, CdkDragStart, CdkDragSortEvent } from '@angular/cdk/drag-drop'; + import { Observable, Subscription } from 'rxjs'; import { auditTime } from 'rxjs/operators'; @@ -25,7 +25,7 @@ enum Direction { * Interface which extends the `OSFlatNode`. * Containing further information like start- and next-position. */ -interface ExFlatNode extends FlatNode { +interface ExFlatNode extends FlatNode { startPosition: number; nextPosition: number; } @@ -56,12 +56,12 @@ export class SortingTreeComponent implemen /** * The data to build the tree */ - public osTreeData: FlatNode[] = []; + public osTreeData: FlatNode[] = []; /** * The tree control */ - public treeControl = new FlatTreeControl(node => node.level, node => node.expandable); + public treeControl = new FlatTreeControl>(node => node.level, node => node.expandable); /** * Source for the tree @@ -83,13 +83,23 @@ export class SortingTreeComponent implemen * Node with calculated next information. * Containing information like the position, when the drag starts and where it is in the moment. */ - public nextNode: ExFlatNode = null; + public nextNode: ExFlatNode = null; /** * Pointer for the move event */ private pointer: DragEvent = null; + /** + * Number, that holds the current visible nodes. + */ + private seenNodes: number; + + /** + * Function that will be used for filtering the nodes. + */ + private activeFilter: (node: T) => boolean; + /** * Subscription for the data store */ @@ -116,6 +126,8 @@ export class SortingTreeComponent implemen /** * Setter to get all models from data store. * It will create or replace the existing subscription. + * + * @param model Is the model the tree will be built of. */ @Input() public set model(model: Observable) { @@ -126,12 +138,53 @@ export class SortingTreeComponent implemen this.setSubscription(); } + /** + * Setter to listen for state changes, expanded or collapsed. + * + * @param nextState Is an event emitter that emits a boolean whether the nodes should expand or not. + */ + @Input() + public set stateChange(nextState: EventEmitter) { + nextState.subscribe((state: boolean) => { + if (state) { + this.expandAll(); + } else { + this.collapseAll(); + } + }); + } + + /** + * Setter to listen for filter change events. + * + * @param filter Is an event emitter that emits all active filters in an array. + */ + @Input() + public set filterChange(filter: EventEmitter<(node: T) => boolean>) { + filter.subscribe((value: (node: T) => boolean) => { + this.activeFilter = value; + this.checkActiveFilters(); + }); + } + /** * EventEmitter to send info if changes has been made. */ @Output() public hasChanged: EventEmitter = new EventEmitter(); + /** + * EventEmitter to emit the info about the currently shown nodes. + */ + @Output() + public visibleNodes: EventEmitter<[number, number]> = new EventEmitter<[number, number]>(); + + /** + * Reference to the template content. + */ + @ContentChild(TemplateRef) + public innerNode: TemplateRef; + /** * Constructor * @@ -158,7 +211,7 @@ export class SortingTreeComponent implemen * * @returns The parent node if available otherwise it returns null. */ - public getParentNode(node: FlatNode): FlatNode { + public getParentNode(node: FlatNode): FlatNode { const nodeIndex = this.osTreeData.indexOf(node); for (let i = nodeIndex - 1; i >= 0; --i) { @@ -178,11 +231,11 @@ export class SortingTreeComponent implemen * * @returns The node which is either the parent if not expanded or the next node. */ - private getExpandedParentNode(node: FlatNode): FlatNode { - const allParents = this.getAllParents(node); - for (let i = allParents.length - 1; i >= 0; --i) { - if (!allParents[i].isExpanded) { - return allParents[i]; + private getExpandedParentNode(node: FlatNode): FlatNode { + for (let i = node.position; i >= 0; --i) { + const treeNode = this.osTreeData[i]; + if (treeNode.isSeen && !treeNode.filtered) { + return treeNode; } } return node; @@ -195,7 +248,7 @@ export class SortingTreeComponent implemen * * @returns An array containing its parent and the parents of its parent. */ - private getAllParents(node: FlatNode): FlatNode[] { + private getAllParents(node: FlatNode): FlatNode[] { return this._getAllParents(node, []); } @@ -207,7 +260,7 @@ export class SortingTreeComponent implemen * * @returns An array containing all parents that are in relation to the given node. */ - private _getAllParents(node: FlatNode, array: FlatNode[]): FlatNode[] { + private _getAllParents(node: FlatNode, array: FlatNode[]): FlatNode[] { const parent = this.getParentNode(node); if (parent) { array.push(parent); @@ -224,9 +277,9 @@ export class SortingTreeComponent implemen * * @returns An array that contains all the nearest children. */ - public getChildNodes(node: FlatNode): FlatNode[] { + public getChildNodes(node: FlatNode): FlatNode[] { const nodeIndex = this.osTreeData.indexOf(node); - const childNodes: FlatNode[] = []; + const childNodes: FlatNode[] = []; if (nodeIndex < this.osTreeData.length - 1) { for (let i = nodeIndex + 1; i < this.osTreeData.length && this.osTreeData[i].level >= node.level + 1; ++i) { @@ -239,6 +292,17 @@ export class SortingTreeComponent implemen return childNodes; } + /** + * Function to search for all nodes under the given node, that are not filtered. + * + * @param node is the parent whose visible children should be returned. + * + * @returns An array containing all nodes that are children and not filtered. + */ + private getUnfilteredChildNodes(node: FlatNode): FlatNode[] { + return this.getChildNodes(node).filter(child => !child.filtered); + } + /** * Function to look for all nodes that are under the given node. * This includes not only the nearest children, but also the children of the children. @@ -247,7 +311,7 @@ export class SortingTreeComponent implemen * * @returns An array containing all the subnodes, inclusive the children of the children. */ - private getAllSubNodes(node: FlatNode): FlatNode[] { + private getAllSubNodes(node: FlatNode): FlatNode[] { return this._getAllSubNodes(node, []); } @@ -260,7 +324,7 @@ export class SortingTreeComponent implemen * * @returns An array containing all subnodes, inclusive the children of the children. */ - private _getAllSubNodes(node: FlatNode, array: FlatNode[]): FlatNode[] { + private _getAllSubNodes(node: FlatNode, array: FlatNode[]): FlatNode[] { array.push(node); for (const child of this.getChildNodes(node)) { this._getAllSubNodes(child, array); @@ -276,7 +340,7 @@ export class SortingTreeComponent implemen * * @returns The calculated position as number. */ - private getPositionOnScreen(node: FlatNode): number { + private getPositionOnScreen(node: FlatNode): number { let currentPosition = this.osTreeData.length; for (let i = this.osTreeData.length - 1; i >= 0; --i) { --currentPosition; @@ -297,7 +361,7 @@ export class SortingTreeComponent implemen * * @returns boolean if the node should render. Related to the state of the parent, if expanded or not. */ - public shouldRender(node: FlatNode): boolean { + public shouldRender(node: FlatNode): boolean { return node.isSeen; } @@ -306,7 +370,7 @@ export class SortingTreeComponent implemen * * @param node which is clicked. */ - public handleClick(node: FlatNode): void { + public handleClick(node: FlatNode): void { node.isExpanded = !node.isExpanded; if (node.isExpanded) { for (const child of this.getChildNodes(node)) { @@ -325,8 +389,8 @@ export class SortingTreeComponent implemen * * @param node is the node which should be shown again. */ - private showChildren(node: FlatNode): void { - node.isSeen = true; + private showChildren(node: FlatNode): void { + this.checkVisibility(node); if (node.expandable && node.isExpanded) { for (const child of this.getChildNodes(node)) { this.showChildren(child); @@ -338,14 +402,63 @@ export class SortingTreeComponent implemen * Function to check the visibility of moved nodes after moving them. * `Warning: Side Effects`: This function works with side effects. The changed nodes won't be returned! * - * @param nodes All affected nodes, that are either shown or not. + * @param node All affected nodes, that are either shown or not. */ - private checkVisibility(nodes: FlatNode[]): void { - if (this.getAllParents(nodes[0]).find(item => item.expandable && !item.isExpanded)) { - for (const child of nodes) { - child.isSeen = false; + private checkVisibility(node: FlatNode): void { + const shouldSee = !this.getAllParents(node).find(item => (item.expandable && !item.isExpanded) || !item.isSeen); + node.isSeen = !node.filtered && shouldSee; + } + + /** + * Find the next not filtered node. + * Necessary to get the next node even though it is not seen. + * + * @param node is the directly next neighbor of the moved node. + * @param direction define in which way it runs. + * + * @returns The next node with parameter `isSeen = true`. + */ + private getNextVisibleNode(node: FlatNode, direction: Direction.DOWNWARDS | Direction.UPWARDS): FlatNode { + if (node) { + switch (direction) { + case Direction.DOWNWARDS: + for (let i = node.position; i < this.osTreeData.length; ++i) { + if (!this.osTreeData[i].filtered) { + return this.osTreeData[i]; + } + } + break; + + case Direction.UPWARDS: + for (let i = node.position; i >= 0; --i) { + if (!this.osTreeData[i].filtered) { + return this.osTreeData[i]; + } + } + break; } } + return null; + } + + /** + * Function to get the last filtered child of a node. + * This is necessary to append moved nodes next to the last place of the corresponding parent. + * + * @param node is the node where it will start. + * + * @returns The node that is an filtered child or itself if there is no filtered child. + */ + private getTheLastInvisibleNode(node: FlatNode): FlatNode { + let result = node; + for (let i = node.position + 1; i < this.osTreeData.length && this.osTreeData[i].level >= node.level + 1; ++i) { + if (this.osTreeData[i].filtered) { + result = this.osTreeData[i]; + } else { + return result; + } + } + return node; } /** @@ -391,7 +504,7 @@ export class SortingTreeComponent implemen */ public startsDrag(event: CdkDragStart): void { this.removeSubscription(); - const draggedNode = event.source.data; + const draggedNode = >event.source.data; this.placeholderLevel = draggedNode.level; this.nextNode = { ...draggedNode, @@ -405,12 +518,11 @@ export class SortingTreeComponent implemen * * @param node Is the dropped node. */ - public onDrop(node: FlatNode): void { - const moving = this.getDirection(); + public onDrop(node: FlatNode): void { this.pointer = null; this.madeChanges(true); - this.moveItemToTree(node, node.position, this.nextPosition, this.placeholderLevel, moving.verticalMove); + this.moveItemToTree(node, node.position, this.nextPosition, this.placeholderLevel); } /** @@ -455,7 +567,13 @@ export class SortingTreeComponent implemen this.nextPosition = nextPosition; const corrector = direction.verticalMove === Direction.DOWNWARDS ? 0 : 1; - const possibleParent = this.osTreeData[nextPosition - corrector]; + let possibleParent = this.osTreeData[nextPosition - corrector]; + for (let i = 0; possibleParent && possibleParent.filtered; ++i) { + possibleParent = this.osTreeData[nextPosition - corrector - i]; + } + if (possibleParent) { + this.nextPosition = this.getTheLastInvisibleNode(possibleParent).position + corrector; + } switch (direction.horizontalMove) { case Direction.LEFT: if (this.nextNode.level > 0 || this.placeholderLevel > 0) { @@ -506,7 +624,7 @@ export class SortingTreeComponent implemen */ private findNextIndex( steps: number, - node: ExFlatNode, + node: ExFlatNode, verticalMove: Direction.DOWNWARDS | Direction.UPWARDS | Direction.NOWAY ): number { let currentPosition = this.osTreeData.length; @@ -519,7 +637,7 @@ export class SortingTreeComponent implemen } else { break; } - if (node.name === parent.name) { + if (node.item.getTitle() === parent.item.getTitle()) { --i; } } @@ -553,20 +671,23 @@ export class SortingTreeComponent implemen * @param nextLevel The next level the node should have. * @param verticalMove The direction of the movement in the vertical way. */ - private moveItemToTree( - node: FlatNode, - previousIndex: number, - nextIndex: number, - nextLevel: number, - verticalMove: Direction.UPWARDS | Direction.DOWNWARDS | Direction.NOWAY - ): void { + private moveItemToTree(node: FlatNode, previousIndex: number, nextIndex: number, nextLevel: number): void { + let verticalMove: string; + if (previousIndex < nextIndex) { + verticalMove = Direction.DOWNWARDS; + } else if (previousIndex > nextIndex) { + verticalMove = Direction.UPWARDS; + } else { + verticalMove = Direction.NOWAY; + } + // Get all affected nodes. const movedNodes = this.getAllSubNodes(node); const corrector = verticalMove === Direction.DOWNWARDS ? 0 : 1; const lastChildIndex = movedNodes[movedNodes.length - 1].position; // Get the neighbor above and below of the new index. - const nextNeighborAbove = this.osTreeData[nextIndex - corrector]; + const nextNeighborAbove = this.getNextVisibleNode(this.osTreeData[nextIndex - corrector], Direction.UPWARDS); const nextNeighborBelow = verticalMove !== Direction.NOWAY ? this.osTreeData[nextIndex - corrector + 1] @@ -579,12 +700,11 @@ export class SortingTreeComponent implemen // Check if the node was a subnode. if (node.level > 0) { - const previousNode = this.osTreeData[previousIndex - 1]; - const isMovedLowerLevel = - previousIndex === nextIndex && - nextLevel <= previousNode.level && - this.getChildNodes(previousNode).length === 1; - const isMovedAway = previousIndex !== nextIndex && this.getChildNodes(previousNode).length === 1; + // const previousNode = this.osTreeData[previousIndex - 1]; + const previousNode = this.getNextVisibleNode(this.osTreeData[previousIndex - 1], Direction.UPWARDS); + const onlyChild = this.getChildNodes(previousNode).length === 1; + const isMovedLowerLevel = previousIndex === nextIndex && nextLevel <= previousNode.level && onlyChild; + const isMovedAway = previousIndex !== nextIndex && onlyChild; // Check if the previous parent will have no children anymore. if (isMovedAway || isMovedLowerLevel) { @@ -595,32 +715,32 @@ export class SortingTreeComponent implemen // Check if the node becomes a subnode. if (nextLevel > 0) { + const noChildren = this.getUnfilteredChildNodes(nextNeighborAbove).length === 0; // Check if the new parent has not have any children before. - if (nextNeighborAbove.level + 1 === nextLevel && this.getChildNodes(nextNeighborAbove).length === 0) { + if (nextNeighborAbove.level + 1 === nextLevel && noChildren) { nextNeighborAbove.expandable = true; nextNeighborAbove.isExpanded = (!!this.getParentNode(nextNeighborAbove) && this.getParentNode(nextNeighborAbove).isExpanded) || - this.getChildNodes(nextNeighborAbove).length === 0 - ? true - : false; + noChildren; } } // Check if the neighbor below has a higher level than the moved node. - if (nextNeighborBelow && nextNeighborBelow.level === nextLevel + 1) { + if (nextNeighborBelow && nextNeighborBelow.level === nextLevel + 1 && !nextNeighborBelow.filtered) { // Check if the new neighbor above has the same level like the moved node. if (nextNeighborAbove.level === nextLevel) { nextNeighborAbove.expandable = false; nextNeighborAbove.isExpanded = false; } - - // Set the moved node to the new parent for the subnodes. node.expandable = true; node.isExpanded = true; } // Check if the neighbor below has a level equals to two or more higher than the moved node. - if (nextNeighborBelow && nextNeighborBelow.level >= nextLevel + 2) { + if ( + (nextNeighborBelow && nextNeighborBelow.level >= nextLevel + 2) || + (nextNeighborBelow.level === nextLevel + 1 && nextNeighborBelow.filtered) + ) { let found = false; for (let i = nextIndex + 1; i < this.osTreeData.length; ++i) { if (this.osTreeData[i].level <= nextLevel && node !== this.osTreeData[i]) { @@ -681,7 +801,9 @@ export class SortingTreeComponent implemen } // Check the visibility to prevent seeing nodes that are actually unseen. - this.checkVisibility(movedNodes); + for (const child of movedNodes) { + this.checkVisibility(child); + } // Set a new data source. this.dataSource = null; @@ -716,6 +838,7 @@ export class SortingTreeComponent implemen this.madeChanges(false); this.modelSubscription = this._model.pipe(auditTime(10)).subscribe(values => { this.osTreeData = this.treeService.makeFlatTree(values, this.weightKey, this.parentKey); + this.checkActiveFilters(); this.dataSource = new ArrayDataSource(this.osTreeData); }); } @@ -729,8 +852,71 @@ export class SortingTreeComponent implemen this.hasChanged.emit(hasChanged); } + /** + * Function to expand all nodes. + */ + private expandAll(): void { + for (const child of this.osTreeData) { + this.checkVisibility(child); + if (child.isSeen && child.expandable) { + child.isExpanded = true; + } + } + } + + /** + * Function to collapse all parent nodes and make their children invisible. + */ + private collapseAll(): void { + for (const child of this.osTreeData) { + child.isExpanded = false; + if (child.level > 0) { + child.isSeen = false; + } + } + } + + /** + * Function that iterates over all top level nodes and pass them to the next function + * to decide if they will be seen or filtered and whether they will be expandable. + */ + private checkActiveFilters(): void { + this.seenNodes = 0; + for (const node of this.osTreeData.filter(item => item.level === 0)) { + this.checkChildrenToBeFiltered(node); + } + this.visibleNodes.emit([this.seenNodes, this.osTreeData.length]); + } + + /** + * Function to check recursively the child nodes of a given node whether they will be filtered or if they should be seen. + * The result is necessary to decide whether the parent node is expandable or not. + * + * @param node is the inspected node. + * @param parent optional: If the node has a parent, it is necessary to see if this parent will be filtered or is seen. + * + * @returns A boolean which describes if the given node will be filtered. + */ + private checkChildrenToBeFiltered(node: FlatNode, parent?: FlatNode): boolean { + let result = false; + const willFiltered = this.activeFilter ? this.activeFilter(node.item) || (parent && parent.filtered) : false; + node.filtered = willFiltered; + const willSeen = !willFiltered && ((parent && parent.isSeen && parent.isExpanded) || !parent); + node.isSeen = willSeen; + if (willSeen) { + this.seenNodes += 1; + } + for (const child of this.getChildNodes(node)) { + if (!this.checkChildrenToBeFiltered(child, node)) { + result = true; + } + } + node.expandable = result; + return willFiltered; + } + /** * Function to check if a node has children. */ - public hasChild = (_: number, node: FlatNode) => node.expandable; + public hasChild = (_: number, node: FlatNode) => node.expandable; } diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.html b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.html index d3ff99ac2..4bc4861ce 100644 --- a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.html +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.html @@ -7,13 +7,59 @@

Sort agenda

- +
+
+ + +
+
+
Active filters
+
+ +
+
+
+ + +
+ + +
{{ 'Visibility' | translate }}
+
+ + {{ getIcon(option.name) }} {{ option.name | translate }} + +
+
+
+
+
+
+
+ You are currently seeing {{ seenNodes[0] }} of {{ seenNodes[1] }} items. + +
+ > + + {{ item.getTitle() }} + + {{ item.verboseType | translate }} {{ getIcon(item.verboseType) }} + + +
diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.scss b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.scss new file mode 100644 index 000000000..7e28eb507 --- /dev/null +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.scss @@ -0,0 +1,68 @@ +.sort-header { + display: block; + padding: 0 8px; + width: auto; + position: relative; + text-align: center; + + .button-menu { + display: inline-block; + } + + .left { + float: left; + } + + .right { + float: right; + } + + mat-drawer { + min-width: 320px; + } + + .sort-drawer-content { + text-align: left; + + .mat-button { + margin-left: 8px; + } + + .sort-grid { + display: inline-grid; + grid-template-columns: auto; + grid-template-rows: 20px 30px; + margin: 8px; + vertical-align: top; + line-height: 1.5; + + mat-checkbox:not(:last-child) { + margin-right: 8px; + } + } + } + + .current-filters { + display: inline-block; + + div { + display: inline; + } + } +} + +.current-nodes { + position: relative; + margin-bottom: 8px; +} + +.sort-node-icon { + margin: 0 12px 0 auto; + + mat-icon { + font-size: 18px; + width: 18px; + height: 18px; + vertical-align: middle; + } +} diff --git a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts index 10e99f833..1f2a784c2 100644 --- a/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts +++ b/client/src/app/site/agenda/components/agenda-sort/agenda-sort.component.ts @@ -1,9 +1,9 @@ -import { Component, ViewChild } from '@angular/core'; +import { Component, ViewChild, EventEmitter, OnInit } from '@angular/core'; import { Title } from '@angular/platform-browser'; import { MatSnackBar } from '@angular/material'; import { TranslateService } from '@ngx-translate/core'; -import { Observable } from 'rxjs'; +import { Observable, BehaviorSubject } from 'rxjs'; import { ItemRepositoryService } from 'app/core/repositories/agenda/item-repository.service'; import { BaseViewComponent } from '../../../base/base-view'; @@ -11,26 +11,64 @@ import { SortingTreeComponent } from 'app/shared/components/sorting-tree/sorting import { ViewItem } from '../../models/view-item'; import { PromptService } from 'app/core/ui-services/prompt.service'; import { CanComponentDeactivate } from 'app/shared/utils/watch-sorting-tree.guard'; +import { itemVisibilityChoices } from 'app/shared/models/agenda/item'; /** * Sort view for the agenda. */ @Component({ selector: 'os-agenda-sort', - templateUrl: './agenda-sort.component.html' + templateUrl: './agenda-sort.component.html', + styleUrls: ['./agenda-sort.component.scss'] }) -export class AgendaSortComponent extends BaseViewComponent implements CanComponentDeactivate { +export class AgendaSortComponent extends BaseViewComponent implements CanComponentDeactivate, OnInit { /** * Reference to the view child */ @ViewChild('osSortedTree') public osSortTree: SortingTreeComponent; + /** + * Emitter to emit if the nodes should expand or collapse. + */ + public readonly changeState: EventEmitter = new EventEmitter(); + + /** + * Emitter who emits the filters to the sorting tree. + */ + public readonly changeFilter: EventEmitter<(item: ViewItem) => boolean> = new EventEmitter< + (item: ViewItem) => boolean + >(); + + /** + * These are the available options for filtering the nodes. + * Adds the property `state` to identify if the option is marked as active. + * When reset the filters, the option `state` will be set to `false`. + */ + public filterOptions = itemVisibilityChoices.map(item => { + return { ...item, state: false }; + }); + + /** + * BehaviourSubject to get informed every time the filters change. + */ + private activeFilters: BehaviorSubject = new BehaviorSubject([]); + /** * Boolean to check if changes has been made. */ public hasChanged = false; + /** + * Boolean to check if filters are active, so they could be removed. + */ + public hasActiveFilter = false; + + /** + * Array, that holds the number of visible nodes and amount of available nodes. + */ + public seenNodes: [number, number] = [0, 0]; + /** * All agendaItems sorted by their virtual weight {@link ViewItem.agendaListWeight} */ @@ -55,6 +93,24 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone this.itemsObservable = this.agendaRepo.getViewModelListObservable(); } + /** + * OnInit method + */ + public ngOnInit(): void { + /** + * Passes the active filters as an array to the subject. + */ + const filter = this.activeFilters.subscribe((value: string[]) => { + this.hasActiveFilter = value.length === 0 ? false : true; + this.changeFilter.emit( + (item: ViewItem): boolean => { + return !(value.includes(item.verboseType) || value.length === 0); + } + ); + }); + this.subscriptions.push(filter); + } + /** * Function to save the tree by click. */ @@ -82,6 +138,54 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone this.hasChanged = hasChanged; } + /** + * Function to receive the new number of visible nodes when the filter has changed. + * + * @param nextNumberOfSeenNodes is an array with two indices: + * The first gives the number of currently shown nodes. + * The second tells how many nodes available. + */ + public onChangeAmountOfItems(nextNumberOfSeenNodes: [number, number]): void { + this.seenNodes = nextNumberOfSeenNodes; + } + + /** + * Function to emit if the nodes should be expanded or collapsed. + * + * @param nextState Is the next state, expanded or collapsed, the nodes should be. + */ + public onStateChange(nextState: boolean): void { + this.changeState.emit(nextState); + } + + /** + * Function to set the active filters to null. + */ + public resetFilters(): void { + for (const option of this.filterOptions) { + option.state = false; + } + this.activeFilters.next([]); + } + + /** + * Function to emit the active filters. + * Filters will be stored in an array to prevent duplicated options. + * Furthermore if the option is already included in this array, then it will be deleted. + * This array will be emitted. + * + * @param filter Is the filter that was activated by the user. + */ + public onFilterChange(filter: string): void { + const value = this.activeFilters.value; + if (!value.includes(filter)) { + value.push(filter); + } else { + value.splice(value.indexOf(filter), 1); + } + this.activeFilters.next(value); + } + /** * Function to open a prompt dialog, * so the user will be warned if he has made changes and not saved them. @@ -96,4 +200,22 @@ export class AgendaSortComponent extends BaseViewComponent implements CanCompone } return true; } + + /** + * Function, that returns an icon depending on the given tag. + * + * @param tag of which the icon will be assigned to. + * + * @returns The icon it should be. + */ + public getIcon(type: string): string { + switch (type.toLowerCase()) { + case 'public item': + return 'public'; + case 'internal item': + return 'visibility'; + case 'hidden item': + return 'visibility_off'; + } + } } diff --git a/client/src/app/site/motions/modules/call-list/call-list.component.html b/client/src/app/site/motions/modules/call-list/call-list.component.html index 62fcc26df..45ca50f8e 100644 --- a/client/src/app/site/motions/modules/call-list/call-list.component.html +++ b/client/src/app/site/motions/modules/call-list/call-list.component.html @@ -24,7 +24,11 @@ parentKey="sort_parent_id" weightKey="weight" (hasChanged)="receiveChanges($event)" - [model]="motionsObservable"> + [model]="motionsObservable"> + + {{ item.getTitle() }} + + diff --git a/client/src/styles.scss b/client/src/styles.scss index 9ce91972f..a5ec3cde4 100644 --- a/client/src/styles.scss +++ b/client/src/styles.scss @@ -12,6 +12,7 @@ @import './assets/styles/global-components-style.scss'; @import './app/shared/components/projector-button/projector-button.component.scss'; @import './app/site/agenda/components/list-of-speakers/list-of-speakers.component.scss-theme.scss'; +@import './app/shared/components/sorting-tree/sorting-tree.component.scss'; /** fonts */ @import './assets/styles/fonts.scss'; @@ -23,6 +24,7 @@ @include os-components-style($theme); @include os-projector-button-style($theme); @include os-list-of-speakers-style($theme); + @include os-sorting-tree-style($theme); /** More components are added here */ }