split-dock 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
Binary file
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "split-dock",
3
+ "version": "1.0.0",
4
+ "description": "A lightweight, flexible docking framework for building split-view layouts with drag-and-drop panel management",
5
+ "main": "src/index.js",
6
+ "type": "module",
7
+ "files": [
8
+ "src/**/*",
9
+ "README.md"
10
+ ],
11
+ "scripts": {
12
+ "test": "echo \"Error: no test specified\" && exit 1"
13
+ },
14
+ "keywords": [
15
+ "dock",
16
+ "docking",
17
+ "split-view",
18
+ "layout",
19
+ "drag-and-drop",
20
+ "panels",
21
+ "resizable",
22
+ "tabs",
23
+ "ui",
24
+ "framework"
25
+ ],
26
+ "author": "",
27
+ "license": "MIT",
28
+ "repository": {
29
+ "type": "git",
30
+ "url": "git+https://github.com/Emaneliforp/split-dock.git"
31
+ },
32
+ "bugs": {
33
+ "url": "https://github.com/Emaneliforp/split-dock/issues"
34
+ },
35
+ "homepage": "https://github.com/Emaneliforp/split-dock#readme",
36
+ "engines": {
37
+ "node": ">=14.0.0"
38
+ }
39
+ }
package/src/dock.js ADDED
@@ -0,0 +1,156 @@
1
+ import { Panel } from './panel.js';
2
+ import { DropHandler } from './handles/drop-handler.js';
3
+ import { generateId } from './index.js';
4
+
5
+ // Dock class - contains panels
6
+ export class Dock {
7
+ constructor(element, parentFrame) {
8
+ this.id = generateId();
9
+ this.parentFrame = parentFrame;
10
+ this.splitDock = parentFrame?.splitDock || null;
11
+ this.panels = [];
12
+ this.activePanel = null;
13
+ this.eventListeners = [];
14
+
15
+ this.element = element || document.createElement('div');
16
+ this.initializeElements();
17
+
18
+ this.dropHandler = new DropHandler(this);
19
+
20
+ this.setupEventListeners();
21
+ }
22
+
23
+ initializeElements() {
24
+ const hasExistingDock = this.element.classList.contains('sd-dock');
25
+
26
+ if (!hasExistingDock) {
27
+ this.element.className = 'sd-dock';
28
+ }
29
+
30
+ this.findOrCreateSubElements();
31
+
32
+ if (hasExistingDock) {
33
+ this.loadPanelsFromHTML();
34
+ }
35
+ }
36
+
37
+ findOrCreateSubElements() {
38
+ this.navbar = this.element.querySelector('.sd-dock-navbar') || this.createNavbar();
39
+ this.content = this.element.querySelector('.sd-dock-content') || this.createContent();
40
+ }
41
+
42
+ createNavbar() {
43
+ const navbar = document.createElement('div');
44
+ navbar.className = 'sd-dock-navbar';
45
+ this.element.insertBefore(navbar, this.element.firstChild);
46
+ return navbar;
47
+ }
48
+
49
+ createContent() {
50
+ const content = document.createElement('div');
51
+ content.className = 'sd-dock-content';
52
+ this.element.appendChild(content);
53
+ return content;
54
+ }
55
+
56
+ loadPanelsFromHTML() {
57
+ const panelElements = Array.from(this.element.querySelectorAll('.sd-panel'));
58
+
59
+ panelElements.forEach(panelElement => {
60
+ const titleEl = panelElement.querySelector('.sd-panel-title');
61
+ const contentEl = panelElement.querySelector('.sd-panel-content');
62
+
63
+ if (!titleEl || !contentEl) return;
64
+
65
+ panelElement.remove();
66
+ const panel = new Panel(titleEl, contentEl);
67
+ this.addPanel(panel);
68
+ });
69
+ }
70
+
71
+ setupEventListeners() {
72
+ const dragOverHandler = (e) => this.dropHandler.onDragOver(e);
73
+ this.element.addEventListener('dragover', dragOverHandler);
74
+ this.eventListeners.push({ event: 'dragover', handler: dragOverHandler });
75
+
76
+ const dropHandler = (e) => this.dropHandler.onDrop(e);
77
+ this.element.addEventListener('drop', dropHandler);
78
+ this.eventListeners.push({ event: 'drop', handler: dropHandler });
79
+
80
+ const dragLeaveHandler = (e) => this.dropHandler.onDragLeave(e);
81
+ this.element.addEventListener('dragleave', dragLeaveHandler);
82
+ this.eventListeners.push({ event: 'dragleave', handler: dragLeaveHandler });
83
+ }
84
+
85
+ addPanel(panel) {
86
+ panel.dock = this;
87
+ this.panels.push(panel);
88
+ this.navbar.appendChild(panel.titleElement);
89
+ this.content.appendChild(panel.contentElement);
90
+
91
+ if (this.panels.length === 1) {
92
+ this.setActivePanel(panel);
93
+ }
94
+
95
+ return panel;
96
+ }
97
+
98
+ acceptPanel(panel, fromDock) {
99
+ fromDock.removePanel(panel, true);
100
+ this.addPanel(panel);
101
+ this.setActivePanel(panel);
102
+ }
103
+
104
+ removePanel(panel, skipCheck = false) {
105
+ const index = this.panels.indexOf(panel);
106
+ if (index === -1) return;
107
+
108
+ this.panels.splice(index, 1);
109
+
110
+ if (!skipCheck) {
111
+ panel.remove();
112
+ }
113
+
114
+ if (this.activePanel === panel) {
115
+ this.activePanel = null;
116
+ if (this.panels.length > 0) {
117
+ const newIndex = Math.min(index, this.panels.length - 1);
118
+ this.setActivePanel(this.panels[newIndex]);
119
+ }
120
+ }
121
+
122
+ // If dock is empty and has parent, remove it
123
+ if (this.panels.length === 0 && this.parentFrame) {
124
+ this.parentFrame.removeChild(this);
125
+ }
126
+ }
127
+
128
+ setActivePanel(panel) {
129
+ this.panels.forEach(p => p.deactivate());
130
+ panel.activate();
131
+ this.activePanel = panel;
132
+ }
133
+
134
+ remove() {
135
+ this.destroy();
136
+ this.element.remove();
137
+ }
138
+
139
+ destroy() {
140
+ // Remove all event listeners
141
+ this.eventListeners.forEach(({ event, handler }) => {
142
+ this.element.removeEventListener(event, handler);
143
+ });
144
+ this.eventListeners = [];
145
+
146
+ // Destroy all panels
147
+ this.panels.forEach(panel => panel.destroy());
148
+ this.panels = [];
149
+
150
+ // Clear references
151
+ this.activePanel = null;
152
+ this.parentFrame = null;
153
+ this.splitDock = null;
154
+ this.dropHandler = null;
155
+ }
156
+ }
package/src/frame.js ADDED
@@ -0,0 +1,189 @@
1
+ import { Dock } from './dock.js';
2
+ import { FrameAdjustHandler } from './handles/frame-adjust-handler.js';
3
+ import { generateId } from './index.js';
4
+
5
+ // Frame class - can contain docks or other frames
6
+ export class Frame {
7
+ constructor(element, splitDock) {
8
+ this.id = generateId();
9
+ this.element = element;
10
+ this.splitDock = splitDock;
11
+ this.adjustHandler = new FrameAdjustHandler(this);
12
+ this.parentFrame = null;
13
+ this.children = []; // Can contain Dock or Frame
14
+ this.splitDirection = null; // null, 'horizontal', or 'vertical'
15
+
16
+ if (element) {
17
+ this.loadChildrenFromHTML();
18
+ } else {
19
+ this.createElements();
20
+ }
21
+
22
+ this.adjustHandler.setupResizeHandles();
23
+ }
24
+
25
+ createElements() {
26
+ this.element = document.createElement('div');
27
+ this.element.className = 'sd-frame';
28
+ }
29
+
30
+ loadChildrenFromHTML() {
31
+ // Detect split direction from existing classes
32
+ if (this.element.classList.contains('horizontal')) {
33
+ this.splitDirection = 'horizontal';
34
+ } else if (this.element.classList.contains('vertical')) {
35
+ this.splitDirection = 'vertical';
36
+ }
37
+
38
+ // First check for dock elements (if this is a leaf window)
39
+ const dockElements = Array.from(this.element.querySelectorAll(':scope > .sd-dock'));
40
+
41
+ if (dockElements.length > 0) {
42
+ // If there are multiple docks, wrap each in its own frame
43
+ if (dockElements.length > 1 && this.splitDirection) {
44
+ dockElements.forEach(dockElement => {
45
+ // Create a wrapper frame for each dock
46
+ const frameWrapper = document.createElement('div');
47
+ frameWrapper.className = 'sd-frame';
48
+
49
+ // Insert wrapper before the dock
50
+ dockElement.parentNode.insertBefore(frameWrapper, dockElement);
51
+ // Move dock into wrapper
52
+ frameWrapper.appendChild(dockElement);
53
+
54
+ // Create Frame instance for wrapper
55
+ const childFrame = new Frame(frameWrapper, this.splitDock);
56
+ childFrame.parentFrame = this;
57
+ this.children.push(childFrame);
58
+ });
59
+ } else {
60
+ // Single dock, add directly
61
+ dockElements.forEach(dockElement => {
62
+ const dock = new Dock(dockElement, this);
63
+ this.children.push(dock);
64
+ });
65
+ }
66
+ } else {
67
+ // Check for nested frames
68
+ const windowElements = Array.from(this.element.querySelectorAll(':scope > .sd-frame'));
69
+
70
+ windowElements.forEach(winElement => {
71
+ const childWindow = new Frame(winElement, this.splitDock);
72
+ childWindow.parentFrame = this;
73
+ this.children.push(childWindow);
74
+ });
75
+ }
76
+ }
77
+
78
+ addChild(child) {
79
+ if (child instanceof Dock) {
80
+ child.parentFrame = this;
81
+ child.splitDock = this.splitDock;
82
+ } else if (child instanceof Frame) {
83
+ child.parentFrame = this;
84
+ }
85
+
86
+ this.children.push(child);
87
+ this.element.appendChild(child.element);
88
+
89
+ // Update styles after adding a child
90
+ this.updateStyles();
91
+
92
+ return child;
93
+ }
94
+
95
+ removeChild(child) {
96
+ const index = this.children.indexOf(child);
97
+ if (index === -1) return;
98
+
99
+ child.remove();
100
+ this.children.splice(index, 1);
101
+
102
+ // Clean up if only one child left - promote the child
103
+ if (this.children.length === 1) {
104
+ this.splitDirection = null;
105
+ this.element.classList.remove('horizontal', 'vertical');
106
+
107
+ const remainingChild = this.children[0];
108
+
109
+ // Reset flex style to fill available space
110
+ remainingChild.element.style.flex = '';
111
+
112
+ // If this window has a parent, we should promote the remaining child
113
+ if (this.parentFrame) {
114
+ const parentIndex = this.parentFrame.children.indexOf(this);
115
+
116
+ if (parentIndex !== -1) {
117
+ // Replace this window with the remaining child in parent
118
+ this.parentFrame.children[parentIndex] = remainingChild;
119
+
120
+ // Update parent reference
121
+ if (remainingChild instanceof Dock) {
122
+ remainingChild.parentFrame = this.parentFrame;
123
+ remainingChild.splitDock = this.parentFrame.splitDock;
124
+ } else if (remainingChild instanceof Frame) {
125
+ remainingChild.parentFrame = this.parentFrame;
126
+ }
127
+
128
+ // Move the child element to parent and remove this window
129
+ this.parentFrame.element.insertBefore(remainingChild.element, this.element);
130
+ this.element.remove();
131
+ this.children = [];
132
+ }
133
+ }
134
+ }
135
+
136
+ // If no children and has parent, remove self
137
+ if (this.children.length === 0) {
138
+ if (this.parentFrame) {
139
+ this.parentFrame.removeChild(this);
140
+ } else {
141
+ // Root window with no children, just remove the element
142
+ this.element.remove();
143
+ }
144
+ }
145
+
146
+ // Update styles after removing a child
147
+ this.updateStyles();
148
+ }
149
+
150
+ remove() {
151
+ this.destroy();
152
+ this.element.remove();
153
+ }
154
+
155
+ destroy() {
156
+ // Destroy all children
157
+ this.children.forEach(child => {
158
+ if (child instanceof Dock || child instanceof Frame) {
159
+ child.destroy();
160
+ }
161
+ });
162
+ this.children = [];
163
+
164
+ // Cleanup adjust handler
165
+ if (this.adjustHandler) {
166
+ this.adjustHandler.destroy();
167
+ this.adjustHandler = null;
168
+ }
169
+
170
+ // Clear references
171
+ this.parentFrame = null;
172
+ this.splitDock = null;
173
+ }
174
+
175
+ updateStyles() {
176
+ // Update split direction classes
177
+ if (this.splitDirection) {
178
+ this.element.classList.remove('horizontal', 'vertical');
179
+ this.element.classList.add(this.splitDirection);
180
+ } else {
181
+ this.element.classList.remove('horizontal', 'vertical');
182
+ }
183
+
184
+ // Update resize handles
185
+ this.adjustHandler.setupResizeHandles();
186
+ }
187
+
188
+
189
+ }
@@ -0,0 +1,187 @@
1
+ import { Frame } from '../frame.js';
2
+ import { Dock } from '../dock.js';
3
+
4
+ export class DockDropHandler {
5
+ constructor(dock) {
6
+ this.dock = dock;
7
+ }
8
+
9
+ onDragOver(e) {
10
+ if (!this.dock.splitDock?.draggedPanel) return;
11
+
12
+ // If over navbar, let the tab handler deal with it exclusively
13
+ if (this.isOverNavbar(e)) {
14
+ return;
15
+ }
16
+
17
+ e.preventDefault();
18
+ e.stopPropagation();
19
+ e.dataTransfer.dropEffect = 'move';
20
+
21
+ // Clear previous dock's indicators if switching to a new dock
22
+ if (this.dock.splitDock.currentHoverDock && this.dock.splitDock.currentHoverDock !== this.dock) {
23
+ this.dock.splitDock.currentHoverDock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
24
+ }
25
+ this.dock.splitDock.currentHoverDock = this.dock;
26
+
27
+ // Remove all zone classes
28
+ this.dock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
29
+
30
+ const zone = this.getDropZoneFromEvent(e);
31
+ if (zone) {
32
+ this.dock.element.classList.add(`drop-${zone}`);
33
+ }
34
+ }
35
+
36
+ onDragLeave(e) {
37
+ const rect = this.dock.element.getBoundingClientRect();
38
+ if (e.clientX < rect.left || e.clientX >= rect.right ||
39
+ e.clientY < rect.top || e.clientY >= rect.bottom) {
40
+ this.dock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
41
+ }
42
+ }
43
+
44
+ onDrop(e) {
45
+ // If over navbar, let the tab handler deal with it
46
+ if (this.isOverNavbar(e)) {
47
+ return;
48
+ }
49
+
50
+ e.preventDefault();
51
+ e.stopPropagation();
52
+
53
+ if (!this.dock.splitDock) return;
54
+
55
+ const panel = this.dock.splitDock.draggedPanel;
56
+ const fromDock = this.dock.splitDock.draggedFromDock;
57
+
58
+ if (!panel || !fromDock) return;
59
+
60
+ this.dock.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
61
+
62
+ const zone = this.getDropZoneFromEvent(e);
63
+
64
+ if (zone === 'center') {
65
+ if (fromDock !== this.dock) {
66
+ this.dock.acceptPanel(panel, fromDock);
67
+ }
68
+ return;
69
+ }
70
+
71
+ // Don't allow splitting if dragging the only panel from the same dock
72
+ if (fromDock === this.dock && fromDock.panels.length === 1) return;
73
+
74
+ this.createSplit(zone, panel, fromDock);
75
+ }
76
+
77
+ isOverNavbar(e) {
78
+ // Check if mouse is over ANY navbar, not just this dock's navbar
79
+ const allNavbars = document.querySelectorAll('.sd-dock-navbar');
80
+ for (const navbar of allNavbars) {
81
+ const rect = navbar.getBoundingClientRect();
82
+ if (e.clientX >= rect.left &&
83
+ e.clientX <= rect.right &&
84
+ e.clientY >= rect.top &&
85
+ e.clientY <= rect.bottom) {
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ getDropZoneFromEvent(e) {
93
+ const rect = this.dock.element.getBoundingClientRect();
94
+ const x = e.clientX - rect.left;
95
+ const y = e.clientY - rect.top;
96
+ return this.getDropZone(x, y, rect.width, rect.height);
97
+ }
98
+
99
+ getDropZone(x, y, width, height) {
100
+ const topThreshold = height * 0.3;
101
+ const bottomThreshold = height * 0.7;
102
+ const leftThreshold = width * 0.3;
103
+ const rightThreshold = width * 0.7;
104
+
105
+ if (y < topThreshold) return 'top';
106
+ if (y > bottomThreshold) return 'bottom';
107
+ if (x < leftThreshold) return 'left';
108
+ if (x > rightThreshold) return 'right';
109
+ return 'center';
110
+ }
111
+
112
+ createSplit(direction, panel, fromDock) {
113
+ if (!this.dock.splitDock) return;
114
+ if (fromDock === this.dock && fromDock.panels.length === 1) return;
115
+ if (!this.dock.parentFrame) return;
116
+
117
+ fromDock.removePanel(panel, true);
118
+
119
+ const newDock = new Dock(null, null);
120
+ newDock.addPanel(panel);
121
+
122
+ const splitDir = (direction === 'left' || direction === 'right') ? 'horizontal' : 'vertical';
123
+ const insertBefore = (direction === 'top' || direction === 'left');
124
+
125
+ this.applySplit(newDock, splitDir, insertBefore);
126
+ }
127
+
128
+ applySplit(newDock, splitDir, insertBefore) {
129
+ const parent = this.dock.parentFrame;
130
+ const isOnlyChild = parent.children.length === 1 && parent.children[0] === this.dock;
131
+ const hasSameSplitDir = parent.splitDirection === splitDir;
132
+
133
+ if (isOnlyChild) {
134
+ this.splitIntoParent(parent, newDock, splitDir, insertBefore);
135
+ } else if (hasSameSplitDir) {
136
+ this.insertIntoParent(parent, newDock, insertBefore);
137
+ } else {
138
+ this.createNestedSplit(parent, newDock, splitDir, insertBefore);
139
+ }
140
+ }
141
+
142
+ splitIntoParent(parent, newDock, splitDir, insertBefore) {
143
+ parent.splitDirection = splitDir;
144
+ parent.children = [];
145
+
146
+ if (insertBefore) {
147
+ parent.addChild(newDock);
148
+ parent.addChild(this.dock);
149
+ } else {
150
+ parent.addChild(this.dock);
151
+ parent.addChild(newDock);
152
+ }
153
+ }
154
+
155
+ insertIntoParent(parent, newDock, insertBefore) {
156
+ const index = parent.children.indexOf(this.dock);
157
+ const insertIndex = insertBefore ? index : index + 1;
158
+
159
+ parent.children.splice(insertIndex, 0, newDock);
160
+ newDock.parentFrame = parent;
161
+
162
+ const refElement = insertBefore ? this.dock.element : this.dock.element.nextSibling;
163
+ parent.element.insertBefore(newDock.element, refElement);
164
+ parent.updateStyles();
165
+ }
166
+
167
+ createNestedSplit(parent, newDock, splitDir, insertBefore) {
168
+ const newFrame = new Frame(null, this.dock.splitDock);
169
+ newFrame.splitDirection = splitDir;
170
+
171
+ const index = parent.children.indexOf(this.dock);
172
+ parent.children[index] = newFrame;
173
+
174
+ parent.element.insertBefore(newFrame.element, this.dock.element);
175
+ this.dock.element.remove();
176
+
177
+ if (insertBefore) {
178
+ newFrame.addChild(newDock);
179
+ newFrame.addChild(this.dock);
180
+ } else {
181
+ newFrame.addChild(this.dock);
182
+ newFrame.addChild(newDock);
183
+ }
184
+
185
+ parent.updateStyles();
186
+ }
187
+ }