A library of web components for modern web applications, featuring the powerful Node Explorer component.
A hierarchical tree view component with drag-and-drop capabilities, multi-selection support, and keyboard navigation.
npm install @jmkcoder/components
// Simple import - automatically registers all components
import '@jmkcoder/components';
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="./node_modules/@jmkcoder/components/dist/components.bundle.css">
<script type="module" src="./node_modules/@jmkcoder/components/dist/index.js"></script>
<script>
window.addEventListener('DOMContentLoaded', () => {
// Register the components
OdysseyComponents.defineNodeExplorer();
});
</script>
<style>
odyssey-node-explorer {
height: 500px;
display: block;
border: 1px solid #e0e0e0;
border-radius: 4px;
}
</style>
</head>
<body>
<h1>Node Explorer Example</h1>
<odyssey-node-explorer id="explorer"></odyssey-node-explorer>
<script>
window.addEventListener('DOMContentLoaded', () => {
const explorer = document.getElementById('explorer');
// Set up nodes data
const nodes = [
{
id: 'root',
label: 'Project Files',
expanded: true,
icon: 'folder',
children: [
{ id: 'src', label: 'src', icon: 'folder' },
{ id: 'docs', label: 'docs', icon: 'folder' },
{ id: 'package.json', label: 'package.json', icon: 'description' }
]
}
];
// Set nodes to the explorer
explorer.setAttribute('nodes', JSON.stringify(nodes));
// Listen for events
explorer.addEventListener('node-selected', (e) => {
console.log('Selected:', e.detail.node);
});
});
</script>
</body>
</html>
Check out the complete examples in the examples/
directory:
-
basic-example.html
: Simple demo with minimal setup -
usage-example.html
: Advanced usage with all features
Attribute | Type | Default | Description |
---|---|---|---|
nodes |
String (JSON) | '[]' |
A JSON string of the tree nodes structure |
allow-drag-drop |
Boolean | true |
Enable or disable drag and drop functionality |
allow-multi-select |
Boolean | false |
Enable or disable multi-selection of nodes |
theme |
String | 'light' |
Theme variant ('light', 'dark', 'minimal', 'high-contrast') |
// Basic node structure
{
id: 'unique-id', // Required unique identifier
label: 'Node Label', // Display text
expanded: false, // Whether the node is expanded (if it has children)
icon: 'folder', // Optional icon name (using Material Icons)
children: [], // Optional array of child nodes
hasChildren: false, // Indicate if node has children even if not loaded
isLazy: false, // For dynamic loading of children
isLoading: false // Indicates loading state
}
// Get a reference to the node explorer
const explorer = document.getElementById('explorer');
// Node manipulation
explorer.addNode('parent-id', { id: 'new-node', label: 'New Node' });
explorer.removeNode('node-id');
explorer.moveNode('source-id', 'target-id', 'inside'); // 'inside', 'before', or 'after'
// Node state control
explorer.expandNode('node-id');
explorer.collapseNode('node-id');
explorer.selectNode('node-id');
// Get node information
const selectedNode = explorer.getSelectedNode();
const selectedNodes = explorer.getSelectedNodes(); // For multi-select
const node = explorer.findNodeById('node-id');
// Lazy loading
explorer.markNodeAsLazy('node-id', true);
explorer.setNodeChildren('parent-id', childNodes);
explorer.appendNodeChildren('parent-id', moreChildNodes);
// Selection events
explorer.addEventListener('node-selected', (e) => {
console.log('Selected node:', e.detail.node);
});
explorer.addEventListener('nodes-selected', (e) => {
console.log('Multiple nodes selected:', e.detail.nodes);
});
// Expansion events
explorer.addEventListener('node-expanded', (e) => {
console.log('Node expanded:', e.detail.nodeId);
});
explorer.addEventListener('node-collapsed', (e) => {
console.log('Node collapsed:', e.detail.nodeId);
});
// Lazy loading
explorer.addEventListener('load-children', (e) => {
const { nodeId, node } = e.detail;
console.log(`Loading children for node: ${nodeId}`);
// Simulate fetching data
setTimeout(() => {
const children = [
{ id: `${nodeId}-child1`, label: 'Child 1' },
{ id: `${nodeId}-child2`, label: 'Child 2' }
];
explorer.setNodeChildren(nodeId, children);
}, 500);
});
// Drag and drop events
explorer.addEventListener('drop', (e) => {
console.log('Node dropped:', e.detail);
});
// Structure changes
explorer.addEventListener('nodes-changed', (e) => {
console.log('Node structure changed:', e.detail.nodes);
// You can persist this new structure to your backend
});
Four built-in themes: light, dark, minimal, and high-contrast for accessibility.
// Apply a theme
explorer.setAttribute('theme', 'dark');
// Or use the property
explorer.theme = 'high-contrast';
Full keyboard support for navigation and selection:
- Arrow keys for navigation
- Space/Enter for selection
- Home/End for jumping to first/last node
Intuitive drag and drop functionality for reorganizing nodes:
- Drag nodes to reorder within the same parent
- Drag nodes to new parents
- Visual indicators show valid drop targets
// Disable drag and drop
explorer.setAttribute('allow-drag-drop', 'false');
// Or use the property
explorer.allowDragDrop = false;
Support for selecting multiple nodes using Ctrl/Cmd+click and Shift+click.
// Enable multi-select
explorer.setAttribute('allow-multi-select', 'true');
// Or use the property
explorer.allowMultiSelect = true;
Efficient handling of large data sets by loading children on demand:
// Mark a node as lazy-loadable
explorer.markNodeAsLazy('folder-id', true);
// Listen for load events
explorer.addEventListener('load-children', handleLoadChildren);
function handleLoadChildren(event) {
const { nodeId } = event.detail;
// Fetch data from server
fetchChildrenFromServer(nodeId).then(children => {
// Provide the children to the explorer
explorer.setNodeChildren(nodeId, children);
});
}
You can customize the component appearance using CSS custom properties:
odyssey-node-explorer {
--primary-color: #3b82f6;
--primary-light: #93c5fd;
--gray-light: #f3f4f6;
--gray-medium: #e5e7eb;
--text-color: #111827;
--text-light: #6b7280;
--node-padding: 6px 8px;
--node-margin: 2px 0;
--node-border-radius: 6px;
--node-font-size: 13px;
--connector-width: 2px;
--connector-color: #e8e8e8;
--selection-bg: rgba(59, 130, 246, 0.2);
--transition-duration: 200ms;
--transition-timing: cubic-bezier(0.4, 0, 0.2, 1);
}
/* Dark theme example */
odyssey-node-explorer.dark-theme {
--primary-color: #60a5fa;
--primary-light: #2563eb;
--gray-light: #374151;
--gray-medium: #1f2937;
--text-color: #f9fafb;
--text-light: #d1d5db;
--connector-color: #4b5563;
--selection-bg: rgba(96, 165, 250, 0.2);
}
odyssey-components/
├── src/ # Source code
│ ├── components/ # Web components
│ │ ├── node-explorer/ # Node Explorer component
│ │ │ ├── node-explorer.ts # Main component file
│ │ │ ├── node-explorer.scss # Component styles
│ │ │ ├── node-explorer.type.ts # TypeScript interfaces
│ │ │ └── services/ # Component services
│ │ │ ├── node.service.ts # Node data handling
│ │ │ ├── node-renderer.service.ts # DOM rendering
│ │ │ └── drag-drop.service.ts # Drag & drop handling
│ │ └── odyssey-button/ # Button component
│ ├── utilities/ # Utility functions
│ └── stories/ # Storybook documentation
├── examples/ # Usage examples
└── distlib/ # Built distribution files
To develop and test the components locally:
# Install dependencies
npm install
# Run development server with hot reloading
npm run dev
# Run Storybook for component documentation
npm run storybook
# Build the library
npm run build
# Build Storybook documentation site
npm run build-storybook
- Make changes to component files in
src/components/
- Test in development server or Storybook
- Build the library when ready
- Check the examples in the
examples/
directory to verify the build
Contributions are welcome! Please feel free to submit a Pull Request.
ISC
The Node Explorer component supports all modern browsers:
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
This component uses Material Icons for node icons.