/* * This file is part of the TYPO3 CMS project. * * It is free software; you can redistribute it and/or modify it under * the terms of the GNU General Public License, either version 2 * of the License, or any later version. * * For the full copyright and license information, please read the * LICENSE.txt file that was distributed with this source code. * * The TYPO3 project - inspiring people to share! */ import {html, TemplateResult} from 'lit'; import {renderNodes} from 'TYPO3/CMS/Core/lit-helper'; import * as d3drag from 'd3-drag'; import * as d3selection from 'd3-selection'; import {SvgTree, SvgTreeWrapper} from '../SvgTree'; /** * Contains basic types for allowing dragging + dropping in trees */ /** * Generates a template for dragged node */ class DraggableTemplate { public static get(icon: string, name: string): TemplateResult { return html`
${name}
`; } } export enum DraggablePositionEnum { INSIDE = 'inside', BEFORE = 'before', AFTER = 'after' } export interface DragDropHandler { startDrag: boolean; startPageX: number; startPageY: number; dragStart(event: d3drag.D3DragEvent): boolean; dragDragged(event: d3drag.D3DragEvent): boolean; dragEnd(event: d3drag.D3DragEvent): boolean; } /** * Contains the information about drag+drop of one tree instance, contains common * functionality used for drag+drop. */ export class DragDrop { protected tree: SvgTree; private timeout: any = {}; private minimalDistance: number = 10; public static setDragStart(): void { document.querySelectorAll('iframe').forEach((htmlElement: HTMLIFrameElement) => htmlElement.style.pointerEvents = 'none' ); } public static setDragEnd(): void { document.querySelectorAll('iframe').forEach((htmlElement: HTMLIFrameElement) => htmlElement.style.pointerEvents = '' ); } constructor(svgTree: SvgTree) { this.tree = svgTree; } /** * Creates a new drag instance and initializes the clickDistance setting to * prevent clicks from being wrongly detected as drag attempts. */ public connectDragHandler(dragHandler: DragDropHandler) { return d3drag .drag() .clickDistance(5) .on('start', function(evt: d3drag.D3DragEvent) { dragHandler.dragStart(evt) && DragDrop.setDragStart(); }) .on('drag', function(evt: d3drag.D3DragEvent) { dragHandler.dragDragged(evt); }) .on('end', function(evt: d3drag.D3DragEvent) { DragDrop.setDragEnd(); dragHandler.dragEnd(evt); }) } public createDraggable(icon: string, name: string) { let svg = this.tree.svg.node() as SVGElement; const draggable = renderNodes(DraggableTemplate.get(icon, name)); svg.after(...draggable); this.tree.svg.node().querySelector('.nodes-wrapper')?.classList.add('nodes-wrapper--dragging'); } public updateDraggablePosition(evt: d3drag.D3DragEvent): void { let left = 18; let top = 15; if (evt.sourceEvent && evt.sourceEvent.pageX) { left += evt.sourceEvent.pageX; } if (evt.sourceEvent && evt.sourceEvent.pageY) { top += evt.sourceEvent.pageY; } document.querySelectorAll('.node-dd').forEach((draggable: HTMLElement) => { draggable.style.top = top + 'px'; draggable.style.left = left + 'px'; draggable.style.display = 'block'; }); } /** * Open node with children while holding the node/element over this node for 1 second */ public openNodeTimeout(): void { if (this.tree.hoveredNode !== null && this.tree.hoveredNode.hasChildren && !this.tree.hoveredNode.expanded) { if (this.timeout.node != this.tree.hoveredNode) { this.timeout.node = this.tree.hoveredNode; clearTimeout(this.timeout.time); this.timeout.time = setTimeout(() => { if (this.tree.hoveredNode) { this.tree.showChildren(this.tree.hoveredNode); this.tree.prepareDataForVisibleNodes(); this.tree.updateVisibleNodes(); } }, 1000); } } else { clearTimeout(this.timeout.time); } } public changeNodeClasses(event: any): void { const elementNodeBg = this.tree.svg.select('.node-over'); const svg = this.tree.svg.node() as SVGElement; const nodeDd = svg.parentNode.querySelector('.node-dd') as HTMLElement; type NodeBgBorderSelection = d3selection.Selection | d3selection.Selection; let nodeBgBorder: NodeBgBorderSelection = this.tree.nodesBgContainer.selectAll('.node-bg__border'); if (elementNodeBg.size() && this.tree.isOverSvg) { // line between nodes if (nodeBgBorder.empty()) { nodeBgBorder = this.tree.nodesBgContainer .append('rect') .attr('class', 'node-bg__border') .attr('height', '1px') .attr('width', '100%'); } const coordinates = d3selection.pointer(event, elementNodeBg.node()); let y = coordinates[1]; if (y < 3) { const attr = nodeBgBorder.attr('transform', 'translate(-8, ' + (this.tree.hoveredNode.y - 10) + ')') as NodeBgBorderSelection; attr.style('display', 'block'); if (this.tree.hoveredNode.depth === 0) { this.addNodeDdClass(nodeDd, 'nodrop'); } else if (this.tree.hoveredNode.firstChild) { this.addNodeDdClass(nodeDd, 'ok-above'); } else { this.addNodeDdClass(nodeDd, 'ok-between'); } this.tree.settings.nodeDragPosition = DraggablePositionEnum.BEFORE; } else if (y > 17) { nodeBgBorder.style('display', 'none'); if (this.tree.hoveredNode.expanded && this.tree.hoveredNode.hasChildren) { this.addNodeDdClass(nodeDd, 'ok-append'); this.tree.settings.nodeDragPosition = DraggablePositionEnum.INSIDE; } else { const attr = nodeBgBorder.attr('transform', 'translate(-8, ' + (this.tree.hoveredNode.y + 10) + ')') as NodeBgBorderSelection; attr.style('display', 'block'); if (this.tree.hoveredNode.lastChild) { this.addNodeDdClass(nodeDd, 'ok-below'); } else { this.addNodeDdClass(nodeDd, 'ok-between'); } this.tree.settings.nodeDragPosition = DraggablePositionEnum.AFTER; } } else { nodeBgBorder.style('display', 'none'); this.addNodeDdClass(nodeDd, 'ok-append'); this.tree.settings.nodeDragPosition = DraggablePositionEnum.INSIDE; } } else { this.tree.nodesBgContainer .selectAll('.node-bg__border') .style('display', 'none'); this.addNodeDdClass(nodeDd, 'nodrop'); } } public addNodeDdClass(nodeDd: HTMLElement|null, className: string): void { const nodesWrap = this.tree.svg.node().querySelector('.nodes-wrapper') as SVGElement; if (nodeDd) { this.applyNodeClassNames(nodeDd, 'node-dd--', className); } if (nodesWrap) { this.applyNodeClassNames(nodesWrap, 'nodes-wrapper--', className); } this.tree.settings.canNodeDrag = className !== 'nodrop'; } // Clean up after a finished drag+drop move public removeNodeDdClass(): void { const nodesWrap = this.tree.svg.node().querySelector('.nodes-wrapper'); // remove any classes from wrapper [ 'nodes-wrapper--nodrop', 'nodes-wrapper--ok-append', 'nodes-wrapper--ok-below', 'nodes-wrapper--ok-between', 'nodes-wrapper--ok-above', 'nodes-wrapper--dragging' ].forEach((className: string) => nodesWrap.classList.remove(className) ); this.tree.nodesBgContainer.node().querySelector('.node-bg.node-bg--dragging')?.classList.remove('node-bg--dragging'); this.tree.nodesBgContainer.selectAll('.node-bg__border').style('display', 'none'); this.tree.svg.node().parentNode.querySelector('.node-dd').remove(); } /** * Check if node is dragged at least @distance */ public isDragNodeDistanceMore(event: d3drag.D3DragEvent, dragHandler: DragDropHandler): boolean { return (dragHandler.startDrag || (((dragHandler.startPageX - this.minimalDistance) > event.sourceEvent.pageX) || ((dragHandler.startPageX + this.minimalDistance) < event.sourceEvent.pageX) || ((dragHandler.startPageY - this.minimalDistance) > event.sourceEvent.pageY) || ((dragHandler.startPageY + this.minimalDistance) < event.sourceEvent.pageY))); } private applyNodeClassNames(target: HTMLElement|SVGElement, prefix: string, className: string): void { const classNames = ['nodrop', 'ok-append', 'ok-below', 'ok-between', 'ok-above', 'dragging']; // remove any existing classes classNames.forEach((className: string) => target.classList.remove(prefix + className)); // apply new class target.classList.add(prefix + className); } }