import React, { useCallback, useRef, useState } from 'react'; import TreeView, { type TreeViewRef } from 'devextreme-react/tree-view'; import Sortable, { type SortableTypes } from 'devextreme-react/sortable'; import service from './data.ts'; const getStateFieldName = (driveName: string) => (driveName === 'driveC' ? 'itemsDriveC' : 'itemsDriveD'); const calculateToIndex = (e: SortableTypes.DragChangeEvent) => { if (e.fromComponent !== e.toComponent || e.dropInsideItem) { return e.toIndex; } return e.fromIndex >= e.toIndex ? e.toIndex : e.toIndex + 1; }; const findNode = (treeView, index: string | number) => { const nodeElement = treeView.element().querySelectorAll('.dx-treeview-node')[index]; if (nodeElement) { return findNodeById(treeView.getNodes(), nodeElement.getAttribute('data-item-id')); } return null; }; const findNodeById = (nodes, id) => { for (let i = 0; i < nodes.length; i += 1) { if (nodes[i].itemData.id === id) { return nodes[i]; } if (nodes[i].children) { const node = findNodeById(nodes[i].children, id); if (node != null) { return node; } } } return null; }; const moveNode = (fromNode, toNode, fromItems, toItems, isDropInsideItem) => { const fromIndex = fromItems.findIndex((item) => item.id === fromNode.itemData.id); fromItems.splice(fromIndex, 1); const toIndex = toNode === null || isDropInsideItem ? toItems.length : toItems.findIndex((item) => item.id === toNode.itemData.id); toItems.splice(toIndex, 0, fromNode.itemData); moveChildren(fromNode, fromItems, toItems); if (isDropInsideItem) { fromNode.itemData.parentId = toNode.itemData.id; } else { fromNode.itemData.parentId = toNode != null ? toNode.itemData.parentId : undefined; } }; const moveChildren = (node, fromDataSource, toDataSource: any[]) => { if (!node.itemData.isDirectory) { return; } node.children.forEach((child) => { if (child.itemData.isDirectory) { moveChildren(child, fromDataSource, toDataSource); } const fromIndex = fromDataSource.findIndex((item) => item.id === child.itemData.id); fromDataSource.splice(fromIndex, 1); toDataSource.splice(toDataSource.length, 0, child.itemData); }); }; const isChildNode = (parentNode: { itemData: { id: any; }; }, childNode: { parent: any; }) => { let { parent } = childNode; while (parent !== null) { if (parent.itemData.id === parentNode.itemData.id) { return true; } parent = parent.parent; } return false; }; const getTopVisibleNode = (component) => { const treeViewElement = component.element(); const treeViewTopPosition = treeViewElement.getBoundingClientRect().top; const nodes = treeViewElement.querySelectorAll('.dx-treeview-node'); for (let i = 0; i < nodes.length; i += 1) { const nodeTopPosition = nodes[i].getBoundingClientRect().top; if (nodeTopPosition >= treeViewTopPosition) { return nodes[i]; } } return null; }; const App = () => { const treeViewDriveCRef = useRef<TreeViewRef>(null); const treeViewDriveDRef = useRef<TreeViewRef>(null); const [itemsDriveC, setItemsDriveC] = useState(service.getItemsDriveC()); const [itemsDriveD, setItemsDriveD] = useState(service.getItemsDriveD()); const getTreeView = useCallback((driveName: string) => (driveName === 'driveC' ? treeViewDriveCRef.current.instance() : treeViewDriveDRef.current.instance()), []); const onDragChange = useCallback((e: SortableTypes.DragChangeEvent) => { if (e.fromComponent === e.toComponent) { const fromNode = findNode(getTreeView(e.fromData), e.fromIndex); const toNode = findNode(getTreeView(e.toData), calculateToIndex(e)); if (toNode !== null && isChildNode(fromNode, toNode)) { e.cancel = true; } } }, [getTreeView]); const onDragEnd = useCallback((e: SortableTypes.DragEndEvent) => { if (e.fromComponent === e.toComponent && e.fromIndex === e.toIndex) { return; } const fromTreeView = getTreeView(e.fromData); const toTreeView = getTreeView(e.toData); const fromNode = findNode(fromTreeView, e.fromIndex); const toNode = findNode(toTreeView, calculateToIndex(e)); if (e.dropInsideItem && toNode !== null && !toNode.itemData.isDirectory) { return; } const fromTopVisibleNode = getTopVisibleNode(e.fromComponent); const toTopVisibleNode = getTopVisibleNode(e.toComponent); const fromItems = getStateFieldName(e.fromData) === 'itemsDriveC' ? itemsDriveC : itemsDriveD; const toItems = getStateFieldName(e.toData) === 'itemsDriveC' ? itemsDriveC : itemsDriveD; moveNode(fromNode, toNode, fromItems, toItems, e.dropInsideItem); if (getStateFieldName(e.fromData) === 'itemsDriveC') { setItemsDriveC([...fromItems]); } else { setItemsDriveD([...fromItems]); } if (getStateFieldName(e.toData) === 'itemsDriveC') { setItemsDriveC([...toItems]); } else { setItemsDriveD([...toItems]); } fromTreeView.scrollToItem(fromTopVisibleNode); toTreeView.scrollToItem(toTopVisibleNode); }, [getTreeView, itemsDriveC, itemsDriveD, setItemsDriveC, setItemsDriveD]); return ( <div className="form"> <div className="drive-panel"> <div className="drive-header dx-treeview-item"><div className="dx-treeview-item-content"><i className="dx-icon dx-icon-activefolder"></i><span>Drive C:</span></div></div> <Sortable filter=".dx-treeview-item" group="shared" data="driveC" allowDropInsideItem={true} allowReordering={true} onDragChange={onDragChange} onDragEnd={onDragEnd} > <TreeView id="treeviewDriveC" expandNodesRecursive={false} dataStructure="plain" ref={treeViewDriveCRef} items={itemsDriveC} width={250} height={380} displayExpr="name" /> </Sortable> </div> <div className="drive-panel"> <div className="drive-header dx-treeview-item"><div className="dx-treeview-item-content"><i className="dx-icon dx-icon-activefolder"></i><span>Drive D:</span></div></div> <Sortable filter=".dx-treeview-item" group="shared" data="driveD" allowDropInsideItem={true} allowReordering={true} onDragChange={onDragChange} onDragEnd={onDragEnd} > <TreeView id="treeviewDriveD" expandNodesRecursive={false} dataStructure="plain" ref={treeViewDriveDRef} items={itemsDriveD} width={250} height={380} displayExpr="name" /> </Sortable> </div> </div> ); }; export default App;
import React, { useCallback, useRef, useState } from 'react'; import TreeView from 'devextreme-react/tree-view'; import Sortable from 'devextreme-react/sortable'; import service from './data.js'; const getStateFieldName = (driveName) => (driveName === 'driveC' ? 'itemsDriveC' : 'itemsDriveD'); const calculateToIndex = (e) => { if (e.fromComponent !== e.toComponent || e.dropInsideItem) { return e.toIndex; } return e.fromIndex >= e.toIndex ? e.toIndex : e.toIndex + 1; }; const findNode = (treeView, index) => { const nodeElement = treeView.element().querySelectorAll('.dx-treeview-node')[index]; if (nodeElement) { return findNodeById(treeView.getNodes(), nodeElement.getAttribute('data-item-id')); } return null; }; const findNodeById = (nodes, id) => { for (let i = 0; i < nodes.length; i += 1) { if (nodes[i].itemData.id === id) { return nodes[i]; } if (nodes[i].children) { const node = findNodeById(nodes[i].children, id); if (node != null) { return node; } } } return null; }; const moveNode = (fromNode, toNode, fromItems, toItems, isDropInsideItem) => { const fromIndex = fromItems.findIndex((item) => item.id === fromNode.itemData.id); fromItems.splice(fromIndex, 1); const toIndex = toNode === null || isDropInsideItem ? toItems.length : toItems.findIndex((item) => item.id === toNode.itemData.id); toItems.splice(toIndex, 0, fromNode.itemData); moveChildren(fromNode, fromItems, toItems); if (isDropInsideItem) { fromNode.itemData.parentId = toNode.itemData.id; } else { fromNode.itemData.parentId = toNode != null ? toNode.itemData.parentId : undefined; } }; const moveChildren = (node, fromDataSource, toDataSource) => { if (!node.itemData.isDirectory) { return; } node.children.forEach((child) => { if (child.itemData.isDirectory) { moveChildren(child, fromDataSource, toDataSource); } const fromIndex = fromDataSource.findIndex((item) => item.id === child.itemData.id); fromDataSource.splice(fromIndex, 1); toDataSource.splice(toDataSource.length, 0, child.itemData); }); }; const isChildNode = (parentNode, childNode) => { let { parent } = childNode; while (parent !== null) { if (parent.itemData.id === parentNode.itemData.id) { return true; } parent = parent.parent; } return false; }; const getTopVisibleNode = (component) => { const treeViewElement = component.element(); const treeViewTopPosition = treeViewElement.getBoundingClientRect().top; const nodes = treeViewElement.querySelectorAll('.dx-treeview-node'); for (let i = 0; i < nodes.length; i += 1) { const nodeTopPosition = nodes[i].getBoundingClientRect().top; if (nodeTopPosition >= treeViewTopPosition) { return nodes[i]; } } return null; }; const App = () => { const treeViewDriveCRef = useRef(null); const treeViewDriveDRef = useRef(null); const [itemsDriveC, setItemsDriveC] = useState(service.getItemsDriveC()); const [itemsDriveD, setItemsDriveD] = useState(service.getItemsDriveD()); const getTreeView = useCallback( (driveName) => (driveName === 'driveC' ? treeViewDriveCRef.current.instance() : treeViewDriveDRef.current.instance()), [], ); const onDragChange = useCallback( (e) => { if (e.fromComponent === e.toComponent) { const fromNode = findNode(getTreeView(e.fromData), e.fromIndex); const toNode = findNode(getTreeView(e.toData), calculateToIndex(e)); if (toNode !== null && isChildNode(fromNode, toNode)) { e.cancel = true; } } }, [getTreeView], ); const onDragEnd = useCallback( (e) => { if (e.fromComponent === e.toComponent && e.fromIndex === e.toIndex) { return; } const fromTreeView = getTreeView(e.fromData); const toTreeView = getTreeView(e.toData); const fromNode = findNode(fromTreeView, e.fromIndex); const toNode = findNode(toTreeView, calculateToIndex(e)); if (e.dropInsideItem && toNode !== null && !toNode.itemData.isDirectory) { return; } const fromTopVisibleNode = getTopVisibleNode(e.fromComponent); const toTopVisibleNode = getTopVisibleNode(e.toComponent); const fromItems = getStateFieldName(e.fromData) === 'itemsDriveC' ? itemsDriveC : itemsDriveD; const toItems = getStateFieldName(e.toData) === 'itemsDriveC' ? itemsDriveC : itemsDriveD; moveNode(fromNode, toNode, fromItems, toItems, e.dropInsideItem); if (getStateFieldName(e.fromData) === 'itemsDriveC') { setItemsDriveC([...fromItems]); } else { setItemsDriveD([...fromItems]); } if (getStateFieldName(e.toData) === 'itemsDriveC') { setItemsDriveC([...toItems]); } else { setItemsDriveD([...toItems]); } fromTreeView.scrollToItem(fromTopVisibleNode); toTreeView.scrollToItem(toTopVisibleNode); }, [getTreeView, itemsDriveC, itemsDriveD, setItemsDriveC, setItemsDriveD], ); return ( <div className="form"> <div className="drive-panel"> <div className="drive-header dx-treeview-item"> <div className="dx-treeview-item-content"> <i className="dx-icon dx-icon-activefolder"></i> <span>Drive C:</span> </div> </div> <Sortable filter=".dx-treeview-item" group="shared" data="driveC" allowDropInsideItem={true} allowReordering={true} onDragChange={onDragChange} onDragEnd={onDragEnd} > <TreeView id="treeviewDriveC" expandNodesRecursive={false} dataStructure="plain" ref={treeViewDriveCRef} items={itemsDriveC} width={250} height={380} displayExpr="name" /> </Sortable> </div> <div className="drive-panel"> <div className="drive-header dx-treeview-item"> <div className="dx-treeview-item-content"> <i className="dx-icon dx-icon-activefolder"></i> <span>Drive D:</span> </div> </div> <Sortable filter=".dx-treeview-item" group="shared" data="driveD" allowDropInsideItem={true} allowReordering={true} onDragChange={onDragChange} onDragEnd={onDragEnd} > <TreeView id="treeviewDriveD" expandNodesRecursive={false} dataStructure="plain" ref={treeViewDriveDRef} items={itemsDriveD} width={250} height={380} displayExpr="name" /> </Sortable> </div> </div> ); }; export default App;
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.tsx'; ReactDOM.render( <App />, document.getElementById('app'), );
const itemsDriveD = []; const itemsDriveC = [{ id: '1', name: 'Documents', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '2', parentId: '1', name: 'Projects', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '3', parentId: '2', name: 'About.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '4', parentId: '2', name: 'Passwords.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '5', parentId: '2', name: 'About.xml', icon: 'file', isDirectory: false, expanded: true, }, { id: '6', parentId: '2', name: 'Managers.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '7', parentId: '2', name: 'ToDo.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '8', name: 'Images', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '9', parentId: '8', name: 'logo.png', icon: 'file', isDirectory: false, expanded: true, }, { id: '10', parentId: '8', name: 'banner.gif', icon: 'file', isDirectory: false, expanded: true, }, { id: '11', name: 'System', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '12', parentId: '11', name: 'Employees.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '13', parentId: '11', name: 'PasswordList.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '14', name: 'Description.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '15', name: 'Description.txt', icon: 'file', isDirectory: false, expanded: true, }]; export default { getItemsDriveC() { return itemsDriveC; }, getItemsDriveD() { return itemsDriveD; }, };
window.exports = window.exports || {}; window.config = { transpiler: 'ts', typescriptOptions: { module: 'system', emitDecoratorMetadata: true, experimentalDecorators: true, jsx: 'react', }, meta: { 'react': { 'esModule': true, }, 'typescript': { 'exports': 'ts', }, 'devextreme/time_zone_utils.js': { 'esModule': true, }, 'devextreme/localization.js': { 'esModule': true, }, 'devextreme/viz/palette.js': { 'esModule': true, }, 'openai': { 'esModule': true, }, }, paths: { 'npm:': 'https://cdn.jsdelivr.net/npm/', 'bundles:': '../../../../bundles/', 'externals:': '../../../../bundles/externals/', }, defaultExtension: 'js', map: { 'ts': 'npm:plugin-typescript@8.0.0/lib/plugin.js', 'typescript': 'npm:typescript@4.2.4/lib/typescript.js', 'jszip': 'npm:jszip@3.10.1/dist/jszip.min.js', 'react': 'npm:react@17.0.2/umd/react.development.js', 'react-dom': 'npm:react-dom@17.0.2/umd/react-dom.development.js', 'prop-types': 'npm:prop-types/prop-types.js', 'rrule': 'npm:rrule@2.6.4/dist/es5/rrule.js', 'luxon': 'npm:luxon@3.4.4/build/global/luxon.min.js', 'es6-object-assign': 'npm:es6-object-assign', 'devextreme': 'npm:devextreme@link:../../packages/devextreme/artifacts/npm/devextreme/cjs', 'devextreme-react': 'npm:devextreme-react@link:../../packages/devextreme-react/npm/cjs', 'devextreme-quill': 'npm:devextreme-quill@1.7.6/dist/dx-quill.min.js', 'devexpress-diagram': 'npm:devexpress-diagram@2.2.24/dist/dx-diagram.js', 'devexpress-gantt': 'npm:devexpress-gantt@4.1.64/dist/dx-gantt.js', 'inferno': 'npm:inferno@8.2.3/dist/inferno.min.js', 'inferno-compat': 'npm:inferno-compat/dist/inferno-compat.min.js', 'inferno-create-element': 'npm:inferno-create-element@8.2.3/dist/inferno-create-element.min.js', 'inferno-dom': 'npm:inferno-dom/dist/inferno-dom.min.js', 'inferno-hydrate': 'npm:inferno-hydrate/dist/inferno-hydrate.min.js', 'inferno-clone-vnode': 'npm:inferno-clone-vnode/dist/inferno-clone-vnode.min.js', 'inferno-create-class': 'npm:inferno-create-class/dist/inferno-create-class.min.js', 'inferno-extras': 'npm:inferno-extras/dist/inferno-extras.min.js', '@preact/signals-core': 'npm:@preact/signals-core@1.8.0/dist/signals-core.min.js', 'devextreme-cldr-data': 'npm:devextreme-cldr-data@1.0.3', // SystemJS plugins 'plugin-babel': 'npm:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'npm:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', // Prettier 'prettier/standalone': 'npm:prettier@2.8.8/standalone.js', 'prettier/parser-html': 'npm:prettier@2.8.8/parser-html.js', }, packages: { 'devextreme': { defaultExtension: 'js', }, 'devextreme-react': { main: 'index.js', }, 'devextreme-react/common': { main: 'index.js', }, 'devextreme/events/utils': { main: 'index', }, 'devextreme/common/core/events/utils': { main: 'index', }, 'devextreme/localization/messages': { format: 'json', defaultExtension: 'json', }, 'devextreme/events': { main: 'index', }, 'es6-object-assign': { main: './index.js', defaultExtension: 'js', }, }, packageConfigPaths: [ 'npm:@devextreme/*/package.json', ], babelOptions: { sourceMaps: false, stage0: true, react: true, }, }; System.config(window.config); // eslint-disable-next-line const useTgzInCSB = ['openai'];
import React from 'react'; import ReactDOM from 'react-dom'; import App from './App.js'; ReactDOM.render(<App />, document.getElementById('app'));
const itemsDriveD = []; const itemsDriveC = [ { id: '1', name: 'Documents', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '2', parentId: '1', name: 'Projects', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '3', parentId: '2', name: 'About.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '4', parentId: '2', name: 'Passwords.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '5', parentId: '2', name: 'About.xml', icon: 'file', isDirectory: false, expanded: true, }, { id: '6', parentId: '2', name: 'Managers.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '7', parentId: '2', name: 'ToDo.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '8', name: 'Images', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '9', parentId: '8', name: 'logo.png', icon: 'file', isDirectory: false, expanded: true, }, { id: '10', parentId: '8', name: 'banner.gif', icon: 'file', isDirectory: false, expanded: true, }, { id: '11', name: 'System', icon: 'activefolder', isDirectory: true, expanded: true, }, { id: '12', parentId: '11', name: 'Employees.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '13', parentId: '11', name: 'PasswordList.txt', icon: 'file', isDirectory: false, expanded: true, }, { id: '14', name: 'Description.rtf', icon: 'file', isDirectory: false, expanded: true, }, { id: '15', name: 'Description.txt', icon: 'file', isDirectory: false, expanded: true, }, ]; export default { getItemsDriveC() { return itemsDriveC; }, getItemsDriveD() { return itemsDriveD; }, };
<!DOCTYPE html> <html lang="en"> <head> <title>DevExtreme Demo</title> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=5.0" /> <link rel="stylesheet" type="text/css" href="https://cdn3.devexpress.com/jslib/25.1.7/css/dx.light.css" /> <link rel="stylesheet" type="text/css" href="styles.css" /> <script src="https://cdn.jsdelivr.net/npm/core-js@2.6.12/client/shim.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.3/dist/system.js"></script> <script type="text/javascript" src="config.js"></script> <script type="text/javascript"> System.import("./index.tsx"); </script> </head> <body class="dx-viewport"> <div class="demo-container"> <div id="app"></div> </div> </body> </html>
.form { display: flex; } .form > div { display: inline-block; vertical-align: top; } .dx-treeview-item { box-sizing: border-box; } .drive-header { min-height: auto; padding: 0; cursor: default; margin-bottom: 10px; } .drive-panel { padding: 20px 30px; font-size: 115%; font-weight: bold; border-right: 1px solid rgba(165, 165, 165, 0.4); height: 100%; } .drive-panel:last-of-type { border-right: none; }