Skip to content

Commit 1fad668

Browse files
committed
#8155 dashboard.Container: documentation, apps/colors: using the new implementation
1 parent cf8d18d commit 1fad668

File tree

3 files changed

+28
-217
lines changed

3 files changed

+28
-217
lines changed

apps/colors/view/Viewport.mjs

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import BaseViewport from '../../../src/container/Viewport.mjs';
22
import BarChartComponent from './BarChartComponent.mjs';
33
import Dashboard from '../../../src/dashboard/Container.mjs';
4+
import DashboardPanel from '../../../src/dashboard/Panel.mjs';
45
import HeaderToolbar from './HeaderToolbar.mjs';
56
import PieChartComponent from './PieChartComponent.mjs';
67
import GridContainer from './GridContainer.mjs';
7-
import Panel from '../../../src/container/Panel.mjs';
88
import ViewportController from './ViewportController.mjs';
99
import ViewportStateProvider from './ViewportStateProvider.mjs';
1010

@@ -42,15 +42,11 @@ class Viewport extends BaseViewport {
4242
}, {
4343
module : Dashboard,
4444
layout : {ntype: 'vbox', align: 'stretch'},
45+
popupUrl : 'apps/colors/childapps/widget/index.html',
4546
reference: 'dashboard',
4647

47-
listeners: {
48-
dragBoundaryEntry: 'onDragBoundaryEntry',
49-
dragBoundaryExit : 'onDragBoundaryExit'
50-
},
51-
5248
items: [{
53-
module : Panel,
49+
module : DashboardPanel,
5450
flex : 1,
5551
reference: 'grid-panel',
5652
headers : [{
@@ -62,7 +58,7 @@ class Viewport extends BaseViewport {
6258
{module: GridContainer, reference: 'grid'}
6359
]
6460
}, {
65-
module : Panel,
61+
module : DashboardPanel,
6662
flex : 1.3,
6763
reference: 'pie-chart-panel',
6864
headers : [{
@@ -74,7 +70,7 @@ class Viewport extends BaseViewport {
7470
{module: PieChartComponent, reference: 'pie-chart'}
7571
]
7672
}, {
77-
module : Panel,
73+
module : DashboardPanel,
7874
flex : 1.3,
7975
reference: 'bar-chart-panel',
8076
headers : [{

apps/colors/view/ViewportController.mjs

Lines changed: 4 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -19,37 +19,11 @@ class ViewportController extends Controller {
1919
className: 'Colors.view.ViewportController'
2020
}
2121

22-
/**
23-
* @summary Tracks the names of widgets currently open in separate windows.
24-
* @member {String[]} connectedApps=[]
25-
*/
26-
connectedApps = []
2722
/**
2823
* @summary The ID for the `setInterval` used for real-time data updates.
2924
* @member {Number|null} intervalId
3025
*/
3126
intervalId = null
32-
/**
33-
* @member {Boolean} #isReintegrating=false
34-
* @private
35-
*/
36-
#isReintegrating = false
37-
/**
38-
* @summary A private flag to track if a drag operation is in the process of moving a widget to a new window.
39-
* @member {Boolean} #isWindowDragging=false
40-
* @private
41-
*/
42-
#isWindowDragging = false
43-
/**
44-
* @summary A map to get the original index of a widget in the dashboard's items array.
45-
* @description This is used to correctly re-insert a widget when its popup window is closed.
46-
* @member {Object} widgetIndexMap
47-
*/
48-
widgetIndexMap = {
49-
'bar-chart': 2,
50-
'pie-chart': 1,
51-
grid : 0
52-
}
5327

5428
/**
5529
* @summary Factory method to open a widget in a new browser window or popup.
@@ -59,10 +33,11 @@ class ViewportController extends Controller {
5933
*/
6034
async createBrowserWindow(name) {
6135
if (this.getStateProvider().getData('openWidgetsAsPopups')) {
62-
let widget = this.getReference(name),
63-
rect = await this.component.getDomRect(widget.vdom.id); // using the vdom id to always get the top-level node
36+
let dashboard = this.getReference('dashboard'),
37+
widget = this.getReference(name),
38+
rect = await this.component.getDomRect(widget.vdom.id); // using the vdom id to always get the top-level node
6439

65-
await this.#openWidgetInPopup(name, rect)
40+
await dashboard.openWidgetInPopup(widget, rect)
6641
} else {
6742
let {config, windowConfigs} = Neo,
6843
{environment} = config,
@@ -89,75 +64,6 @@ class ViewportController extends Controller {
8964
super.destroy(...args)
9065
}
9166

92-
/**
93-
* @summary Handles the `connect` event fired by `Neo.currentWorker`.
94-
* @description This is triggered when a new child application (a detached widget window) connects to the shared worker.
95-
* It re-parents the widget component from the main app's component tree into the new child app's viewport.
96-
* @param {Object} data The event data from the worker.
97-
* @param {String} data.appName The name of the connecting application.
98-
* @param {Number} data.windowId The ID of the new window.
99-
*/
100-
async onAppConnect(data) {
101-
if (data.appName === 'ColorsWidget') {
102-
let me = this,
103-
app = Neo.apps[data.windowId],
104-
mainView = app.mainView,
105-
{windowId} = data,
106-
url = await Neo.Main.getByPath({path: 'document.URL', windowId}),
107-
widgetName = new URL(url).searchParams.get('name'),
108-
widget = me.getReference(widgetName),
109-
parent = widget.up('panel');
110-
111-
if (!me.#isWindowDragging) {
112-
parent.hide()
113-
}
114-
115-
me.connectedApps.push(widgetName);
116-
117-
me.getReference(`detach-${widgetName}-button`).disabled = true;
118-
119-
mainView.add(widget, false, !me.#isWindowDragging)
120-
}
121-
}
122-
123-
/**
124-
* @summary Handles the `disconnect` event fired by `Neo.currentWorker`.
125-
* @description This is triggered when a child application window is closed. It moves the widget component
126-
* back into its original position in the main application's dashboard.
127-
* @param {Object} data The event data from the worker.
128-
* @param {String} data.appName The name of the disconnecting application.
129-
* @param {Number} data.windowId The ID of the closed window.
130-
*/
131-
async onAppDisconnect(data) {
132-
let me = this;
133-
134-
if (me.#isWindowDragging || me.#isReintegrating) {
135-
me.#isWindowDragging = false;
136-
return
137-
}
138-
139-
let {appName, windowId} = data,
140-
dashboard = me.getReference('dashboard'),
141-
url = await Neo.Main.getByPath({path: 'document.URL', windowId}),
142-
widgetName = new URL(url).searchParams.get('name'),
143-
widget = me.getReference(widgetName);
144-
145-
// Closing a non-main app needs to move the widget back into its original position & re-enable the show button
146-
if (appName === 'ColorsWidget') {
147-
let itemPanel = dashboard.items[me.widgetIndexMap[widgetName]],
148-
bodyContainer = itemPanel.getReference('bodyContainer');
149-
150-
bodyContainer.add(widget);
151-
itemPanel.show();
152-
153-
me.getReference(`detach-${widgetName}-button`).disabled = false
154-
}
155-
// Close popup windows when closing or reloading the main window
156-
else if (appName === 'Colors') {
157-
Neo.Main.windowClose({names: me.connectedApps, windowId})
158-
}
159-
}
160-
16167
/**
16268
* @summary Handles the change event from the 'Amount of Colors' slider.
16369
* @param {Object} data The event data.
@@ -190,22 +96,6 @@ class ViewportController extends Controller {
19096
this.setState('openWidgetsAsPopups', data.value)
19197
}
19298

193-
/**
194-
* @summary Lifecycle method, called after the controller's constructor.
195-
* @description Sets up listeners for the shared worker's connect and disconnect events.
196-
*/
197-
onConstructed() {
198-
super.onConstructed();
199-
200-
let me = this;
201-
202-
Neo.currentWorker.on({
203-
connect : me.onAppConnect,
204-
disconnect: me.onAppDisconnect,
205-
scope : me
206-
})
207-
}
208-
20999
/**
210100
* @summary Lifecycle method, called after the controller's component is constructed.
211101
* @description Triggers the initial data load for the widgets.
@@ -239,60 +129,6 @@ class ViewportController extends Controller {
239129
await this.createBrowserWindow('pie-chart')
240130
}
241131

242-
/**
243-
* @summary Handles the `dragBoundaryEntry` event from the `SortZone`.
244-
* @description This is the core of the drag-to-re-dock feature. When a dragged window re-enters the main
245-
* application's boundary, this method closes the popup window and re-inserts the widget component
246-
* back into its original dashboard container, providing a seamless user experience.
247-
* @param {Object} data The event data from the SortZone.
248-
*/
249-
async onDragBoundaryEntry(data) {
250-
let me = this,
251-
{windowId} = me,
252-
{sortZone} = data,
253-
widgetName = data.draggedItem.reference.replace('-panel', ''),
254-
widget = me.getReference(widgetName);
255-
256-
me.#isReintegrating = true;
257-
258-
sortZone.dragProxy.add(widget, true);
259-
260-
await Neo.Main.windowClose({names: widgetName, windowId});
261-
262-
me.#isReintegrating = false;
263-
me.#isWindowDragging = false;
264-
265-
sortZone.isWindowDragging = false;
266-
sortZone.dragProxy.style = {opacity: 1};
267-
268-
Neo.main.addon.DragDrop.setConfigs({isWindowDragging: false, windowId})
269-
}
270-
271-
/**
272-
* @summary Handles the `dragBoundaryExit` event from the `SortZone`.
273-
* @description This is the core of the drag-to-popup feature. When a dragged component's proxy leaves
274-
* the boundary of its container, this method orchestrates the creation of a new popup window
275-
* and hands off the drag operation to the main thread's DragDrop addon to drag the OS-level window.
276-
* @param {Object} data The event data from the SortZone.
277-
*/
278-
async onDragBoundaryExit(data) {
279-
let {draggedItem, proxyRect, sortZone} = data,
280-
widgetName = draggedItem.reference.replace('-panel', ''),
281-
popupData;
282-
283-
this.#isWindowDragging = true;
284-
285-
// Prohibit the size reduction inside #openWidgetInPopup().
286-
proxyRect.height += 50;
287-
288-
popupData = await this.#openWidgetInPopup(widgetName, proxyRect);
289-
290-
sortZone.startWindowDrag({
291-
dragData: data,
292-
...popupData
293-
});
294-
}
295-
296132
/**
297133
* @summary Handles the click event to request Window Management permissions.
298134
* @description The Window Management API is a new browser feature that allows web apps to control
@@ -346,46 +182,6 @@ class ViewportController extends Controller {
346182
}
347183
}
348184

349-
/**
350-
* @summary Private helper method to open a widget in a new popup window.
351-
* @description This method calculates the precise screen coordinates for the new window based on the main
352-
* window's position and the drag proxy's last known rectangle. It then calls the main thread
353-
* to open the new window with the correct URL and features.
354-
* @param {String} name The reference name of the widget.
355-
* @param {Object} rect The DOM rect of the drag proxy.
356-
* @returns {Promise<Object>} A promise that resolves with the new window's properties.
357-
* @private
358-
*/
359-
async #openWidgetInPopup(name, rect) {
360-
let me = this,
361-
{windowId} = me,
362-
{config, windowConfigs} = Neo,
363-
{environment} = config,
364-
firstWindowId = Object.keys(windowConfigs)[0],
365-
{basePath} = windowConfigs[firstWindowId],
366-
url;
367-
368-
if (environment !== 'development') {
369-
basePath = `${basePath + environment}/`
370-
}
371-
372-
url = `${basePath}apps/colors/childapps/widget/index.html?name=${name}`;
373-
374-
let winData = await Neo.Main.getWindowData({windowId}),
375-
{height, width, x, y} = rect,
376-
popupHeight = height - 50, // popup header in Chrome
377-
popupLeft = x + winData.screenLeft,
378-
popupTop = y + (winData.outerHeight - winData.innerHeight + winData.screenTop);
379-
380-
await Neo.Main.windowOpen({
381-
url,
382-
windowFeatures: `height=${popupHeight},left=${popupLeft},top=${popupTop},width=${width}`,
383-
windowName : name
384-
});
385-
386-
return {popupHeight, popupLeft, popupTop, popupWidth: width, windowName: name}
387-
}
388-
389185
/**
390186
* @summary Updates the chart components with new data.
391187
* @param {Object} data The data for the charts.

src/dashboard/Container.mjs

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,27 @@ import BaseContainer from '../container/Base.mjs';
22
import DragProxyContainer from '../draggable/DragProxyContainer.mjs';
33

44
/**
5+
* @summary A container that manages a dynamic layout of sortable items, with built-in support for detaching items into separate browser windows.
6+
*
7+
* This class extends `Neo.container.Base` to provide a drag-and-drop dashboard experience. Its most powerful feature is the
8+
* **"Detach to Window"** capability. When a user drags a dashboard item outside the container's boundary, this class automatically:
9+
* 1. Opens a new browser popup window (app shell) based on the item's or container's `popupUrl`.
10+
* 2. Moves the item's component instance into the new window's component tree.
11+
* 3. Maintains a link between the detached item and its original dashboard slot.
12+
*
13+
* **Re-integration:**
14+
* If the user drags the detached window back over the original dashboard, this class detects the re-entry, closes the popup,
15+
* and seamlessly re-inserts the item into its previous position (or a new sort index).
16+
*
17+
* **Architecture:**
18+
* This class leverages the `Neo.worker.App`'s shared nature. It listens for global `connect` and `disconnect` events to track
19+
* the lifecycle of detached windows. It uses a robust `windowId` mapping to ensure that even if a window is closed manually by the user,
20+
* the widget is correctly reclaimed and restored to the dashboard, preventing data loss or "zombie" widgets.
21+
*
522
* @class Neo.dashboard.Container
623
* @extends Neo.container.Base
24+
* @see Neo.dashboard.Panel
25+
* @see Neo.draggable.container.SortZone
726
*/
827
class Container extends BaseContainer {
928
static config = {

0 commit comments

Comments
 (0)