@wxn0brp/nya-dock 0.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/LICENSE +21 -0
- package/README.md +103 -0
- package/dist/const.d.ts +2 -0
- package/dist/const.js +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +103 -0
- package/dist/logger.d.ts +17 -0
- package/dist/logger.js +29 -0
- package/dist/mouse/index.d.ts +2 -0
- package/dist/mouse/index.js +2 -0
- package/dist/mouse/movePanel.d.ts +1 -0
- package/dist/mouse/movePanel.js +74 -0
- package/dist/mouse/resize.d.ts +1 -0
- package/dist/mouse/resize.js +53 -0
- package/dist/render.d.ts +2 -0
- package/dist/render.js +31 -0
- package/dist/state.d.ts +19 -0
- package/dist/state.js +45 -0
- package/dist/storage.d.ts +5 -0
- package/dist/storage.js +32 -0
- package/dist/style.css +142 -0
- package/dist/style.css.map +1 -0
- package/dist/types.d.ts +8 -0
- package/dist/types.js +1 -0
- package/dist/utils/convertSplit.d.ts +2 -0
- package/dist/utils/convertSplit.js +54 -0
- package/dist/utils/detect.d.ts +6 -0
- package/dist/utils/detect.js +21 -0
- package/dist/utils/movePanel.d.ts +3 -0
- package/dist/utils/movePanel.js +57 -0
- package/package.json +41 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 wxn0brP
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# Nyadock
|
|
2
|
+
|
|
3
|
+
A dock panel component library. Currently in early development stage.
|
|
4
|
+
|
|
5
|
+
> [!WARNING]
|
|
6
|
+
> This project is currently in early development and is not yet ready for production use.
|
|
7
|
+
> Expect breaking changes and incomplete features.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
- Dock interface component
|
|
11
|
+
- Built with TypeScript for type safety
|
|
12
|
+
- Styled with SCSS
|
|
13
|
+
- Part of the @wxn0brp ecosystem
|
|
14
|
+
|
|
15
|
+
## Demo
|
|
16
|
+
|
|
17
|
+
[https://wxn0brp.github.io/Nyadock/demo](https://wxn0brp.github.io/Nyadock/demo)
|
|
18
|
+
|
|
19
|
+
## Installation
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
yarn add github:wxn0brP/Nyadock#dist
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
Nyadock is controlled via a single `controller` instance that manages the entire layout.
|
|
28
|
+
|
|
29
|
+
### 1. HTML Setup
|
|
30
|
+
|
|
31
|
+
First, you need a container element in your HTML to host the dock layout, and individual `div` elements that will become your panels.
|
|
32
|
+
|
|
33
|
+
```html
|
|
34
|
+
<!-- The main container for the dock layout -->
|
|
35
|
+
<div id="app"></div>
|
|
36
|
+
|
|
37
|
+
<!-- Your panel content (can be hidden initially) -->
|
|
38
|
+
<div id="panel1-content" style="display: none;">
|
|
39
|
+
<h2>Panel 1</h2>
|
|
40
|
+
<p>Content for the first panel.</p>
|
|
41
|
+
</div>
|
|
42
|
+
<div id="panel2-content" style="display: none;">
|
|
43
|
+
<h2>Panel 2</h2>
|
|
44
|
+
<p>Content for the second panel.</p>
|
|
45
|
+
</div>
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 2. JavaScript/TypeScript Initialization
|
|
49
|
+
|
|
50
|
+
Import the `controller` and use it to configure and initialize the layout. Note that you also need to import the stylesheet.
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { controller } from "@wxn0brp/nya-dock/state";
|
|
54
|
+
import "@wxn0brp/nya-dock/style.css";
|
|
55
|
+
|
|
56
|
+
// 1. Get the master container element
|
|
57
|
+
const appContainer = document.querySelector("#app");
|
|
58
|
+
|
|
59
|
+
// 2. Get your panel content elements
|
|
60
|
+
const panel1 = document.querySelector("#panel1-content");
|
|
61
|
+
const panel2 = document.querySelector("#panel2-content");
|
|
62
|
+
|
|
63
|
+
// 3. Assign the master container
|
|
64
|
+
controller.master = appContainer;
|
|
65
|
+
|
|
66
|
+
// 4. Register your panels with unique IDs
|
|
67
|
+
controller.registerPanel("panel1", panel1);
|
|
68
|
+
controller.registerPanel("panel2", panel2);
|
|
69
|
+
|
|
70
|
+
// 5. Define the layout structure
|
|
71
|
+
// A horizontal split between "panel1" and "panel2"
|
|
72
|
+
const layout = ["panel1", "panel2"];
|
|
73
|
+
|
|
74
|
+
// A vertical split would be: ["panel1", "panel2", 1]
|
|
75
|
+
// Layouts can be nested.
|
|
76
|
+
controller.setDefaultState(layout);
|
|
77
|
+
|
|
78
|
+
// 6. Initialize the dock layout
|
|
79
|
+
controller.init();
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
### How the Layout Structure Works
|
|
83
|
+
|
|
84
|
+
The layout is defined by a `StructNode`, which is an array of panel IDs and nested arrays.
|
|
85
|
+
|
|
86
|
+
- **Horizontal Split (Row):** `["panelA", "panelB"]`
|
|
87
|
+
- **Vertical Split (Column):** `["panelA", "panelB", 1]` (the `1` indicates a vertical split)
|
|
88
|
+
|
|
89
|
+
You can nest these structures to create complex layouts:
|
|
90
|
+
|
|
91
|
+
```javascript
|
|
92
|
+
// panelA is on the left.
|
|
93
|
+
// The right side is a vertical split between panelB and panelC.
|
|
94
|
+
const complexLayout = ["panelA", ["panelB", "panelC", 1]];
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## License
|
|
98
|
+
|
|
99
|
+
MIT [LICENSE](./LICENSE)
|
|
100
|
+
|
|
101
|
+
## Contributing
|
|
102
|
+
|
|
103
|
+
Contributions are welcome!
|
package/dist/const.d.ts
ADDED
package/dist/const.js
ADDED
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import "@wxn0brp/flanker-ui/html";
|
|
2
|
+
import logger from "./logger.js";
|
|
3
|
+
import "./mouse/index.js";
|
|
4
|
+
import { controller } from "./state.js";
|
|
5
|
+
logger.setLogLevel("DEBUG");
|
|
6
|
+
window.logger = logger;
|
|
7
|
+
window.controller = controller;
|
|
8
|
+
const app = qs("#app");
|
|
9
|
+
const panel1 = document.createElement("div");
|
|
10
|
+
panel1.innerHTML = `
|
|
11
|
+
<div class="panel-header">Panel 1</div>
|
|
12
|
+
<div class="panel-content">
|
|
13
|
+
<h3>Welcome to Panel 1</h3>
|
|
14
|
+
<p>This is the first panel in the layout.</p>
|
|
15
|
+
<ul>
|
|
16
|
+
<li>Item 1</li>
|
|
17
|
+
<li>Item 2</li>
|
|
18
|
+
<li>Item 3</li>
|
|
19
|
+
</ul>
|
|
20
|
+
<div class="button-group">
|
|
21
|
+
<button onclick="controller.reset()">Reset Config</button>
|
|
22
|
+
</div>
|
|
23
|
+
</div>
|
|
24
|
+
`;
|
|
25
|
+
const panel2 = document.createElement("div");
|
|
26
|
+
panel2.innerHTML = `
|
|
27
|
+
<div class="panel-header">Panel 2</div>
|
|
28
|
+
<div class="panel-content">
|
|
29
|
+
<h3>Content for Panel 2</h3>
|
|
30
|
+
<p>This is the second panel, split from the main container.</p>
|
|
31
|
+
<div class="button-group">
|
|
32
|
+
<button>Button 1</button>
|
|
33
|
+
<button>Button 2</button>
|
|
34
|
+
</div>
|
|
35
|
+
</div>
|
|
36
|
+
`;
|
|
37
|
+
const panel3 = document.createElement("div");
|
|
38
|
+
panel3.innerHTML = `
|
|
39
|
+
<div class="panel-header">Panel 3</div>
|
|
40
|
+
<div class="panel-content">
|
|
41
|
+
<h3>Additional Panel</h3>
|
|
42
|
+
<p>This is the third panel in the layout.</p>
|
|
43
|
+
<div class="info-box">
|
|
44
|
+
<p>This panel was created by splitting with Panel 2.</p>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
`;
|
|
48
|
+
const panel4 = document.createElement("div");
|
|
49
|
+
panel4.innerHTML = `
|
|
50
|
+
<div class="panel-header">Panel 4</div>
|
|
51
|
+
<div class="panel-content">
|
|
52
|
+
<h3>Form Panel</h3>
|
|
53
|
+
<form>
|
|
54
|
+
<label for="name">Name:</label>
|
|
55
|
+
<input type="text" id="name" name="name"><br><br>
|
|
56
|
+
<label for="email">Email:</label>
|
|
57
|
+
<input type="email" id="email" name="email"><br><br>
|
|
58
|
+
<input type="submit" value="Submit">
|
|
59
|
+
</form>
|
|
60
|
+
</div>
|
|
61
|
+
`;
|
|
62
|
+
const panel5 = document.createElement("div");
|
|
63
|
+
panel5.innerHTML = `
|
|
64
|
+
<div class="panel-header">Panel 5</div>
|
|
65
|
+
<div class="panel-content">
|
|
66
|
+
<h3>Table Panel</h3>
|
|
67
|
+
<table border="1">
|
|
68
|
+
<tr>
|
|
69
|
+
<th>First name</th>
|
|
70
|
+
<th>Last name</th>
|
|
71
|
+
<th>Age</th>
|
|
72
|
+
</tr>
|
|
73
|
+
<tr>
|
|
74
|
+
<td>Jill</td>
|
|
75
|
+
<td>Smith</td>
|
|
76
|
+
<td>50</td>
|
|
77
|
+
</tr>
|
|
78
|
+
<tr>
|
|
79
|
+
<td>Eve</td>
|
|
80
|
+
<td>Jackson</td>
|
|
81
|
+
<td>94</td>
|
|
82
|
+
</tr>
|
|
83
|
+
</table>
|
|
84
|
+
</div>
|
|
85
|
+
`;
|
|
86
|
+
controller.master = app;
|
|
87
|
+
controller.setDefaultState([
|
|
88
|
+
"panel1",
|
|
89
|
+
[
|
|
90
|
+
"panel2",
|
|
91
|
+
[
|
|
92
|
+
["panel3", "panel5", 1],
|
|
93
|
+
"panel4",
|
|
94
|
+
],
|
|
95
|
+
1
|
|
96
|
+
]
|
|
97
|
+
]);
|
|
98
|
+
controller.registerPanel("panel1", panel1);
|
|
99
|
+
controller.registerPanel("panel2", panel2);
|
|
100
|
+
controller.registerPanel("panel3", panel3);
|
|
101
|
+
controller.registerPanel("panel4", panel4);
|
|
102
|
+
controller.registerPanel("panel5", panel5);
|
|
103
|
+
controller.init();
|
package/dist/logger.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
declare const LOG_LEVELS: {
|
|
2
|
+
DEBUG: number;
|
|
3
|
+
INFO: number;
|
|
4
|
+
WARN: number;
|
|
5
|
+
ERROR: number;
|
|
6
|
+
};
|
|
7
|
+
declare function setLogLevel(level: keyof typeof LOG_LEVELS): void;
|
|
8
|
+
declare function exportLogs(): void;
|
|
9
|
+
declare const logger: {
|
|
10
|
+
debug: (...args: any[]) => void;
|
|
11
|
+
info: (...args: any[]) => void;
|
|
12
|
+
warn: (...args: any[]) => void;
|
|
13
|
+
error: (...args: any[]) => void;
|
|
14
|
+
setLogLevel: typeof setLogLevel;
|
|
15
|
+
exportLogs: typeof exportLogs;
|
|
16
|
+
};
|
|
17
|
+
export default logger;
|
package/dist/logger.js
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const LOG_LEVELS = {
|
|
2
|
+
DEBUG: 0,
|
|
3
|
+
INFO: 1,
|
|
4
|
+
WARN: 2,
|
|
5
|
+
ERROR: 3,
|
|
6
|
+
};
|
|
7
|
+
let logs = [];
|
|
8
|
+
let currentLogLevel = LOG_LEVELS.ERROR;
|
|
9
|
+
function setLogLevel(level) {
|
|
10
|
+
currentLogLevel = LOG_LEVELS[level];
|
|
11
|
+
}
|
|
12
|
+
function exportLogs() {
|
|
13
|
+
console.log(logs.join("\n"));
|
|
14
|
+
}
|
|
15
|
+
function log(level, ...args) {
|
|
16
|
+
if (LOG_LEVELS[level] >= currentLogLevel) {
|
|
17
|
+
console.log(`[${level}]`, ...args);
|
|
18
|
+
logs.push(`[${level}] ${args.map(a => typeof a === "string" ? a : JSON.stringify(a)).join(" ")}`);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const logger = {
|
|
22
|
+
debug: (...args) => log("DEBUG", ...args),
|
|
23
|
+
info: (...args) => log("INFO", ...args),
|
|
24
|
+
warn: (...args) => log("WARN", ...args),
|
|
25
|
+
error: (...args) => log("ERROR", ...args),
|
|
26
|
+
setLogLevel,
|
|
27
|
+
exportLogs,
|
|
28
|
+
};
|
|
29
|
+
export default logger;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { DRAG } from "../const.js";
|
|
2
|
+
import { controller } from "../state.js";
|
|
3
|
+
import logger from "../logger.js";
|
|
4
|
+
import { detectDockZone, getRelativePosition } from "../utils/detect.js";
|
|
5
|
+
import { saveNyaState } from "../storage.js";
|
|
6
|
+
let draggingPanel = null;
|
|
7
|
+
document.addEventListener("mousedown", (e) => {
|
|
8
|
+
const target = e.target;
|
|
9
|
+
if (!target.classList.contains("panel")) {
|
|
10
|
+
logger.debug("Mousedown event ignored: target is not a panel");
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
const data = getRelativePosition(e, target);
|
|
14
|
+
if (data.x > DRAG || data.y > DRAG) {
|
|
15
|
+
logger.debug("Mousedown event ignored: not in drag area");
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
draggingPanel = target;
|
|
19
|
+
logger.info("Dragging panel started", draggingPanel);
|
|
20
|
+
document.body.style.cursor = "move";
|
|
21
|
+
});
|
|
22
|
+
document.addEventListener("mouseup", (e) => {
|
|
23
|
+
if (!draggingPanel) {
|
|
24
|
+
logger.debug("Mouseup event ignored: no panel is being dragged");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
document.body.style.cursor = "";
|
|
28
|
+
function end() {
|
|
29
|
+
logger.info("Dragging finished");
|
|
30
|
+
draggingPanel = null;
|
|
31
|
+
}
|
|
32
|
+
const allPanels = [...controller._panels.values()].filter(p => p !== draggingPanel);
|
|
33
|
+
logger.debug("All panels", allPanels);
|
|
34
|
+
const elemUnder = document.elementFromPoint(e.clientX, e.clientY);
|
|
35
|
+
if (!elemUnder) {
|
|
36
|
+
logger.debug("Mouseup event ignored: no element under cursor");
|
|
37
|
+
return end();
|
|
38
|
+
}
|
|
39
|
+
const targetPanel = elemUnder.closest(".panel");
|
|
40
|
+
if (!targetPanel) {
|
|
41
|
+
logger.debug("Mouseup event ignored: no target panel under cursor");
|
|
42
|
+
return end();
|
|
43
|
+
}
|
|
44
|
+
logger.debug("Target panel", targetPanel);
|
|
45
|
+
if (!allPanels.includes(targetPanel)) {
|
|
46
|
+
logger.debug("Mouseup event ignored: target panel is not a valid drop target");
|
|
47
|
+
return end();
|
|
48
|
+
}
|
|
49
|
+
const zone = detectDockZone(e, targetPanel);
|
|
50
|
+
logger.info(`Docking to ${zone}`);
|
|
51
|
+
if (zone === "center") {
|
|
52
|
+
logger.debug("Docking to center, no action taken");
|
|
53
|
+
return end();
|
|
54
|
+
}
|
|
55
|
+
let sourceId = draggingPanel.dataset.nya_id;
|
|
56
|
+
const targetId = targetPanel.dataset.nya_id;
|
|
57
|
+
if (!sourceId)
|
|
58
|
+
sourceId = draggingPanel.qs(".panel[data-nya_id]")?.dataset.nya_id;
|
|
59
|
+
if (!sourceId || !targetId) {
|
|
60
|
+
logger.error("Panel ID not found");
|
|
61
|
+
logger.error("Source ID:", sourceId);
|
|
62
|
+
logger.error("Target ID:", targetId);
|
|
63
|
+
return end();
|
|
64
|
+
}
|
|
65
|
+
if (sourceId === targetId) {
|
|
66
|
+
logger.debug("Mouseup event ignored: source and target are the same");
|
|
67
|
+
return end();
|
|
68
|
+
}
|
|
69
|
+
controller.movePanel(sourceId, targetId, zone);
|
|
70
|
+
controller._render();
|
|
71
|
+
controller._setDefaultSize();
|
|
72
|
+
saveNyaState();
|
|
73
|
+
end();
|
|
74
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { clamp } from "@wxn0brp/flanker-ui/utils";
|
|
2
|
+
import { DRAG, RESIZE_MIN } from "../const.js";
|
|
3
|
+
import logger from "../logger.js";
|
|
4
|
+
import { controller } from "../state.js";
|
|
5
|
+
import { getRelativePosition } from "../utils/detect.js";
|
|
6
|
+
import { saveNyaState } from "../storage.js";
|
|
7
|
+
let draggingPanel = null;
|
|
8
|
+
let leftPanel = null;
|
|
9
|
+
let panelType = "width";
|
|
10
|
+
document.addEventListener("mousedown", (e) => {
|
|
11
|
+
const target = e.target;
|
|
12
|
+
if (!target.classList.contains("panel"))
|
|
13
|
+
return;
|
|
14
|
+
const _panelType = target.parentElement.classList.contains("column") ? "height" : "width";
|
|
15
|
+
const _leftPanel = target.parentElement.children[0];
|
|
16
|
+
const data = getRelativePosition(e, _leftPanel);
|
|
17
|
+
if (_panelType === "width") {
|
|
18
|
+
const delta = _leftPanel.offsetWidth - data.x;
|
|
19
|
+
if (delta > DRAG || delta < 0)
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const delta = _leftPanel.offsetHeight - data.y;
|
|
24
|
+
if (delta > DRAG || delta < 0)
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
draggingPanel = target;
|
|
28
|
+
leftPanel = _leftPanel;
|
|
29
|
+
panelType = _panelType;
|
|
30
|
+
logger.debug(`Resizing panel ${panelType}`);
|
|
31
|
+
document.body.style.cursor = panelType === "width" ? "col-resize" : "row-resize";
|
|
32
|
+
});
|
|
33
|
+
document.addEventListener("mousemove", (e) => {
|
|
34
|
+
if (!draggingPanel)
|
|
35
|
+
return;
|
|
36
|
+
const data = getRelativePosition(e, leftPanel);
|
|
37
|
+
let value = 0;
|
|
38
|
+
if (panelType === "width")
|
|
39
|
+
value = clamp(RESIZE_MIN, data.x, draggingPanel.parentElement.offsetWidth - RESIZE_MIN);
|
|
40
|
+
else
|
|
41
|
+
value = clamp(RESIZE_MIN, data.y, draggingPanel.parentElement.offsetHeight - RESIZE_MIN);
|
|
42
|
+
const id = draggingPanel.parentElement.dataset.nya_split;
|
|
43
|
+
const split = controller._splits.get(id);
|
|
44
|
+
split.style.setProperty("--size", `${value}px`);
|
|
45
|
+
logger.debug(`Resizing to ${value}`);
|
|
46
|
+
});
|
|
47
|
+
document.addEventListener("mouseup", (e) => {
|
|
48
|
+
if (draggingPanel)
|
|
49
|
+
logger.debug("Resizing finished");
|
|
50
|
+
draggingPanel = null;
|
|
51
|
+
document.body.style.cursor = "";
|
|
52
|
+
saveNyaState();
|
|
53
|
+
});
|
package/dist/render.d.ts
ADDED
package/dist/render.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export function render(controller) {
|
|
2
|
+
const tempElement = document.createElement("div");
|
|
3
|
+
for (const panel of controller._panels.values()) {
|
|
4
|
+
tempElement.appendChild(panel);
|
|
5
|
+
}
|
|
6
|
+
controller.master.innerHTML = "";
|
|
7
|
+
const masterSplit = renderSplit(controller, controller._split);
|
|
8
|
+
controller.master.appendChild(masterSplit);
|
|
9
|
+
}
|
|
10
|
+
function renderSplit(controller, split, prefix = "0") {
|
|
11
|
+
const div = document.createElement("div");
|
|
12
|
+
div.classList.add("split");
|
|
13
|
+
div.classList.add(split.type);
|
|
14
|
+
div.dataset.nya_split = prefix;
|
|
15
|
+
controller._splits.set(prefix, div);
|
|
16
|
+
for (let i = 0; i < split.nodes.length; i++) {
|
|
17
|
+
const node = split.nodes[i];
|
|
18
|
+
if (typeof node === "string") {
|
|
19
|
+
const panel = controller._panels.get(node);
|
|
20
|
+
div.appendChild(panel);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
const splitPanel = document.createElement("div");
|
|
24
|
+
splitPanel.classList.add("panel");
|
|
25
|
+
const splitContent = renderSplit(controller, node, `${prefix}.${i}`);
|
|
26
|
+
splitPanel.appendChild(splitContent);
|
|
27
|
+
div.appendChild(splitPanel);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return div;
|
|
31
|
+
}
|
package/dist/state.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { Direction, NyaSplit, StructNode } from "./types.js";
|
|
2
|
+
export declare class NyaController {
|
|
3
|
+
_split: NyaSplit;
|
|
4
|
+
_panels: Map<string, HTMLDivElement>;
|
|
5
|
+
_splits: Map<string, HTMLDivElement>;
|
|
6
|
+
master: HTMLDivElement;
|
|
7
|
+
settings: {
|
|
8
|
+
key: string;
|
|
9
|
+
};
|
|
10
|
+
_struct: NyaSplit;
|
|
11
|
+
setDefaultState(state: NyaSplit | StructNode): void;
|
|
12
|
+
init(): void;
|
|
13
|
+
registerPanel(id: string, panel: HTMLDivElement): void;
|
|
14
|
+
_render(): void;
|
|
15
|
+
_setDefaultSize(): void;
|
|
16
|
+
movePanel(sourceId: string, targetId: string, zone: Direction): void;
|
|
17
|
+
reset(): void;
|
|
18
|
+
}
|
|
19
|
+
export declare const controller: NyaController;
|
package/dist/state.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { render } from "./render.js";
|
|
2
|
+
import { defaultState, loadState } from "./storage.js";
|
|
3
|
+
import { convertToSplits } from "./utils/convertSplit.js";
|
|
4
|
+
import { movePanel } from "./utils/movePanel.js";
|
|
5
|
+
export class NyaController {
|
|
6
|
+
_split;
|
|
7
|
+
_panels = new Map();
|
|
8
|
+
_splits = new Map();
|
|
9
|
+
master;
|
|
10
|
+
settings = {
|
|
11
|
+
key: "nya.dock",
|
|
12
|
+
};
|
|
13
|
+
_struct;
|
|
14
|
+
setDefaultState(state) {
|
|
15
|
+
this._struct = Array.isArray(state) ? convertToSplits(state) : state;
|
|
16
|
+
}
|
|
17
|
+
init() {
|
|
18
|
+
if (!this.master)
|
|
19
|
+
throw new Error("Master element not set");
|
|
20
|
+
if (!this._struct)
|
|
21
|
+
throw new Error("Structure not set");
|
|
22
|
+
loadState(this);
|
|
23
|
+
}
|
|
24
|
+
registerPanel(id, panel) {
|
|
25
|
+
panel.dataset.nya_id = id;
|
|
26
|
+
panel.classList.add("panel");
|
|
27
|
+
this._panels.set(id, panel);
|
|
28
|
+
}
|
|
29
|
+
_render() {
|
|
30
|
+
this._splits.clear();
|
|
31
|
+
return render(this);
|
|
32
|
+
}
|
|
33
|
+
_setDefaultSize() {
|
|
34
|
+
this._splits.forEach((split) => {
|
|
35
|
+
split.style.setProperty("--size", "50%");
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
movePanel(sourceId, targetId, zone) {
|
|
39
|
+
return movePanel(this, sourceId, targetId, zone);
|
|
40
|
+
}
|
|
41
|
+
reset() {
|
|
42
|
+
defaultState(this);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
export const controller = new NyaController();
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { NyaController } from "./state.js";
|
|
2
|
+
export declare function saveState(controller: NyaController): void;
|
|
3
|
+
export declare function defaultState(controller: NyaController): void;
|
|
4
|
+
export declare function loadState(controller: NyaController): void;
|
|
5
|
+
export declare const saveNyaState: Function;
|
package/dist/storage.js
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { throttle } from "@wxn0brp/flanker-ui/utils";
|
|
2
|
+
import { controller } from "./state.js";
|
|
3
|
+
import logger from "./logger.js";
|
|
4
|
+
export function saveState(controller) {
|
|
5
|
+
const state = controller._split;
|
|
6
|
+
const sizes = {};
|
|
7
|
+
controller._splits.forEach((split, key) => {
|
|
8
|
+
sizes[key] = split.style.getPropertyValue("--size");
|
|
9
|
+
});
|
|
10
|
+
localStorage.setItem(controller.settings.key, JSON.stringify({ state, sizes }));
|
|
11
|
+
}
|
|
12
|
+
export function defaultState(controller) {
|
|
13
|
+
controller._split = Object.assign({}, controller._struct);
|
|
14
|
+
controller._render();
|
|
15
|
+
controller._setDefaultSize();
|
|
16
|
+
saveState(controller);
|
|
17
|
+
}
|
|
18
|
+
export function loadState(controller) {
|
|
19
|
+
const stateString = localStorage.getItem(controller.settings.key);
|
|
20
|
+
if (!stateString)
|
|
21
|
+
return defaultState(controller);
|
|
22
|
+
const { state, sizes } = JSON.parse(stateString);
|
|
23
|
+
controller._split = state;
|
|
24
|
+
controller._render();
|
|
25
|
+
controller._splits.forEach((split, key) => {
|
|
26
|
+
split.style.setProperty("--size", sizes[key]);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
export const saveNyaState = throttle(() => {
|
|
30
|
+
logger.debug("Saving state");
|
|
31
|
+
saveState(controller);
|
|
32
|
+
}, 3000);
|
package/dist/style.css
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
* {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
box-sizing: border-box;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
:root {
|
|
8
|
+
--txt: #fff;
|
|
9
|
+
--back: #111;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
body {
|
|
13
|
+
background: var(--back);
|
|
14
|
+
color: var(--txt);
|
|
15
|
+
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
|
16
|
+
width: 100vw;
|
|
17
|
+
height: 100vh;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.master {
|
|
21
|
+
height: 100%;
|
|
22
|
+
width: 100%;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
.split {
|
|
26
|
+
display: flex;
|
|
27
|
+
height: 100%;
|
|
28
|
+
width: 100%;
|
|
29
|
+
position: relative;
|
|
30
|
+
}
|
|
31
|
+
.split .panel {
|
|
32
|
+
position: relative;
|
|
33
|
+
}
|
|
34
|
+
.split .panel::after {
|
|
35
|
+
content: "";
|
|
36
|
+
position: absolute;
|
|
37
|
+
top: 0;
|
|
38
|
+
left: 0;
|
|
39
|
+
width: 30px;
|
|
40
|
+
height: 30px;
|
|
41
|
+
cursor: move;
|
|
42
|
+
z-index: 99;
|
|
43
|
+
}
|
|
44
|
+
.split.column {
|
|
45
|
+
flex-direction: column;
|
|
46
|
+
}
|
|
47
|
+
.split.column .panel {
|
|
48
|
+
width: 100%;
|
|
49
|
+
}
|
|
50
|
+
.split.column > :first-child {
|
|
51
|
+
border-bottom: 1px solid silver;
|
|
52
|
+
height: var(--size) !important;
|
|
53
|
+
}
|
|
54
|
+
.split.column > :first-child::before {
|
|
55
|
+
content: "";
|
|
56
|
+
position: absolute;
|
|
57
|
+
left: 0;
|
|
58
|
+
right: 0;
|
|
59
|
+
bottom: -15px;
|
|
60
|
+
height: 30px;
|
|
61
|
+
z-index: 99;
|
|
62
|
+
cursor: row-resize;
|
|
63
|
+
}
|
|
64
|
+
.split.row {
|
|
65
|
+
flex-direction: row;
|
|
66
|
+
}
|
|
67
|
+
.split.row .panel {
|
|
68
|
+
height: 100%;
|
|
69
|
+
}
|
|
70
|
+
.split.row > :first-child {
|
|
71
|
+
border-right: 1px solid silver;
|
|
72
|
+
width: var(--size) !important;
|
|
73
|
+
}
|
|
74
|
+
.split.row > :first-child::before {
|
|
75
|
+
content: "";
|
|
76
|
+
position: absolute;
|
|
77
|
+
top: 0;
|
|
78
|
+
bottom: 0;
|
|
79
|
+
right: -15px;
|
|
80
|
+
width: 30px;
|
|
81
|
+
z-index: 99;
|
|
82
|
+
cursor: col-resize;
|
|
83
|
+
}
|
|
84
|
+
.split > :nth-child(2) {
|
|
85
|
+
flex: 1;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.split .panel {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: column;
|
|
91
|
+
}
|
|
92
|
+
.split .panel .panel-header {
|
|
93
|
+
background-color: #2c2c2c;
|
|
94
|
+
padding: 10px;
|
|
95
|
+
border-bottom: 1px solid #444;
|
|
96
|
+
font-weight: bold;
|
|
97
|
+
color: #ddd;
|
|
98
|
+
}
|
|
99
|
+
.split .panel .panel-content {
|
|
100
|
+
padding: 15px;
|
|
101
|
+
flex: 1;
|
|
102
|
+
overflow-y: auto;
|
|
103
|
+
}
|
|
104
|
+
.split .panel .panel-content h3 {
|
|
105
|
+
margin-top: 0;
|
|
106
|
+
color: #aaa;
|
|
107
|
+
border-bottom: 1px solid #444;
|
|
108
|
+
padding-bottom: 5px;
|
|
109
|
+
}
|
|
110
|
+
.split .panel .panel-content p {
|
|
111
|
+
margin: 10px 0;
|
|
112
|
+
line-height: 1.5;
|
|
113
|
+
}
|
|
114
|
+
.split .panel .panel-content ul {
|
|
115
|
+
padding-left: 20px;
|
|
116
|
+
}
|
|
117
|
+
.split .panel .panel-content ul li {
|
|
118
|
+
margin: 5px 0;
|
|
119
|
+
}
|
|
120
|
+
.split .panel .panel-content .button-group {
|
|
121
|
+
margin-top: 15px;
|
|
122
|
+
}
|
|
123
|
+
.split .panel .panel-content .button-group button {
|
|
124
|
+
background: #444;
|
|
125
|
+
color: white;
|
|
126
|
+
border: none;
|
|
127
|
+
padding: 8px 15px;
|
|
128
|
+
margin-right: 10px;
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
cursor: pointer;
|
|
131
|
+
}
|
|
132
|
+
.split .panel .panel-content .button-group button:hover {
|
|
133
|
+
background: #555;
|
|
134
|
+
}
|
|
135
|
+
.split .panel .panel-content .info-box {
|
|
136
|
+
background: #222;
|
|
137
|
+
padding: 10px;
|
|
138
|
+
border-radius: 4px;
|
|
139
|
+
border-left: 3px solid #666;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/*# sourceMappingURL=style.css.map */
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sourceRoot":"","sources":["../src/style.scss"],"names":[],"mappings":"AAAA;EACI;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;EACA;;;AAGJ;EACI;EACA;;;AAGJ;EACI;EACA;EACA;EACA;;AAEA;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAIR;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAKZ;EACI;;AAEA;EACI;;AAGJ;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;AAKZ;EACI;;;AAIR;EACI;EACA;;AAEA;EACI;EACA;EACA;EACA;EACA;;AAGJ;EACI;EACA;EACA;;AAEA;EACI;EACA;EACA;EACA;;AAGJ;EACI;EACA;;AAGJ;EACI;;AAEA;EACI;;AAIR;EACI;;AAEA;EACI;EACA;EACA;EACA;EACA;EACA;EACA;;AAEA;EACI;;AAKZ;EACI;EACA;EACA;EACA","file":"style.css"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type Direction = "left" | "right" | "top" | "bottom";
|
|
2
|
+
export type NyaNode = string;
|
|
3
|
+
export type SplitType = "row" | "column";
|
|
4
|
+
export interface NyaSplit {
|
|
5
|
+
nodes: [NyaNode | NyaSplit, NyaNode | NyaSplit];
|
|
6
|
+
type: SplitType;
|
|
7
|
+
}
|
|
8
|
+
export type StructNode = [string | StructNode, string | StructNode, 1?];
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
export function convertToSplits(arr) {
|
|
2
|
+
if (arr.length === 0) {
|
|
3
|
+
throw new Error("Array structure must have at least one element");
|
|
4
|
+
}
|
|
5
|
+
let isColumnLayout = false;
|
|
6
|
+
let actualArray = arr;
|
|
7
|
+
if (arr.length === 3 && arr[2] === 1) {
|
|
8
|
+
isColumnLayout = true;
|
|
9
|
+
actualArray = arr.slice(0, 2);
|
|
10
|
+
}
|
|
11
|
+
if (actualArray.length === 1) {
|
|
12
|
+
const element = actualArray[0];
|
|
13
|
+
if (Array.isArray(element)) {
|
|
14
|
+
const nestedSplit = convertToSplits(element);
|
|
15
|
+
return {
|
|
16
|
+
type: isColumnLayout ? "column" : "row",
|
|
17
|
+
nodes: [nestedSplit, "empty"]
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
return {
|
|
22
|
+
type: isColumnLayout ? "column" : "row",
|
|
23
|
+
nodes: [element, "empty"]
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
else if (actualArray.length === 2) {
|
|
28
|
+
const [first, second] = actualArray;
|
|
29
|
+
const firstElement = Array.isArray(first) ? convertToSplits(first) : first;
|
|
30
|
+
const secondElement = Array.isArray(second) ? convertToSplits(second) : second;
|
|
31
|
+
return {
|
|
32
|
+
type: isColumnLayout ? "column" : "row",
|
|
33
|
+
nodes: [firstElement, secondElement]
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
const [first, ...rest] = actualArray;
|
|
38
|
+
let restStructure = [rest[0]];
|
|
39
|
+
for (let i = 1; i < rest.length; i++) {
|
|
40
|
+
if (isColumnLayout) {
|
|
41
|
+
restStructure = [restStructure, rest[i], 1];
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
restStructure = [restStructure, rest[i]];
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const firstElement = Array.isArray(first) ? convertToSplits(first) : first;
|
|
48
|
+
const restElement = Array.isArray(restStructure) ? convertToSplits(restStructure) : restStructure;
|
|
49
|
+
return {
|
|
50
|
+
type: isColumnLayout ? "column" : "row",
|
|
51
|
+
nodes: [firstElement, restElement]
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Direction } from "../types.js";
|
|
2
|
+
export declare function getRelativePosition(e: MouseEvent, element: HTMLElement): {
|
|
3
|
+
x: number;
|
|
4
|
+
y: number;
|
|
5
|
+
};
|
|
6
|
+
export declare function detectDockZone(e: MouseEvent, panel: HTMLElement, threshold?: number): Direction | "center";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function getRelativePosition(e, element) {
|
|
2
|
+
const rect = element.getBoundingClientRect();
|
|
3
|
+
return {
|
|
4
|
+
x: e.clientX - rect.left,
|
|
5
|
+
y: e.clientY - rect.top
|
|
6
|
+
};
|
|
7
|
+
}
|
|
8
|
+
export function detectDockZone(e, panel, threshold = 0.35) {
|
|
9
|
+
const rect = panel.getBoundingClientRect();
|
|
10
|
+
const relX = (e.clientX - rect.left) / rect.width;
|
|
11
|
+
const relY = (e.clientY - rect.top) / rect.height;
|
|
12
|
+
if (relX < threshold)
|
|
13
|
+
return "left";
|
|
14
|
+
if (relX > 1 - threshold)
|
|
15
|
+
return "right";
|
|
16
|
+
if (relY < threshold)
|
|
17
|
+
return "top";
|
|
18
|
+
if (relY > 1 - threshold)
|
|
19
|
+
return "bottom";
|
|
20
|
+
return "center";
|
|
21
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
export function movePanel(controller, sourceId, targetId, zone) {
|
|
2
|
+
if (!controller._split || sourceId === targetId)
|
|
3
|
+
return;
|
|
4
|
+
if (controller._split.nodes.length === 0) {
|
|
5
|
+
controller._split = {
|
|
6
|
+
nodes: [sourceId, targetId],
|
|
7
|
+
type: (zone === "left" || zone === "right") ? "row" : "column",
|
|
8
|
+
};
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
function recursiveRemove(node) {
|
|
12
|
+
if (typeof node === "string") {
|
|
13
|
+
return node === sourceId ? null : node;
|
|
14
|
+
}
|
|
15
|
+
const [nodeA, nodeB] = node.nodes;
|
|
16
|
+
const newA = recursiveRemove(nodeA);
|
|
17
|
+
const newB = recursiveRemove(nodeB);
|
|
18
|
+
if (newA && newB) {
|
|
19
|
+
if (newA !== nodeA || newB !== nodeB) {
|
|
20
|
+
return { ...node, nodes: [newA, newB] };
|
|
21
|
+
}
|
|
22
|
+
return node;
|
|
23
|
+
}
|
|
24
|
+
return newA || newB;
|
|
25
|
+
}
|
|
26
|
+
const treeAfterRemove = recursiveRemove(controller._split);
|
|
27
|
+
const newSplitNode = {
|
|
28
|
+
type: (zone === "left" || zone === "right") ? "row" : "column",
|
|
29
|
+
nodes: (zone === "left" || zone === "top") ? [sourceId, targetId] : [targetId, sourceId],
|
|
30
|
+
};
|
|
31
|
+
function recursiveDock(node) {
|
|
32
|
+
if (typeof node === "string") {
|
|
33
|
+
return node === targetId ? newSplitNode : node;
|
|
34
|
+
}
|
|
35
|
+
const [nodeA, nodeB] = node.nodes;
|
|
36
|
+
if (typeof nodeA === "string" && nodeA === targetId) {
|
|
37
|
+
return { ...node, nodes: [newSplitNode, nodeB] };
|
|
38
|
+
}
|
|
39
|
+
if (typeof nodeB === "string" && nodeB === targetId) {
|
|
40
|
+
return { ...node, nodes: [nodeA, newSplitNode] };
|
|
41
|
+
}
|
|
42
|
+
const newA = recursiveDock(nodeA);
|
|
43
|
+
const newB = recursiveDock(nodeB);
|
|
44
|
+
if (newA !== nodeA || newB !== nodeB) {
|
|
45
|
+
return { ...node, nodes: [newA, newB] };
|
|
46
|
+
}
|
|
47
|
+
return node;
|
|
48
|
+
}
|
|
49
|
+
let finalTree;
|
|
50
|
+
if (!treeAfterRemove) {
|
|
51
|
+
finalTree = newSplitNode;
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
finalTree = recursiveDock(treeAfterRemove);
|
|
55
|
+
}
|
|
56
|
+
controller._split = finalTree;
|
|
57
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wxn0brp/nya-dock",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"main": "dist/index.js",
|
|
5
|
+
"types": "dist/index.d.ts",
|
|
6
|
+
"description": "",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/wxn0brP/Nyadock.git"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://github.com/wxn0brP/Nyadock",
|
|
12
|
+
"author": "wxn0brP",
|
|
13
|
+
"license": "MIT",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc && tsc-alias && sass src:dist"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"sass": "*",
|
|
20
|
+
"tsc-alias": "*",
|
|
21
|
+
"typescript": "*"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"dist"
|
|
25
|
+
],
|
|
26
|
+
"exports": {
|
|
27
|
+
".": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"import": "./dist/index.js",
|
|
30
|
+
"default": "./dist/index.js"
|
|
31
|
+
},
|
|
32
|
+
"./*": {
|
|
33
|
+
"types": "./dist/*.d.ts",
|
|
34
|
+
"import": "./dist/*.js",
|
|
35
|
+
"default": "./dist/*.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"dependencies": {
|
|
39
|
+
"@wxn0brp/flanker-ui": "^0.4.3"
|
|
40
|
+
}
|
|
41
|
+
}
|