split-dock 1.0.0 → 1.0.1

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 CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "split-dock",
3
- "version": "1.0.0",
3
+ "version": "1.0.1",
4
4
  "description": "A lightweight, flexible docking framework for building split-view layouts with drag-and-drop panel management",
5
5
  "main": "src/index.js",
6
6
  "type": "module",
package/src/dock.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import { Panel } from './panel.js';
2
- import { DropHandler } from './handles/drop-handler.js';
3
- import { generateId } from './index.js';
2
+ import { DragDropHandler } from './handles/drag-drop-handler.js';
3
+ import { generateId, CONFIG } from './index.js';
4
4
 
5
5
  // Dock class - contains panels
6
6
  export class Dock {
@@ -10,76 +10,54 @@ export class Dock {
10
10
  this.splitDock = parentFrame?.splitDock || null;
11
11
  this.panels = [];
12
12
  this.activePanel = null;
13
- this.eventListeners = [];
13
+ this.abortController = new AbortController();
14
14
 
15
15
  this.element = element || document.createElement('div');
16
16
  this.initializeElements();
17
17
 
18
- this.dropHandler = new DropHandler(this);
18
+ this.dragDropHandler = new DragDropHandler(this);
19
19
 
20
20
  this.setupEventListeners();
21
21
  }
22
22
 
23
23
  initializeElements() {
24
24
  const hasExistingDock = this.element.classList.contains('sd-dock');
25
+ if (!hasExistingDock) this.element.className = 'sd-dock';
25
26
 
26
- if (!hasExistingDock) {
27
- this.element.className = 'sd-dock';
27
+ this.navbar = this.element.querySelector('.sd-dock-navbar');
28
+ if (!this.navbar) {
29
+ this.navbar = document.createElement('div');
30
+ this.navbar.className = 'sd-dock-navbar';
31
+ this.element.insertBefore(this.navbar, this.element.firstChild);
28
32
  }
29
33
 
30
- this.findOrCreateSubElements();
31
-
32
- if (hasExistingDock) {
33
- this.loadPanelsFromHTML();
34
+ this.content = this.element.querySelector('.sd-dock-content');
35
+ if (!this.content) {
36
+ this.content = document.createElement('div');
37
+ this.content.className = 'sd-dock-content';
38
+ this.element.appendChild(this.content);
34
39
  }
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;
40
+
41
+ if (hasExistingDock) this.loadPanelsFromHTML();
54
42
  }
55
43
 
56
44
  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);
45
+ this.element.querySelectorAll('.sd-panel').forEach(panelEl => {
46
+ const titleEl = panelEl.querySelector('.sd-panel-title');
47
+ const contentEl = panelEl.querySelector('.sd-panel-content');
48
+ if (titleEl && contentEl) {
49
+ panelEl.remove();
50
+ this.addPanel(new Panel(titleEl, contentEl));
51
+ }
68
52
  });
69
53
  }
70
54
 
71
55
  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 });
56
+ const signal = this.abortController.signal;
57
+
58
+ this.element.addEventListener('dragover', (e) => this.dragDropHandler.onDragOver(e, this), { signal });
59
+ this.element.addEventListener('drop', (e) => this.dragDropHandler.onDrop(e, this), { signal });
60
+ this.element.addEventListener('dragleave', (e) => this.dragDropHandler.onDragLeave(e, this), { signal });
83
61
  }
84
62
 
