Implements filtering the sorting-tree.component
- Added filtering by visibility, like internal, public or hidden items - Added option to collapse or expand all nodes - Added also style for the dark theme
This commit is contained in:
parent
ebbb369124
commit
cdc849de6d
@ -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 = [];
|
||||
|
@ -36,7 +36,9 @@
|
||||
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'"
|
||||
|
@ -1,12 +1,18 @@
|
||||
@import '../../../../assets/styles/drag.scss';
|
||||
@import '~@angular/material/theming';
|
||||
|
||||
@mixin os-sorting-tree-style($theme) {
|
||||
$background: map-get($theme, background);
|
||||
|
||||
cdk-tree-node {
|
||||
margin-bottom: 5px;
|
||||
margin: 3px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
cursor: pointer;
|
||||
|
||||
div {
|
||||
display: inherit;
|
||||
align-items: inherit;
|
||||
width: 100%;
|
||||
|
||||
mat-icon {
|
||||
@ -17,21 +23,26 @@ cdk-tree-node {
|
||||
|
||||
// Overwrite the preview
|
||||
.cdk-drag-preview {
|
||||
box-shadow: none;
|
||||
background-color: unset;
|
||||
box-shadow: none !important;
|
||||
background-color: unset !important;
|
||||
border-radius: 6px !important;
|
||||
|
||||
div {
|
||||
box-sizing: border-box;
|
||||
background-color: white;
|
||||
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;
|
||||
opacity: 1 !important;
|
||||
background: #ccc;
|
||||
border: dotted 3px #999;
|
||||
min-height: 42px;
|
||||
@ -41,3 +52,4 @@ cdk-tree-node {
|
||||
.cdk-drop-list.cdk-drop-list-dragging .cdk-tree-node:not(.cdk-drag-placeholder) {
|
||||
transition: transform 250ms cubic-bezier(0, 0, 0.2, 1);
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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>: </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>
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
@ -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';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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">
|
||||
|
@ -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 */
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user