@igo2/common
Version:
501 lines (495 loc) • 20.2 kB
JavaScript
import * as i0 from '@angular/core';
import { EventEmitter, HostListener, HostBinding, Output, Input, Directive, booleanAttribute, ContentChildren, NgModule } from '@angular/core';
import { SelectionModel } from '@angular/cdk/collections';
import '@angular/cdk/tree';
import { MatTreeNode } from '@angular/material/tree';
class DragAndDropDirective {
allowedExtensions = [];
filesDropped = new EventEmitter();
filesInvalid = new EventEmitter();
background = 'inherit';
onDragOver(evt) {
evt.preventDefault();
evt.stopPropagation();
this.background = '#999';
}
onDragLeave(evt) {
evt.preventDefault();
evt.stopPropagation();
this.background = 'inherit';
}
onDrop(evt) {
evt.preventDefault();
evt.stopPropagation();
if (evt.alreadyFired) {
return;
}
evt.alreadyFired = true;
this.background = 'inherit';
const filesObj = this.validExtensions(evt);
if (filesObj.valid.length) {
this.filesDropped.emit(filesObj.valid);
}
if (filesObj.invalid.length) {
this.filesInvalid.emit(filesObj.invalid);
}
}
validExtensions(evt) {
const files = evt.dataTransfer.files;
const filesObj = {
valid: [],
invalid: []
};
if (files.length > 0) {
for (const file of files) {
const ext = file.name.split('.')[file.name.split('.').length - 1];
if (this.allowedExtensions.length === 0 ||
(this.allowedExtensions.lastIndexOf(ext) !== -1 && file.size !== 0)) {
filesObj.valid.push(file);
}
else {
filesObj.invalid.push(file);
}
}
}
return filesObj;
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DragAndDropDirective, deps: [], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.15", type: DragAndDropDirective, isStandalone: true, selector: "[igoDragAndDrop]", inputs: { allowedExtensions: "allowedExtensions" }, outputs: { filesDropped: "filesDropped", filesInvalid: "filesInvalid" }, host: { listeners: { "dragover": "onDragOver($event)", "dragleave": "onDragLeave($event)", "drop": "onDrop($event)" }, properties: { "style.background": "this.background" } }, ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: DragAndDropDirective, decorators: [{
type: Directive,
args: [{
selector: '[igoDragAndDrop]',
standalone: true
}]
}], propDecorators: { allowedExtensions: [{
type: Input
}], filesDropped: [{
type: Output
}], filesInvalid: [{
type: Output
}], background: [{
type: HostBinding,
args: ['style.background']
}], onDragOver: [{
type: HostListener,
args: ['dragover', ['$event']]
}], onDragLeave: [{
type: HostListener,
args: ['dragleave', ['$event']]
}], onDrop: [{
type: HostListener,
args: ['drop', ['$event']]
}] } });
/**
* This directive should be use with a MatTree flatened
* It should add all logic to drag the MatTreeNode and connect the (onDrop) output
* Class added:
* Tree: --dragging
* Node: --drag-hover | --dragged
*/
class TreeDragDropDirective {
elementRef;
renderer;
draggedNode;
dropNodeTarget;
expandTimeout;
nodesListeners;
dropLineTarget;
highlightedNode = new SelectionModel();
treeControl;
/** The default is 5 */
maxLevel = 5;
set treeDragDropIsDisabled(disabled) {
this.isDisabled = disabled;
disabled ? this.removeAllListener() : this.addAllListener();
}
isDisabled = false;
dragStart = new EventEmitter();
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
onDrop = new EventEmitter(null);
// eslint-disable-next-line @angular-eslint/no-output-on-prefix
onDropError = new EventEmitter();
hostDragOver(event) {
event.preventDefault();
}
hostDrop(event) {
this.drop(event);
}
dragging;
nodes;
constructor(elementRef, renderer) {
this.elementRef = elementRef;
this.renderer = renderer;
this.onDrop.subscribe(() => this.dragEnd());
this.highlightedNode.changed.subscribe((change) => {
if (change.removed.length) {
this.removeNodeClass(change.removed[0].id, '--drag-hover');
}
if (change.added.length) {
this.addNodeClass(change.added[0].id, '--drag-hover');
}
});
}
ngAfterContentInit() {
this.nodes.changes.subscribe(() => {
this.addAllListener();
});
}
ngOnDestroy() {
this.removeAllListener();
}
onDragStart(node) {
this.dragging = true;
this.draggedNode = node;
this.addNodeClass(node.id, '--dragged');
this.dragStart.emit(node);
}
dragEnd() {
this.dragging = false;
if (this.draggedNode) {
this.removeNodeClass(this.draggedNode.id, '--dragged');
this.draggedNode = null;
}
this.dragLeave();
this.removeDropTargetLine();
}
dragOver(node, event) {
event.preventDefault();
event.stopPropagation();
this.dropNodeTarget = node;
const position = this.getPosition(event, node);
this.updateDropTargetLinePosition(position);
this.setHighlightedNode(node);
this.handleGroupExpansion(node, position.type === 'inside');
}
dragLeave() {
clearTimeout(this.expandTimeout);
this.expandTimeout = null;
this.highlightedNode.clear();
}
drop(event) {
if (!this.dropNodeTarget || this.isDisabled) {
return;
}
const dropPosition = this.getPosition(event, this.dropNodeTarget);
const validation = this.canDropNode(this.dropNodeTarget, dropPosition);
if (!validation.canDrop) {
this.onDropError.emit(validation);
return;
}
// Allow to drop a last child outside is group. We refer to it's ancestor for the target
if (dropPosition.type === 'below' &&
dropPosition.level !== this.dropNodeTarget.level) {
this.dropNodeTarget = this.getNodeAncestors(this.dropNodeTarget.id, dropPosition.level);
}
if (this.dropNodeTarget.isGroup) {
if (dropPosition.type === 'inside') {
const isExpanded = this.treeControl.isExpanded(this.dropNodeTarget);
if (isExpanded) {
const children = this.getDirectDescendants(this.dropNodeTarget);
if (children[0]?.id === this.draggedNode.id) {
return;
}
}
this.treeControl.expand(this.dropNodeTarget);
return this.onDrop.emit({
node: this.draggedNode,
ref: this.dropNodeTarget,
position: dropPosition.type
});
}
}
if (dropPosition.type === 'below' &&
dropPosition.level !== this.dropNodeTarget.level) {
const ancestor = this.getNodeAncestors(this.dropNodeTarget.id);
return this.onDrop.emit({
node: this.draggedNode,
ref: ancestor,
position: dropPosition.type
});
}
this.onDrop.emit({
node: this.draggedNode,
ref: this.dropNodeTarget,
position: dropPosition.type
});
}
addNodeClass(id, className) {
const node = this.nodes.find((node) => node.data.id === id);
this.renderer.addClass(node['_elementRef'].nativeElement, className);
}
removeNodeClass(id, className) {
const node = this.nodes.find((node) => node.data.id === id);
this.renderer.removeClass(node['_elementRef'].nativeElement, className);
}
addAllListener() {
if (this.isDisabled) {
return;
}
const nodes = this.nodes?.toArray();
if (this.nodesListeners) {
this.removeAllListener();
}
this.nodesListeners = nodes?.map((node) => this.addListener(node));
}
addListener(node) {
const element = node['_elementRef'].nativeElement;
const listeners = [
['dragstart', () => this.onDragStart(node.data)],
['dragend', () => this.dragEnd()],
['dragover', (event) => this.dragOver(node.data, event)],
['dragleave', () => this.dragLeave()]
];
element.setAttribute('draggable', 'true');
listeners.forEach(([type, listener]) => element.addEventListener(type, listener));
return { element, listeners };
}
removeAllListener() {
this.nodesListeners?.forEach(({ element, listeners }) => {
listeners.forEach(([type, listener]) => {
element.setAttribute('draggable', 'false');
element.removeEventListener(type, listener);
});
});
this.nodesListeners = null;
}
setHighlightedNode(node) {
const toHighlight = node.isGroup ? node : this.getNodeAncestors(node.id);
toHighlight
? this.highlightedNode.select(toHighlight)
: this.highlightedNode.clear();
}
addDropTargetLine() {
const target = this.renderer.createElement('div');
target.classList.add('drop-target-line');
target.setAttribute('style', `position: absolute; top: 0; width: 100%; pointer-events: none; z-index: 4; height: 2px; background-color: black; transition: margin-left 150ms;`);
this.dropLineTarget = target;
this.renderer.appendChild(this.elementRef.nativeElement.parentElement, this.dropLineTarget);
}
removeDropTargetLine() {
if (!this.dropLineTarget) {
return;
}
this.renderer.removeChild(this.elementRef.nativeElement.parentElement, this.dropLineTarget);
this.dropLineTarget = null;
}
updateDropTargetLinePosition(position) {
if (!this.dropLineTarget) {
this.addDropTargetLine();
}
if (position.type === 'inside') {
return this.removeDropTargetLine();
}
this.renderer.setStyle(this.dropLineTarget, 'margin-left', position.level * 24 + 20 + 'px');
this.renderer.setStyle(this.dropLineTarget, 'transform', 'translate3d(0,' + position.y + 'px, 0)');
}
handleGroupExpansion(node, isInside) {
if (!isInside) {
return this.dragLeave();
}
const isOpen = this.treeControl.isExpanded(node);
if (!isOpen && !this.expandTimeout) {
this.expandTimeout = window.setTimeout(() => {
this.treeControl.expand(node);
}, 1200);
}
}
getNodeElement(node) {
const treeNode = this.nodes.find((n) => n.data.id === node.id);
return treeNode?.['_elementRef'].nativeElement;
}
canDropNode(hoveredNode, position) {
if (this.draggedNode.isGroup) {
return this.canDropGroup(hoveredNode, position);
}
const hasMaxLevelRestrictions = this.validateMaxHierarchyLevel(position);
if (hasMaxLevelRestrictions) {
return hasMaxLevelRestrictions;
}
return { canDrop: true };
}
canDropGroup(hoveredNode, position) {
// On ne veut pas permettre le Drop pour un groupe qui s'auto référence
if (hoveredNode.id === this.draggedNode.id) {
return {
canDrop: false,
message: 'igo.common.dragDrop.cannot.dropInsideItself'
};
}
const hasMaxLevelRestrictions = this.validateMaxHierarchyLevel(position);
if (hasMaxLevelRestrictions) {
return hasMaxLevelRestrictions;
}
const isHoverDescendant = this.isHoverDescendant(hoveredNode.id);
return {
canDrop: !isHoverDescendant,
message: isHoverDescendant && 'igo.common.dragDrop.cannot.dropInsideItself'
};
}
validateMaxHierarchyLevel(position) {
if (!this.maxLevel) {
return;
}
let level = position.type === 'inside' ? position.level + 1 : position.level;
if (this.draggedNode.isGroup) {
// We add an extra level +1 to avoid empty group in group
level = level + (this.draggedNode.descendantLevels ?? 0) + 1;
}
if (level > this.maxLevel) {
return {
canDrop: false,
message: 'igo.common.dragDrop.cannot.maxLevel',
params: { value: this.maxLevel }
};
}
}
isHoverDescendant(id) {
return this.treeControl
.getDescendants(this.draggedNode)
.some((child) => child.id === id);
}
getPosition(event, hoveredNode) {
if (this.draggedNode.isGroup) {
if (this.isHoverDescendant(hoveredNode.id)) {
hoveredNode = this.draggedNode;
}
}
let positionType = this.getPositionType(event, hoveredNode);
const bellowOpenedGroup = hoveredNode.isGroup &&
positionType === 'below' &&
this.treeControl.isExpanded(hoveredNode);
if (bellowOpenedGroup) {
positionType = 'inside';
}
return {
x: event.x,
y: this.getPositionY(hoveredNode, positionType),
level: this.getPositionLevel(hoveredNode, positionType, event.x),
type: positionType
};
}
getPositionY(node, type) {
const element = this.getNodeElement(node);
const rect = element.getBoundingClientRect();
return type === 'below'
? element.offsetTop + rect.height - 1
: element.offsetTop + 1;
}
getPositionType(event, hoveredNode) {
const target = this.getNodeElement(hoveredNode);
const rect = target.getBoundingClientRect();
const middle = rect.top + rect.height / 2;
const y = event.y;
const selfReferencing = this.draggedNode.id === hoveredNode.id;
if (hoveredNode.isGroup && !selfReferencing) {
const tolerence = 5;
if (y <= middle + tolerence && y >= middle - tolerence) {
return 'inside';
}
}
const isBelow = y > middle;
return isBelow ? 'below' : 'above';
}
getPositionLevel(hoveredNode, type, x) {
if (hoveredNode.level === 0) {
return 0;
}
const ancestor = this.getNodeAncestors(hoveredNode.id);
const children = this.getDirectDescendants(ancestor);
const lastChild = [...children].pop();
if (type === 'below' && lastChild.id === hoveredNode.id) {
const target = this.getNodeElement(hoveredNode);
const rect = target.getBoundingClientRect();
const indentation = 24;
const xMin = rect.x + indentation;
const xMax = xMin + indentation * hoveredNode.level;
const isInsideSameGroup = x > xMax;
if (!isInsideSameGroup) {
const levelSubstracted = Math.round((xMax - x) / indentation);
return Math.max(0, hoveredNode.level - levelSubstracted);
}
}
return hoveredNode.level;
}
/**
* @param level if level is defined with go to the defined levle to find ancestor. Sinon on prend le current ancestor
*/
getNodeAncestors(id, level) {
const nodes = this.treeControl.dataNodes;
const index = nodes.findIndex((node) => node.id === id);
const targetLevel = level ?? Math.max(0, nodes[index].level - 1);
return nodes
.slice(0, index)
.reverse()
.find((node) => node.level === targetLevel);
}
getDirectDescendants(node) {
const level = node.level + 1;
return this.treeControl
.getDescendants(node)
.filter((child) => child.level === level);
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TreeDragDropDirective, deps: [{ token: i0.ElementRef }, { token: i0.Renderer2 }], target: i0.ɵɵFactoryTarget.Directive });
static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.15", type: TreeDragDropDirective, isStandalone: true, selector: "[igoTreeDragDrop]", inputs: { treeControl: "treeControl", maxLevel: "maxLevel", treeDragDropIsDisabled: ["treeDragDropIsDisabled", "treeDragDropIsDisabled", booleanAttribute] }, outputs: { dragStart: "dragStart", onDrop: "onDrop", onDropError: "onDropError" }, host: { listeners: { "dragover": "hostDragOver($event)", "drop": "hostDrop($event)" }, properties: { "class.--dragging": "this.dragging" } }, queries: [{ propertyName: "nodes", predicate: MatTreeNode, descendants: true }], ngImport: i0 });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: TreeDragDropDirective, decorators: [{
type: Directive,
args: [{
selector: '[igoTreeDragDrop]',
standalone: true
}]
}], ctorParameters: () => [{ type: i0.ElementRef }, { type: i0.Renderer2 }], propDecorators: { treeControl: [{
type: Input,
args: [{ required: true }]
}], maxLevel: [{
type: Input
}], treeDragDropIsDisabled: [{
type: Input,
args: [{ transform: booleanAttribute }]
}], dragStart: [{
type: Output
}], onDrop: [{
type: Output
}], onDropError: [{
type: Output
}], hostDragOver: [{
type: HostListener,
args: ['dragover', ['$event']]
}], hostDrop: [{
type: HostListener,
args: ['drop', ['$event']]
}], dragging: [{
type: HostBinding,
args: ['class.--dragging']
}], nodes: [{
type: ContentChildren,
args: [MatTreeNode, { descendants: true }]
}] } });
/**
* @deprecated import the DragAndDropDirective directly
*/
class IgoDrapDropModule {
static forRoot() {
return {
ngModule: IgoDrapDropModule,
providers: []
};
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: IgoDrapDropModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "19.2.15", ngImport: i0, type: IgoDrapDropModule, imports: [DragAndDropDirective, TreeDragDropDirective], exports: [DragAndDropDirective, TreeDragDropDirective] });
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: IgoDrapDropModule });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.15", ngImport: i0, type: IgoDrapDropModule, decorators: [{
type: NgModule,
args: [{
imports: [DragAndDropDirective, TreeDragDropDirective],
exports: [DragAndDropDirective, TreeDragDropDirective]
}]
}] });
/**
* Generated bundle index. Do not edit.
*/
export { DragAndDropDirective, IgoDrapDropModule, TreeDragDropDirective };
//# sourceMappingURL=igo2-common-drag-drop.mjs.map