85
63
  addPanel(panel) {
@@ -87,11 +65,7 @@ export class Dock {
87
65
  this.panels.push(panel);
88
66
  this.navbar.appendChild(panel.titleElement);
89
67
  this.content.appendChild(panel.contentElement);
90
-
91
- if (this.panels.length === 1) {
92
- this.setActivePanel(panel);
93
- }
94
-
68
+ if (this.panels.length === 1) this.setActivePanel(panel);
95
69
  return panel;
96
70
  }
97
71
 
@@ -106,20 +80,15 @@ export class Dock {
106
80
  if (index === -1) return;
107
81
 
108
82
  this.panels.splice(index, 1);
109
-
110
- if (!skipCheck) {
111
- panel.remove();
112
- }
83
+ if (!skipCheck) panel.remove();
113
84
 
114
85
  if (this.activePanel === panel) {
115
86
  this.activePanel = null;
116
87
  if (this.panels.length > 0) {
117
- const newIndex = Math.min(index, this.panels.length - 1);
118
- this.setActivePanel(this.panels[newIndex]);
88
+ this.setActivePanel(this.panels[Math.min(index, this.panels.length - 1)]);
119
89
  }
120
90
  }
121
91
 
122
- // If dock is empty and has parent, remove it
123
92
  if (this.panels.length === 0 && this.parentFrame) {
124
93
  this.parentFrame.removeChild(this);
125
94
  }
@@ -131,26 +100,106 @@ export class Dock {
131
100
  this.activePanel = panel;
132
101
  }
133
102
 
134
- remove() {
135
- this.destroy();
136
- this.element.remove();
103
+ clearDropIndicators() {
104
+ this.element.classList.remove('drop-center', 'drop-top', 'drop-bottom', 'drop-left', 'drop-right', 'drop-navbar');
105
+ this.navbar.querySelectorAll('.sd-panel-title').forEach(el =>
106
+ el.classList.remove('drop-before', 'drop-after')
107
+ );
137
108
  }
138
109
 
139
- destroy() {
140
- // Remove all event listeners
141
- this.eventListeners.forEach(({ event, handler }) => {
142
- this.element.removeEventListener(event, handler);
143
- });
144
- this.eventListeners = [];
110
+ showNavbarDropIndicator(e) {
111
+ const titles = Array.from(this.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
112
+ if (titles.length === 0) return;
113
+
114
+ const firstRect = titles[0].getBoundingClientRect();
115
+ const lastRect = titles[titles.length - 1].getBoundingClientRect();
116
+
117
+ if (e.clientX < firstRect.left) {
118
+ titles[0].classList.add('drop-before');
119
+ } else if (e.clientX > lastRect.right) {
120
+ titles[titles.length - 1].classList.add('drop-after');
121
+ } else {
122
+ const hovered = titles.find(el => {
123
+ const rect = el.getBoundingClientRect();
124
+ return e.clientX >= rect.left && e.clientX <= rect.right;
125
+ });
126
+ if (hovered) {
127
+ const rect = hovered.getBoundingClientRect();
128
+ hovered.classList.add(e.clientX < rect.left + rect.width / 2 ? 'drop-before' : 'drop-after');
129
+ }
130
+ }
131
+ }
132
+
133
+ getDropTargetPanelInfo(e) {
134
+ const titles = Array.from(this.navbar.querySelectorAll('.sd-panel-title:not(.dragging)'));
135
+ if (titles.length === 0) return null;
136
+
137
+ const firstRect = titles[0].getBoundingClientRect();
138
+ const lastRect = titles[titles.length - 1].getBoundingClientRect();
139
+
140
+ let targetTitle, position;
141
+ if (e.clientX < firstRect.left) {
142
+ targetTitle = titles[0];
143
+ position = 'before';
144
+ } else if (e.clientX > lastRect.right) {
145
+ targetTitle = titles[titles.length - 1];
146
+ position = 'after';
147
+ } else {
148
+ targetTitle = titles.find(el => {
149
+ const rect = el.getBoundingClientRect();
150
+ return e.clientX >= rect.left && e.clientX <= rect.right;
151
+ });
152
+ if (targetTitle) {
153
+ const rect = targetTitle.getBoundingClientRect();
154
+ position = e.clientX < rect.left + rect.width / 2 ? 'before' : 'after';
155
+ }
156
+ }
157
+
158
+ const panel = this.panels.find(p => p.titleElement === targetTitle);
159
+ return panel ? { panel, position } : null;
160
+ }
161
+
162
+ reorderPanel(panel, targetPanel, position) {
163
+ const panelIdx = this.panels.indexOf(panel);
164
+ const targetIdx = this.panels.indexOf(targetPanel);
165
+ if (panelIdx === -1 || targetIdx === -1) return;
166
+
167
+ this.panels.splice(panelIdx, 1);
168
+ let newIdx = targetIdx + (position === 'after' ? 1 : 0);
169
+ if (panelIdx < targetIdx) newIdx--;
170
+ this.panels.splice(newIdx, 0, panel);
171
+
172
+ const refNode = position === 'before' ? targetPanel.titleElement : targetPanel.titleElement.nextSibling;
173
+ this.navbar.insertBefore(panel.titleElement, refNode);
174
+ }
175
+
176
+ acceptPanelAt(panel, fromDock, targetPanel, position) {
177
+ fromDock.removePanel(panel, true);
145
178
 
146
- // Destroy all panels
179
+ const targetIdx = this.panels.indexOf(targetPanel);
180
+ if (targetIdx === -1) {
181
+ this.addPanel(panel);
182
+ return;
183
+ }
184
+
185
+ panel.dock = this;
186
+ this.panels.splice(position === 'before' ? targetIdx : targetIdx + 1, 0, panel);
187
+
188
+ const refNode = position === 'before' ? targetPanel.titleElement : targetPanel.titleElement.nextSibling;
189
+ this.navbar.insertBefore(panel.titleElement, refNode);
190
+ this.content.appendChild(panel.contentElement);
191
+ this.setActivePanel(panel);
192
+ }
193
+
194
+ destroy() {
195
+ this.abortController.abort();
147
196
  this.panels.forEach(panel => panel.destroy());
148
197
  this.panels = [];
149
-
150
- // Clear references
151
198
  this.activePanel = null;
152
199
  this.parentFrame = null;
153
200
  this.splitDock = null;
154
- this.dropHandler = null;
201
+ this.dragDropHandler?.destroy();
202
+ this.dragDropHandler = null;
203
+ this.element.remove();
155
204
  }
156
205
  }
package/src/frame.js CHANGED
@@ -1,94 +1,54 @@
1
1
  import { Dock } from './dock.js';
2
2
  import { FrameAdjustHandler } from './handles/frame-adjust-handler.js';
3
- import { generateId } from './index.js';
3
+ import { generateId, CONFIG } from './index.js';
4
4
 
5
5
  // Frame class - can contain docks or other frames
6
6
  export class Frame {
7
7
  constructor(element, splitDock) {
8
8
  this.id = generateId();
9
- this.element = element;
10
9
  this.splitDock = splitDock;
11
10
  this.adjustHandler = new FrameAdjustHandler(this);
12
11
  this.parentFrame = null;
13
- this.children = []; // Can contain Dock or Frame
14
- this.splitDirection = null; // null, 'horizontal', or 'vertical'
12
+ this.children = [];
13
+ this.splitDirection = null;
15
14
 
16
15
  if (element) {
16
+ this.element = element;
17
17
  this.loadChildrenFromHTML();
18
18
  } else {
19
- this.createElements();
19
+ this.element = document.createElement('div');
20
+ this.element.className = 'sd-frame';
20
21
  }
21
22
 
22
23
  this.adjustHandler.setupResizeHandles();
23
24
  }
24
25
 
25
- createElements() {
26
- this.element = document.createElement('div');
27
- this.element.className = 'sd-frame';
28
- }
29
-
30
26
  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'));
27
+ this.detectSplitDirection();
40
28
 
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
- }
29
+ this.element.querySelectorAll(':scope > .sd-dock').forEach(el => {
30
+ this.children.push(new Dock(el, this));
31
+ });
32
+
33
+ this.element.querySelectorAll(':scope > .sd-frame').forEach(el => {
34
+ const frame = new Frame(el, this.splitDock);
35
+ frame.parentFrame = this;
36
+ this.children.push(frame);
37
+ });
38
+ }
39
+
40
+ detectSplitDirection() {
41
+ if (this.element.classList.contains('horizontal')) this.splitDirection = 'horizontal';
42
+ else if (this.element.classList.contains('vertical')) this.splitDirection = 'vertical';
76
43
  }
77
44
 
78
45
  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
- }
46
+ child.parentFrame = this;
47
+ if (child instanceof Dock) child.splitDock = this.splitDock;
85
48
 
86
49
  this.children.push(child);
87
50
  this.element.appendChild(child.element);
88
-
89
- // Update styles after adding a child
90
51
  this.updateStyles();
91
-
92
52
  return child;
93
53
  }
94
54
 
@@ -96,94 +56,122 @@ export class Frame {
96
56
  const index = this.children.indexOf(child);
97
57
  if (index === -1) return;
98
58
 
99
- child.remove();
100
59
  this.children.splice(index, 1);
60
+ child.element?.remove();
101
61
 
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
- }
62
+ if (this.children.length === 1) this.promoteChild();
63
+ else if (this.isEmpty()) this.handleEmptyFrame();
64
+ else this.updateStyles();
65
+ }
135
66
 
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
67
+ promoteChild() {
68
+ this.splitDirection = null;
69
+ this.element.classList.remove('horizontal', 'vertical');
70
+
71
+ const child = this.children[0];
72
+ child.element.style.flex = '';
73
+
74
+ if (this.parentFrame) {
75
+ const parentIdx = this.parentFrame.children.indexOf(this);
76
+ if (parentIdx !== -1) {
77
+ this.parentFrame.children[parentIdx] = child;
78
+ child.parentFrame = this.parentFrame;
79
+ if (child instanceof Dock) child.splitDock = this.parentFrame.splitDock;
80
+
81
+ this.parentFrame.element.insertBefore(child.element, this.element);
142
82
  this.element.remove();
83
+ this.children = [];
84
+ this.parentFrame.updateStyles();
143
85
  }
144
86
  }
145
-
146
- // Update styles after removing a child
147
- this.updateStyles();
148
87
  }
