runlab 1.0.0 → 1.2.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/dist/app/app.js CHANGED
@@ -3,12 +3,18 @@ import { createNavbar } from "./core/navbar.js";
3
3
  import { FileExplorer } from "./fileExplorer/FileExplorer.js";
4
4
  import { createTerminal } from "./terminal/terminal.js";
5
5
 
6
- export function generateContainer(parentId,w,h) {
6
+ export function generateContainer(parentId) {
7
7
  const container = document.createElement("div");
8
8
  container.id = "runlab-container";
9
9
  Object.assign(container.style, {
10
- width: w + "px",
11
- height: h + "px",
10
+ position: "fixed",
11
+ inset: "0",
12
+ left: "0",
13
+ bottom: "0",
14
+
15
+ width: "100vw",
16
+ height: "100vh",
17
+
12
18
  border: "1px solid #000",
13
19
  display: "flex",
14
20
  justifyContent: "center",
@@ -16,7 +22,7 @@ export function generateContainer(parentId,w,h) {
16
22
  overflow: "hidden"
17
23
  });
18
24
  document.getElementById(parentId).appendChild(container);
19
- gridTemplate("runlab-container", w, h);
25
+ gridTemplate("runlab-container");
20
26
  createEditor("runlab-editor","");
21
27
 
22
28
  // View and Editor toggle
@@ -202,7 +208,7 @@ export function setEditorActive(status = true){
202
208
  //================================ SVG Warning Text ================================
203
209
 
204
210
  const warningText = document.createElement("p");
205
- warningText.textContent = "No file selected.";
211
+ warningText.textContent = "No files selected.";
206
212
  warningText.style.color = "#aaa";
207
213
  warningText.style.fontSize = "0.9em";
208
214
  warningText.style.margin = "0";
@@ -253,4 +259,12 @@ export function getFeExecutables() {
253
259
 
254
260
  export function getFileNode(file) {
255
261
  return feNode.getFileNode(file);
262
+ }
263
+
264
+ export function getNodeFromPath(path, currentPath) {
265
+ return feNode.getNodeFromPath(path, currentPath);
266
+ }
267
+
268
+ export function setMoveNode(node, destNode) {
269
+ feNode.setMoveNode(node, destNode);
256
270
  }
@@ -34,7 +34,7 @@ export function createNavbar(parentId) {
34
34
  });
35
35
  ul.appendChild(leftGroup);
36
36
 
37
- const menuItems = ["Run", "Help"];
37
+ const menuItems = ["✕", "🗗","Run", "Help"];
38
38
  menuItems.forEach(item => {
39
39
  const li = document.createElement("li");
40
40
  li.textContent = item;
@@ -46,6 +46,36 @@ export function createNavbar(parentId) {
46
46
  cursor: "pointer",
47
47
  color: "#fff"
48
48
  });
49
+ if (item === "✕") {
50
+ li.addEventListener("click", () => {
51
+ const app = document.getElementById("runlab-custom-div");
52
+ app.style.display = "none";
53
+ document.getElementById("runlab-custom-btn").textContent = "Show App";
54
+ });
55
+ }
56
+ if (item === "🗗"){
57
+ li.addEventListener("click", () => {
58
+ const container = document.getElementById("runlab-container");
59
+ let isMaximized = container.style.height === "100vh"? false: true;
60
+
61
+ if(isMaximized){
62
+ container.style.height = "100vh";
63
+ container.style.width = "100vw";
64
+ container.style.left = "0";
65
+ container.style.top = "0";
66
+ container.style.bottom = "0";
67
+ container.style.transform = "none";
68
+ }
69
+ if(!isMaximized){
70
+ container.style.height = "70vh";
71
+ container.style.width = "70vw";
72
+ container.style.left = "50%";
73
+ container.style.top = "auto";
74
+ container.style.bottom = "0";
75
+ container.style.transform = "translateX(-50%)";
76
+ }
77
+ });
78
+ }
49
79
  if (item === "Run") {
50
80
  li.addEventListener("click", () => {
51
81
  sendCode(getCurrentCode(),getCurrentExtension());
@@ -73,7 +103,9 @@ export function createNavbar(parentId) {
73
103
  <li><span style="font-weight: bold;">pwd</span>: Print current directory</li>
74
104
  <li><span style="font-weight: bold;">run</span>: Run a file (e.g., run script.js)</li>
75
105
  <li><span style="font-weight: bold;">help | h</span>: Show help message</li>
106
+ <li><span style="font-weight: bold;">mv</span>: Move a file or folder (e.g., mv folder1/script.js folder2)</li>
76
107
  </ul>
108
+ <p>If the path starts with / it will be considered absolute (from root), otherwise it will be considered relative to the current path.</p>
77
109
  `,
78
110
  width: 700,
79
111
  confirmButtonText: "Got it!"
@@ -11,6 +11,7 @@ export class FileExplorer {
11
11
  this.execFiles = [];
12
12
  this.filesPath = [];
13
13
  this.path = [];
14
+ this.currentNodeDragged = null;
14
15
  this.render();
15
16
  }
16
17
 
@@ -42,6 +43,106 @@ export class FileExplorer {
42
43
  this.execFiles = files;
43
44
  }
44
45
 
46
+ getNodeFromPath(path, currentPath) {
47
+
48
+ if (path === "/") {
49
+ return this.root;
50
+ }
51
+
52
+ if (path.includes("..")) {
53
+ return null;
54
+ }
55
+ // Absolute path
56
+ if (path.startsWith("/")) {
57
+ path = path.slice(1);
58
+ let currentNode = this.root;
59
+
60
+ if (!path.includes("/")) {
61
+ if (path.includes(".")) { //File
62
+ const [name, ext] = path.split(".");
63
+ return currentNode.children.find(c => c.name === name && c.ext === ext);
64
+ }
65
+ return currentNode.children.find(c => c.name === path);//Folder
66
+ }
67
+
68
+ let pathList = path.split("/").filter(p => p);
69
+
70
+ for (let i = 0; i < pathList.length; i++) {
71
+ const part = pathList[i];
72
+
73
+ if (part.includes(".")) {
74
+ const [name, ext] = part.split(".");
75
+ const nextNode = currentNode.children.find(c => c.name === name && c.ext === ext);
76
+ if (!nextNode) return null;
77
+ currentNode = nextNode;
78
+ }
79
+
80
+ if (!part.includes(".")) {
81
+ const nextNode = currentNode.children.find(c => c.name === part && !c.ext);
82
+ if (!nextNode) return null;
83
+ currentNode = nextNode;
84
+ }
85
+
86
+ }
87
+ return currentNode;
88
+
89
+ }
90
+ // Relative path
91
+ if (!path.startsWith("/")) {
92
+ let currentNode = this.root;
93
+
94
+ if (currentPath.length > 1) {
95
+ for (let i = 1; i < currentPath.length; i++) {
96
+ const part = currentPath[i];
97
+ const nextNode = currentNode.children.find(c => c.name === part && !c.ext);
98
+ if (!nextNode) return null;
99
+ currentNode = nextNode;
100
+ }
101
+ }
102
+
103
+ if (!path.includes("/")) {
104
+ if (path.includes(".")) { //File
105
+ const [name, ext] = path.split(".");
106
+ return currentNode.children.find(c => c.name === name && c.ext === ext);
107
+ }
108
+ return currentNode.children.find(c => c.name === path);//Folder
109
+ }
110
+
111
+ let pathList = path.split("/").filter(p => p);
112
+
113
+ for (let i = 0; i < pathList.length; i++) {
114
+ const part = pathList[i];
115
+
116
+ if (part.includes(".")) {
117
+ const [name, ext] = part.split(".");
118
+ const nextNode = currentNode.children.find(c => c.name === name && c.ext === ext);
119
+ if (!nextNode) return null;
120
+ currentNode = nextNode;
121
+ }
122
+
123
+ if (!part.includes(".")) {
124
+ const nextNode = currentNode.children.find(c => c.name === part && !c.ext);
125
+ if (!nextNode) return null;
126
+ currentNode = nextNode;
127
+ }
128
+
129
+ }
130
+ return currentNode;
131
+
132
+ }
133
+
134
+ }
135
+
136
+ setMoveNode(node, destNode) {
137
+ if (node === this.root) return;
138
+ if (node instanceof File) {
139
+ this.moveFile(node, destNode);
140
+ }
141
+ if (node instanceof Folder) {
142
+ this.moveFolder(node, destNode);
143
+ }
144
+ }
145
+
45
146
  /* =========================
46
147
  RENDER
47
148
  ========================== */
@@ -61,6 +162,19 @@ export class FileExplorer {
61
162
  this.showRootMenu(e);
62
163
  };
63
164
 
165
+ rootDiv.addEventListener("dragover", (e) => {
166
+ rootDiv.style.border = "1px solid #117bd1";
167
+ e.preventDefault();
168
+ });
169
+ rootDiv.addEventListener("dragleave", (e) => {
170
+ rootDiv.style.border = "none";
171
+ });
172
+ rootDiv.addEventListener("drop", (e) => {
173
+ rootDiv.style.border = "none";
174
+ e.preventDefault();
175
+ this.setMoveNode(this.currentNodeDragged, this.root);
176
+ this.currentNodeDragged = null;
177
+ });
64
178
 
65
179
  this.renderFolder(this.root, rootDiv);
66
180
 
@@ -70,6 +184,8 @@ export class FileExplorer {
70
184
  renderFolder(folder, parentUl) {
71
185
  folder.children.forEach(node => {
72
186
  if (node instanceof Folder) {
187
+
188
+ /* ============ BUTTON =============== */
73
189
  const btn = document.createElement("button");
74
190
  btn.textContent = `📁 ${node.name}`;
75
191
  btn.style.width = "100%";
@@ -80,18 +196,18 @@ export class FileExplorer {
80
196
  btn.style.paddingTop = "8px";
81
197
  btn.style.paddingBottom = "8px";
82
198
  btn.style.cursor = "pointer";
83
-
84
-
85
-
86
- btn.onmouseover = () => btn.style.backgroundColor = "#555";
87
- btn.onmouseout = () => btn.style.backgroundColor = "transparent";
88
-
199
+ btn.draggable = true;
200
+ /* ============ UL =============== */
89
201
  const ul = document.createElement("ul");
90
202
  ul.style.display = "none";
91
203
  ul.style.listStyle = "none";
92
204
  ul.style.margin = "0";
93
205
  ul.style.padding = "0 0 0 24px";
94
-
206
+ /* ============ DIV =============== */
207
+ const div = document.createElement("div");
208
+ /* ============ EVENTS =============== */
209
+ btn.onmouseover = () => btn.style.backgroundColor = "#555";
210
+ btn.onmouseout = () => btn.style.backgroundColor = "transparent";
95
211
 
96
212
  btn.onclick = () => this.toggle(ul);
97
213
  btn.oncontextmenu = e => {
@@ -100,11 +216,46 @@ export class FileExplorer {
100
216
  this.showFolderMenu(e, node);
101
217
  };
102
218
 
103
- parentUl.append(btn, ul);
219
+ btn.addEventListener("dragstart", (e) => {
220
+
221
+ this.currentNodeDragged = node;
222
+ });
223
+ btn.addEventListener("dragover", (e) => {
224
+ div.style.border = "1px solid #117bd1";
225
+ e.preventDefault();
226
+ });
227
+ btn.addEventListener("dragleave", (e) => {
228
+ div.style.border = "none";
229
+ });
230
+ btn.addEventListener("drop", (e) => {
231
+ div.style.border = "none";
232
+ e.preventDefault();
233
+ this.setMoveNode(this.currentNodeDragged, node);
234
+ this.currentNodeDragged = null;
235
+ });
236
+
237
+ ul.addEventListener("dragover", (e) => {
238
+ div.style.border = "1px solid #117bd1";
239
+ e.preventDefault();
240
+ });
241
+ ul.addEventListener("dragleave", (e) => {
242
+ div.style.border = "none";
243
+ });
244
+ ul.addEventListener("drop", (e) => {
245
+ div.style.border = "none";
246
+ e.preventDefault();
247
+ this.setMoveNode(this.currentNodeDragged, node);
248
+ this.currentNodeDragged = null;
249
+ });
250
+
251
+ /* ============ APPEND =============== */
252
+ div.append(btn, ul);
253
+ parentUl.append(div);
104
254
  this.renderFolder(node, ul);
105
255
  }
106
256
 
107
257
  if (node instanceof File) {
258
+ /* ============ LI =============== */
108
259
  const li = document.createElement("li");
109
260
  li.textContent = `📄 ${node.name}.${node.ext}`;
110
261
  li.style.width = "100%";
@@ -115,8 +266,8 @@ export class FileExplorer {
115
266
  li.style.paddingTop = "8px";
116
267
  li.style.paddingBottom = "8px";
117
268
  li.style.cursor = "pointer";
118
-
119
-
269
+ li.draggable = true;
270
+ /* ============ EVENTS =============== */
120
271
  li.onclick = () => {
121
272
  setActiveFile(node);
122
273
  setViewActive(false);
@@ -130,6 +281,23 @@ export class FileExplorer {
130
281
  this.showFileMenu(e, node);
131
282
  };
132
283
 
284
+ li.addEventListener("dragstart", (e) => {
285
+ this.currentNodeDragged = node;
286
+ });
287
+ li.addEventListener("dragover", (e) => {
288
+ parentUl.parentElement.style.border = "1px solid #117bd1";
289
+ e.preventDefault();
290
+ });
291
+ li.addEventListener("dragleave", (e) => {
292
+ parentUl.parentElement.style.border = "none";
293
+ });
294
+ li.addEventListener("drop", (e) => {
295
+ parentUl.parentElement.style.border = "none";
296
+ e.preventDefault();
297
+ this.setMoveNode(this.currentNodeDragged, node.parent);
298
+ this.currentNodeDragged = null;
299
+ });
300
+ /* ============ APPEND =============== */
133
301
  parentUl.appendChild(li);
134
302
  this.filesPath.push({ name: `${node.name}.${node.ext}`, path: node.parent.path });
135
303
 
@@ -168,6 +336,29 @@ export class FileExplorer {
168
336
  this.render();
169
337
  }
170
338
 
339
+ moveFile(node, newParent) {
340
+ if (newParent === node) return;
341
+ if (newParent instanceof File) return;
342
+ if (node === this.root) return;
343
+ const oldParent = node.parent;
344
+ oldParent.children = oldParent.children.filter(c => c !== node);
345
+ newParent.children.push(node);
346
+ node.parent = newParent;
347
+ this.render();
348
+ }
349
+
350
+ moveFolder(node, newParent) {
351
+ if (newParent === node) return;
352
+ if (newParent instanceof File) return;
353
+ if (node === this.root) return;
354
+ const oldParent = node.parent;
355
+ oldParent.children = oldParent.children.filter(c => c !== node);
356
+ newParent.children.push(node);
357
+ node.parent = newParent;
358
+ node.path = [...newParent.path, node.name];
359
+ this.render();
360
+ }
361
+
171
362
  /* =========================
172
363
  MENUS
173
364
  ========================== */
@@ -1,6 +1,6 @@
1
1
  import { getCurrentDir, getCurrentPath, setCurrentDir, setCurrentPath } from "./terminal.js"
2
2
  import { sendCode } from "../../index.js";
3
- import { setViewActive, getFeExecutables, getFileNode } from "../app.js";
3
+ import { setViewActive, getFeExecutables, getFileNode, getNodeFromPath, setMoveNode } from "../app.js";
4
4
 
5
5
  export function print(text = "") {
6
6
  const div = document.createElement("div")
@@ -114,4 +114,33 @@ export function handleRun(cmd) {
114
114
  const fileNode = getFileNode(file)
115
115
  sendCode(fileNode.content, fileNode.ext)
116
116
  setViewActive(true)
117
+ }
118
+
119
+ export function handleMove(cmd) {
120
+
121
+ if (cmd.split(" ").length !== 3) {
122
+ print("Invalid move command format! Use: mv <source> <destination>")
123
+ return
124
+ }
125
+
126
+ const nodePath = cmd.split(" ")[1];
127
+ const destPath = cmd.split(" ")[2];
128
+
129
+ const currentPath = getCurrentPath();
130
+
131
+ const node = getNodeFromPath(nodePath, currentPath);
132
+ const destNode = getNodeFromPath(destPath, currentPath);
133
+
134
+ if (!node) {
135
+ print("File or folder to be moved not found!")
136
+ return
137
+ }
138
+
139
+ if (!destNode) {
140
+ print("Folder to move into not found!")
141
+ return
142
+ }
143
+
144
+ setMoveNode(node, destNode)
145
+
117
146
  }
@@ -1,4 +1,4 @@
1
- import {print, handleLS, handleClear, handleChangeDirectory, handleRun} from "./commands.js"
1
+ import {print, handleLS, handleClear, handleChangeDirectory, handleRun, handleMove} from "./commands.js"
2
2
 
3
3
  export let currentDirNode = null
4
4
  export let currentPath = []
@@ -120,7 +120,13 @@ function handleCommand(cmd) {
120
120
  print("- pwd: Print current directory")
121
121
  print("- run: Run a file (e.g., run script.js)")
122
122
  print("- help | h: Show this help message")
123
- } else if (trimmed !== "") {
123
+ print("- mv: Move a file or folder (e.g., mv <source> <destination>)")
124
+ print(" If the path starts with / it will be considered absolute (from root), otherwise it will be considered relative to the current path.")
125
+
126
+ } else if (split[0] === "mv") {
127
+ handleMove(trimmed)
128
+
129
+ }else if (trimmed !== "") {
124
130
  print(`command not found: ${trimmed}`)
125
131
  }
126
132
  }
package/dist/index.js CHANGED
@@ -1,5 +1,6 @@
1
1
  import { generateContainer } from "./app/app.js";
2
2
  import { appendViewContent } from "./app/app.js";
3
+ import { ReactRunlabButton } from "./reactButton.jsx";
3
4
 
4
5
  let runtimeUrl = "";
5
6
  function setRuntimeUrl(url) {
@@ -11,11 +12,9 @@ function getRuntimeUrl() {
11
12
 
12
13
  export async function run({
13
14
  parentId,
14
- width = 1200,
15
- height = 800,
16
15
  runtimeUrl = ""
17
16
  }) {
18
- generateContainer(parentId, width, height);
17
+ generateContainer(parentId);
19
18
  setRuntimeUrl(runtimeUrl);
20
19
  }
21
20
 
@@ -47,3 +46,7 @@ export async function sendCode(code, ext = "txt") {
47
46
  console.error(err);
48
47
  }
49
48
  }
49
+
50
+ //=============================== Buttons ===============================
51
+
52
+ export { ReactRunlabButton };
@@ -0,0 +1,46 @@
1
+ import { useEffect } from 'react';
2
+ import { run } from "./index.js";
3
+
4
+ export function ReactRunlabButton(props = { runConfig: {} }) {
5
+ const { runConfig } = props;
6
+
7
+ useEffect(() => {
8
+ if (typeof window !== 'undefined' && window.__runlab_started) return;
9
+
10
+ if (typeof window !== 'undefined') {
11
+ window.__runlab_started = true;
12
+ }
13
+
14
+ run(runConfig);
15
+ }, []);
16
+
17
+ const showApp = () => {
18
+ const app = document.getElementById("runlab-custom-div");
19
+ const btn = document.getElementById("runlab-custom-btn");
20
+
21
+ const displayStyle = window.getComputedStyle(app).display;
22
+
23
+ if (displayStyle === "none") {
24
+ app.style.display = "block";
25
+ btn.textContent = "Hide App";
26
+ }
27
+ if (displayStyle === "block") {
28
+ app.style.display = "none";
29
+ btn.textContent = "Show App";
30
+ }
31
+ };
32
+
33
+ return (
34
+ <>
35
+ <button
36
+ id="runlab-custom-btn"
37
+ onClick={showApp}
38
+ className="show-app-button"
39
+ >
40
+ Show App
41
+ </button>
42
+
43
+ <div id="runlab-custom-div" style={{ display: "none" }}></div>
44
+ </>
45
+ );
46
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "runlab",
3
- "version": "1.0.0",
3
+ "version": "1.2.0",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",