UNPKG

@igo2/common

Version:
501 lines (495 loc) 20.2 kB
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