149
88
 
150
- remove() {
151
- this.destroy();
152
- this.element.remove();
89
+ handleEmptyFrame() {
90
+ if (this.parentFrame) this.parentFrame.removeChild(this);
91
+ else this.element.remove();
153
92
  }
154
93
 
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 = [];
94
+ isEmpty() { return this.children.length === 0; }
163
95
 
164
- // Cleanup adjust handler
165
- if (this.adjustHandler) {
166
- this.adjustHandler.destroy();
167
- this.adjustHandler = null;
168
- }
96
+ hasMultipleChildren() { return this.children.length > 1; }
169
97
 
170
- // Clear references
98
+ destroy() {
99
+ this.children.forEach(child => child.destroy());
100
+ this.children = [];
101
+ this.adjustHandler?.destroy();
102
+ this.adjustHandler = null;
171
103
  this.parentFrame = null;
172
104
  this.splitDock = null;
173
105
  }
174
106
 
175
107
  updateStyles() {
176
- // Update split direction classes
177
- if (this.splitDirection) {
178
- this.element.classList.remove('horizontal', 'vertical');
179
- this.element.classList.add(this.splitDirection);
108
+ this.element.classList.remove('horizontal', 'vertical');
109
+ if (this.splitDirection) this.element.classList.add(this.splitDirection);
110
+ this.adjustHandler.setupResizeHandles();
111
+ }
112
+
113
+ splitWithPanel(dock, direction, panel, fromDock) {
114
+ const currentIndex = this.children.indexOf(dock);
115
+ if (currentIndex === -1) return;
116
+
117
+ const newDock = new Dock(null, this);
118
+ this.applySplit(dock, direction, newDock, currentIndex);
119
+ fromDock.removePanel(panel, true);
120
+ newDock.addPanel(panel);
121
+ }
122
+
123
+ applySplit(dock, direction, newDock, currentIndex) {
124
+ const needsVerticalSplit = direction === 'top' || direction === 'bottom';
125
+ const needsHorizontalSplit = direction === 'left' || direction === 'right';
126
+
127
+ if ((needsVerticalSplit && this.splitDirection === 'vertical') ||
128
+ (needsHorizontalSplit && this.splitDirection === 'horizontal')) {
129
+ this.splitIntoExisting(dock, direction, newDock, currentIndex);
130
+ } else if (this.splitDirection === null && this.children.length === 1) {
131
+ this.initializeSplit(direction, newDock, currentIndex);
180
132
  } else {
181
- this.element.classList.remove('horizontal', 'vertical');
133
+ this.createNestedSplit(dock, direction, newDock);
182
134
  }
135
+ }
183
136
 
184
- // Update resize handles
185
- this.adjustHandler.setupResizeHandles();
137
+ splitIntoExisting(dock, direction, newDock, currentIndex) {
138
+ const insertBefore = direction === 'top' || direction === 'left';
139
+ const insertIdx = insertBefore ? currentIndex : currentIndex + 1;
140
+
141
+ this.children.splice(insertIdx, 0, newDock);
142
+ const refNode = insertBefore ? dock.element : dock.element.nextSibling;
143
+ this.element.insertBefore(newDock.element, refNode);
144
+
145
+ dock.element.style.flex = CONFIG.layout.defaultFlexBasis;
146
+ newDock.element.style.flex = CONFIG.layout.defaultFlexBasis;
147
+ this.updateStyles();
186
148
  }
187
149
 
150
+ initializeSplit(direction, newDock, currentIndex) {
151
+ this.splitDirection = (direction === 'top' || direction === 'bottom') ? 'vertical' : 'horizontal';
152
+ this.splitIntoExisting(this.children[0], direction, newDock, currentIndex);
153
+ }
154
+
155
+ createNestedSplit(dock, direction, newDock) {
156
+ const currentIndex = this.children.indexOf(dock);
157
+ const splitDirection = (direction === 'top' || direction === 'bottom') ? 'vertical' : 'horizontal';
158
+
159
+ const wrapper = new Frame(null, this.splitDock);
160
+ wrapper.splitDirection = splitDirection;
161
+ wrapper.parentFrame = this;
188
162
 
163
+ this.element.insertBefore(wrapper.element, dock.element);
164
+ dock.element.remove();
165
+ this.children[currentIndex] = wrapper;
166
+ dock.parentFrame = wrapper;
167
+ newDock.parentFrame = wrapper;
168
+
169
+ const insertBefore = direction === 'top' || direction === 'left';
170
+ wrapper.children = insertBefore ? [newDock, dock] : [dock, newDock];
171
+ wrapper.children.forEach(child => wrapper.element.appendChild(child.element));
172
+
173
+ dock.element.style.flex = CONFIG.layout.defaultFlexBasis;
174
+ newDock.element.style.flex = CONFIG.layout.defaultFlexBasis;
175
+ wrapper.updateStyles();
176
+ }
189
177
  }