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 +0 -0
- package/package.json +39 -0
- package/src/dock.js +156 -0
- package/src/frame.js +189 -0
- package/src/handles/dock-drop-handler.js +187 -0
- package/src/handles/drop-handler.js +348 -0
- package/src/handles/frame-adjust-handler.js +119 -0
- package/src/handles/tab-bar-drop-handler.js +173 -0
- package/src/index.js +115 -0
- package/src/panel.js +122 -0
- package/src/style.css +276 -0
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
|
+
}
|