Merge pull request #4575 from GabrielInTheWorld/tree-sort-agenda

Implements filtering the `sorting-tree.component`
This commit is contained in:
Sean 2019-04-17 13:14:57 +02:00 committed by GitHub
commit bf525cf852
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 565 additions and 122 deletions

View File

@ -31,21 +31,23 @@ export interface OSTreeNode<T> 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<T> {
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<T>[] {
const tree = this.makeTree(items, weightKey, parentKey);
const flatNodes: FlatNode[] = [];
const flatNodes: FlatNode<T>[] = [];
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<T extends Identifiable & Displayable>(nodes: FlatNode<T>[]): 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<T extends Identifiable & Displayable>(
item: OSTreeNode<T>,
level: number,
parents: FlatNode[]
): FlatNode[] {
level: number
): FlatNode<T>[] {
const children = item.children;
const node: FlatNode = {
const node: FlatNode<T> = {
id: item.id,
name: item.name,
item: item.item,
expandable: !!children,
isExpanded: !!children,
level: level,
isSeen: true
};
const flatNodes: FlatNode[] = [node];
const flatNodes: FlatNode<T>[] = [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<T extends Identifiable & Displayable>(
node: FlatNode<T>,
nodes: FlatNode<T>[],
length: number
): { node: TreeIdNode; length: number } {
const children = [];

View File

@ -36,10 +36,12 @@
chevron_right
</mat-icon>
</button>
{{ node.name }}
<ng-container
[ngTemplateOutlet]="innerNode"
[ngTemplateOutletContext]="{item: node.item}"></ng-container>
</div>
<div
[style.margin-left]="placeholderLevel * 40 + 'px'"
*cdkDragPlaceholder></div>
[style.margin-left]="placeholderLevel * 40 + 'px'"
*cdkDragPlaceholder></div>
</cdk-tree-node>
</cdk-tree>

View File

@ -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);
}

View File

@ -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<T extends Identifiable & Displayable> extends FlatNode<T> {
startPosition: number;
nextPosition: number;
}
@ -56,12 +56,12 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
/**
* The data to build the tree
*/
public osTreeData: FlatNode[] = [];
public osTreeData: FlatNode<T>[] = [];
/**
* The tree control
*/
public treeControl = new FlatTreeControl<FlatNode>(node => node.level, node => node.expandable);
public treeControl = new FlatTreeControl<FlatNode<T>>(node => node.level, node => node.expandable);
/**
* Source for the tree
@ -83,13 +83,23 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<T> = 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<T extends Identifiable & Displayable> 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<T[]>) {
@ -126,12 +138,53 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<Boolean>) {
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<boolean> = new EventEmitter<boolean>();
/**
* 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<any>;
/**
* Constructor
*
@ -158,7 +211,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @returns The parent node if available otherwise it returns null.
*/
public getParentNode(node: FlatNode): FlatNode {
public getParentNode(node: FlatNode<T>): FlatNode<T> {
const nodeIndex = this.osTreeData.indexOf(node);
for (let i = nodeIndex - 1; i >= 0; --i) {
@ -178,11 +231,11 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<T>): FlatNode<T> {
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<T extends Identifiable & Displayable> implemen
*
* @returns An array containing its parent and the parents of its parent.
*/
private getAllParents(node: FlatNode): FlatNode[] {
private getAllParents(node: FlatNode<T>): FlatNode<T>[] {
return this._getAllParents(node, []);
}
@ -207,7 +260,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<T>, array: FlatNode<T>[]): FlatNode<T>[] {
const parent = this.getParentNode(node);
if (parent) {
array.push(parent);
@ -224,9 +277,9 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @returns An array that contains all the nearest children.
*/
public getChildNodes(node: FlatNode): FlatNode[] {
public getChildNodes(node: FlatNode<T>): FlatNode<T>[] {
const nodeIndex = this.osTreeData.indexOf(node);
const childNodes: FlatNode[] = [];
const childNodes: FlatNode<T>[] = [];
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<T extends Identifiable & Displayable> 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<T>): FlatNode<T>[] {
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<T extends Identifiable & Displayable> implemen
*
* @returns An array containing all the subnodes, inclusive the children of the children.
*/
private getAllSubNodes(node: FlatNode): FlatNode[] {
private getAllSubNodes(node: FlatNode<T>): FlatNode<T>[] {
return this._getAllSubNodes(node, []);
}
@ -260,7 +324,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @returns An array containing all subnodes, inclusive the children of the children.
*/
private _getAllSubNodes(node: FlatNode, array: FlatNode[]): FlatNode[] {
private _getAllSubNodes(node: FlatNode<T>, array: FlatNode<T>[]): FlatNode<T>[] {
array.push(node);
for (const child of this.getChildNodes(node)) {
this._getAllSubNodes(child, array);
@ -276,7 +340,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @returns The calculated position as number.
*/
private getPositionOnScreen(node: FlatNode): number {
private getPositionOnScreen(node: FlatNode<T>): number {
let currentPosition = this.osTreeData.length;
for (let i = this.osTreeData.length - 1; i >= 0; --i) {
--currentPosition;
@ -297,7 +361,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<T>): boolean {
return node.isSeen;
}
@ -306,7 +370,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @param node which is clicked.
*/
public handleClick(node: FlatNode): void {
public handleClick(node: FlatNode<T>): void {
node.isExpanded = !node.isExpanded;
if (node.isExpanded) {
for (const child of this.getChildNodes(node)) {
@ -325,8 +389,8 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @param node is the node which should be shown again.
*/
private showChildren(node: FlatNode): void {
node.isSeen = true;
private showChildren(node: FlatNode<T>): 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<T extends Identifiable & Displayable> 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<T>): 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<T>, direction: Direction.DOWNWARDS | Direction.UPWARDS): FlatNode<T> {
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<T>): FlatNode<T> {
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<T extends Identifiable & Displayable> implemen
*/
public startsDrag(event: CdkDragStart): void {
this.removeSubscription();
const draggedNode = <FlatNode>event.source.data;
const draggedNode = <FlatNode<T>>event.source.data;
this.placeholderLevel = draggedNode.level;
this.nextNode = {
...draggedNode,
@ -405,12 +518,11 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
*
* @param node Is the dropped node.
*/
public onDrop(node: FlatNode): void {
const moving = this.getDirection();
public onDrop(node: FlatNode<T>): 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<T extends Identifiable & Displayable> 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<T extends Identifiable & Displayable> implemen
*/
private findNextIndex(
steps: number,
node: ExFlatNode,
node: ExFlatNode<T>,
verticalMove: Direction.DOWNWARDS | Direction.UPWARDS | Direction.NOWAY
): number {
let currentPosition = this.osTreeData.length;
@ -519,7 +637,7 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> implemen
} else {
break;
}
if (node.name === parent.name) {
if (node.item.getTitle() === parent.item.getTitle()) {
--i;
}
}
@ -553,20 +671,23 @@ export class SortingTreeComponent<T extends Identifiable & Displayable> 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<T>, 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<T extends Identifiable & Displayable> 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<T extends Identifiable & Displayable> 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<T extends Identifiable & Displayable> 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<T extends Identifiable & Displayable> 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<T extends Identifiable & Displayable> 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<T>, parent?: FlatNode<T>): 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<T>) => node.expandable;
}

View File

@ -7,13 +7,59 @@
<!-- Title -->
<div class="title-slot"><h2 translate>Sort agenda</h2></div>
</os-head-bar>
<div class="custom-table-header sort-header">
<div class="button-menu left">
<button mat-button (click)="onStateChange(true)">{{ 'Expand all' | translate }}</button>
<button mat-button (click)="onStateChange(false)">{{ 'Collapse all' | translate }}</button>
</div>
<div class="current-filters" *ngIf="hasActiveFilter">
<div><span translate>Active filters</span>:&nbsp;</div>
<div>
<button mat-button (click)="resetFilters()">
<mat-icon inline>cancel</mat-icon>
<span>{{ 'Visibility' | translate }}</span>
</button>
</div>
</div>
<div class="button-menu right">
<button mat-button (click)="visibilityFilter.opened ? visibilityFilter.close() : visibilityFilter.open()">Filter</button>
<mat-drawer #visibilityFilter mode="over" position="end">
<section class="sort-drawer-content">
<button mat-button (click)="visibilityFilter.toggle()">
<mat-icon>keyboard_arrow_right</mat-icon>
</button>
<span class="sort-grid">
<div class="hint">{{ 'Visibility' | translate }}</div>
<div>
<mat-checkbox *ngFor="let option of filterOptions" [(ngModel)]="option.state" (change)="onFilterChange(option.name)">
<mat-icon matTooltip="{{ option.name | translate }}">{{ getIcon(option.name) }}</mat-icon> {{ option.name | translate }}
</mat-checkbox>
</div>
</span>
</section>
</mat-drawer>
</div>
</div>
<mat-card>
<div class="current-nodes">
<span translate>You are currently seeing {{ seenNodes[0] }} of {{ seenNodes[1] }} items.</span>
<mat-divider></mat-divider>
</div>
<os-sorting-tree
#osSortedTree
(hasChanged)="receiveChanges($event)"
(visibleNodes)="onChangeAmountOfItems($event)"
[model]="itemsObservable"
[stateChange]="changeState"
[filterChange]="changeFilter"
parentKey="parent_id"
weightKey="weight"
></os-sorting-tree>
>
<ng-template #innerNode class="sorting-tree-node" let-item="item">
<span class="sort-node-title">{{ item.getTitle() }}</span>
<span class="sort-node-icon">
{{ item.verboseType | translate }} <mat-icon>{{ getIcon(item.verboseType) }}</mat-icon>
</span>
</ng-template>
</os-sorting-tree>
</mat-card>

View File

@ -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;
}
}

View File

@ -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<ViewItem>;
/**
* Emitter to emit if the nodes should expand or collapse.
*/
public readonly changeState: EventEmitter<Boolean> = new EventEmitter<Boolean>();
/**
* 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<string[]> = new BehaviorSubject<string[]>([]);
/**
* 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';
}
}
}

View File

@ -24,7 +24,11 @@
parentKey="sort_parent_id"
weightKey="weight"
(hasChanged)="receiveChanges($event)"
[model]="motionsObservable"></os-sorting-tree>
[model]="motionsObservable">
<ng-template #innerNode let-item="item">
<span>{{ item.getTitle() }}</span>
</ng-template>
</os-sorting-tree>
</mat-card>
<mat-menu #downloadMenu="matMenu">

View File

@ -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 */
}