positron.js 1.0.0 → 1.0.2
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/core/mac/main.swift +23 -2
- package/core/win/main.cs +14 -4
- package/index.js +179 -18
- package/package.json +2 -3
package/core/mac/main.swift
CHANGED
|
@@ -18,6 +18,7 @@ let AUTH_TOKEN: String = {
|
|
|
18
18
|
}()
|
|
19
19
|
|
|
20
20
|
var windowObservations: [Int: NSKeyValueObservation] = [:]
|
|
21
|
+
var navigationDelegates: [Int: WebViewNavigationDelegate] = [:]
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
import Foundation
|
|
@@ -225,6 +226,9 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
|
|
|
225
226
|
config.userContentController.addUserScript(preload)
|
|
226
227
|
|
|
227
228
|
let webView = PositronWebView(frame: NSRect(origin: .zero, size: frame.size), configuration: config)
|
|
229
|
+
let navDelegate = WebViewNavigationDelegate(windowId: windowId)
|
|
230
|
+
webView.navigationDelegate = navDelegate
|
|
231
|
+
navigationDelegates[windowId] = navDelegate
|
|
228
232
|
// Resize webview automatically when the window resizes
|
|
229
233
|
webView.autoresizingMask = [.width, .height]
|
|
230
234
|
newWindow.contentView = webView
|
|
@@ -242,6 +246,7 @@ func handleCommand(windowId: Int, command: String, args: [String]) {
|
|
|
242
246
|
observation.invalidate()
|
|
243
247
|
windowObservations.removeValue(forKey: windowId)
|
|
244
248
|
}
|
|
249
|
+
navigationDelegates.removeValue(forKey: windowId)
|
|
245
250
|
|
|
246
251
|
|
|
247
252
|
windows.removeValue(forKey: windowId)
|
|
@@ -305,7 +310,6 @@ case "forceCloseWindow":
|
|
|
305
310
|
}
|
|
306
311
|
(window.contentView as? WKWebView)?.load(URLRequest(url: url))
|
|
307
312
|
|
|
308
|
-
|
|
309
313
|
case "hide":
|
|
310
314
|
guard let window = windows[windowId] else { return }
|
|
311
315
|
window.orderOut(nil)
|
|
@@ -344,7 +348,6 @@ case "forceCloseWindow":
|
|
|
344
348
|
(window.contentView as? WKWebView)?
|
|
345
349
|
.loadFileURL(fileURL, allowingReadAccessTo: fileURL.deletingLastPathComponent())
|
|
346
350
|
|
|
347
|
-
|
|
348
351
|
case "setBounds":
|
|
349
352
|
guard let window = windows[windowId] else { return }
|
|
350
353
|
guard args.count >= 4,
|
|
@@ -682,6 +685,24 @@ case "resetMenu":
|
|
|
682
685
|
}
|
|
683
686
|
}
|
|
684
687
|
|
|
688
|
+
// MARK: - WebView Navigation Delegate
|
|
689
|
+
|
|
690
|
+
final class WebViewNavigationDelegate: NSObject, WKNavigationDelegate {
|
|
691
|
+
let windowId: Int
|
|
692
|
+
|
|
693
|
+
init(windowId: Int) {
|
|
694
|
+
self.windowId = windowId
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
698
|
+
let isFile = webView.url?.isFileURL ?? false
|
|
699
|
+
let eventName = isFile ? "loadFile-reply-\(windowId)" : "loadURL-reply-\(windowId)"
|
|
700
|
+
AppDelegate.shared?.ipcClient.send(
|
|
701
|
+
IPCResponse(windowId: windowId, event: eventName, data: [:])
|
|
702
|
+
)
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
685
706
|
// MARK: - WebView → Swift IPC Handler
|
|
686
707
|
|
|
687
708
|
/// Receives messages from renderer JS: window.webkit.messageHandlers.ipc.postMessage({...})
|
package/core/win/main.cs
CHANGED
|
@@ -102,11 +102,10 @@ namespace PositronWindows
|
|
|
102
102
|
|
|
103
103
|
public static bool IsPackaged { get; private set; } = false;
|
|
104
104
|
|
|
105
|
-
|
|
105
|
+
public static IPCClient _ipcClient = null!;
|
|
106
106
|
private static Process? _nodeProcess;
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
private static readonly Dictionary<int, Window> WindowsMap = new();
|
|
108
|
+
public static readonly Dictionary<int, Window> WindowsMap = new();
|
|
110
109
|
private static readonly Dictionary<int, DockPanel> LayoutMap = new();
|
|
111
110
|
private static readonly Dictionary<int, Menu> MenuMap = new();
|
|
112
111
|
private static readonly HashSet<int> _forceClosing = new();
|
|
@@ -330,6 +329,17 @@ private void StartNodeProcess(string workingDirectory)
|
|
|
330
329
|
window.Title = webView.CoreWebView2.DocumentTitle;
|
|
331
330
|
};
|
|
332
331
|
|
|
332
|
+
webView.CoreWebView2.NavigationCompleted += (s, e) =>
|
|
333
|
+
{
|
|
334
|
+
bool isFile = webView.Source != null && webView.Source.IsFile;
|
|
335
|
+
string eventName = isFile ? $"loadFile-reply-{windowId}" : $"loadURL-reply-{windowId}";
|
|
336
|
+
_ipcClient.Send(new IPCResponse
|
|
337
|
+
{
|
|
338
|
+
windowId = windowId,
|
|
339
|
+
@event = eventName
|
|
340
|
+
});
|
|
341
|
+
};
|
|
342
|
+
|
|
333
343
|
webView.CoreWebView2.ContextMenuRequested += (s, e) =>
|
|
334
344
|
{
|
|
335
345
|
if (LayoutMap.TryGetValue(windowId, out var l) && l.ContextMenu != null)
|
|
@@ -819,7 +829,7 @@ case "setBounds":
|
|
|
819
829
|
}
|
|
820
830
|
}
|
|
821
831
|
|
|
822
|
-
|
|
832
|
+
public static WebView2? GetWebView(int windowId)
|
|
823
833
|
{
|
|
824
834
|
if (LayoutMap.TryGetValue(windowId, out var layout))
|
|
825
835
|
{
|
package/index.js
CHANGED
|
@@ -226,6 +226,8 @@ let _windowCounter = 0;
|
|
|
226
226
|
|
|
227
227
|
class Window extends Events.EventEmitter {
|
|
228
228
|
|
|
229
|
+
id = 0;
|
|
230
|
+
|
|
229
231
|
/** Creates a new window instance. */
|
|
230
232
|
constructor(options = {
|
|
231
233
|
|
|
@@ -292,16 +294,6 @@ class Window extends Events.EventEmitter {
|
|
|
292
294
|
this.emit("title-updated", title);
|
|
293
295
|
}
|
|
294
296
|
|
|
295
|
-
/**
|
|
296
|
-
* Loads a remote URL in the window. Emits "url-loaded" and "navigated" events with the URL as data.
|
|
297
|
-
* @param {string} url The URL to load.
|
|
298
|
-
*/
|
|
299
|
-
loadURL(url) {
|
|
300
|
-
this.sendCommand("loadURL", [url]);
|
|
301
|
-
this.emit("url-loaded", url);
|
|
302
|
-
this.emit("navigated", url);
|
|
303
|
-
}
|
|
304
|
-
|
|
305
297
|
/**
|
|
306
298
|
* Triggers the print dialog for the window. Emits a "print" event. Note that the actual print functionality and dialog is handled by the native layer, so behavior may vary across platforms.
|
|
307
299
|
*/
|
|
@@ -320,13 +312,25 @@ class Window extends Events.EventEmitter {
|
|
|
320
312
|
}
|
|
321
313
|
|
|
322
314
|
/**
|
|
323
|
-
* Loads a
|
|
315
|
+
* Loads a file into the window. The path can be an absolute file path or a relative path from the application's root directory. Emits a "file-loaded" event with the path as data, and a "navigated" event with the path as data.
|
|
324
316
|
* @param {string} path The path to the file to load.
|
|
325
317
|
*/
|
|
326
|
-
loadFile(path) {
|
|
327
|
-
this.
|
|
328
|
-
this.emit("file-loaded", path);
|
|
329
|
-
this.emit("navigated", path);
|
|
318
|
+
async loadFile(path) {
|
|
319
|
+
const res = await this.request("loadFile", `loadFile-reply-${this.id}`, path);
|
|
320
|
+
this.emit("file-loaded", path);
|
|
321
|
+
this.emit("navigated", path);
|
|
322
|
+
return res;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Loads a URL into the window. Emits a "url-loaded" event with the URL as data, and a "navigated" event with the URL as data.
|
|
327
|
+
* @param {string} url The URL to load.
|
|
328
|
+
*/
|
|
329
|
+
async loadURL(url) {
|
|
330
|
+
const res = await this.request("loadURL", `loadURL-reply-${this.id}`, url);
|
|
331
|
+
this.emit("url-loaded", url);
|
|
332
|
+
this.emit("navigated", url);
|
|
333
|
+
return res;
|
|
330
334
|
}
|
|
331
335
|
|
|
332
336
|
/**
|
|
@@ -592,6 +596,15 @@ async request(command, replyChannel, ...args) {
|
|
|
592
596
|
return new Promise((resolve, reject) => {
|
|
593
597
|
let settled = false;
|
|
594
598
|
|
|
599
|
+
if(!command) {
|
|
600
|
+
reject(new Error("Command is required for request"));
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if(!replyChannel) {
|
|
605
|
+
replyChannel = `${command}-reply-${this.id}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
595
608
|
const unsubscribe = ipc.handle(replyChannel, (data) => {
|
|
596
609
|
if (!settled) {
|
|
597
610
|
settled = true;
|
|
@@ -601,15 +614,34 @@ async request(command, replyChannel, ...args) {
|
|
|
601
614
|
}
|
|
602
615
|
});
|
|
603
616
|
|
|
604
|
-
|
|
617
|
+
let timeout;
|
|
618
|
+
|
|
619
|
+
if(!args.includes("NO_TIMEOUT")) {
|
|
620
|
+
let timeoutDuration = 7000;
|
|
621
|
+
|
|
622
|
+
const timeoutArg = args.find(
|
|
623
|
+
arg => typeof arg === "string" && arg.startsWith("TIMEOUT=")
|
|
624
|
+
);
|
|
625
|
+
|
|
626
|
+
if (timeoutArg) {
|
|
627
|
+
|
|
628
|
+
args = args.filter(arg => arg !== timeoutArg);
|
|
629
|
+
|
|
630
|
+
timeoutDuration = parseInt(timeoutArg.split("=")[1], 10);
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
timeout = setTimeout(() => {
|
|
605
634
|
if (!settled) {
|
|
606
635
|
settled = true;
|
|
607
636
|
unsubscribe();
|
|
608
637
|
reject(new Error(`Request timed out waiting for reply on channel "${replyChannel}"`));
|
|
609
638
|
}
|
|
610
|
-
},
|
|
639
|
+
}, timeoutDuration);
|
|
640
|
+
} else {
|
|
641
|
+
args = args.filter(arg => arg !== "NO_TIMEOUT");
|
|
642
|
+
}
|
|
611
643
|
|
|
612
|
-
this.sendCommand(command, args);
|
|
644
|
+
this.sendCommand(command, [...args, replyChannel]);
|
|
613
645
|
});
|
|
614
646
|
}
|
|
615
647
|
|
|
@@ -772,6 +804,135 @@ const res = await this.request("evaluateJS", `evaluateJS-reply-${this.id}`, scri
|
|
|
772
804
|
return res;
|
|
773
805
|
}
|
|
774
806
|
|
|
807
|
+
/**
|
|
808
|
+
* Gets the user agent string of the window. Returns a Promise that resolves to the user agent as a string.
|
|
809
|
+
* @returns {Promise<string>} The user agent string of the window.
|
|
810
|
+
*/
|
|
811
|
+
async getUserAgent() {
|
|
812
|
+
const res = await this.evaluateJavaScript("navigator.userAgent");
|
|
813
|
+
return res;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
/**
|
|
817
|
+
* Sets the style of elements matching a CSS selector. Returns a Promise that resolves when the style has been applied.
|
|
818
|
+
* @param {string} selector The CSS selector for the elements to style.
|
|
819
|
+
* @param {Object} style The style properties to apply.
|
|
820
|
+
* @returns {Promise<void>} A Promise that resolves when the style has been applied.
|
|
821
|
+
*/
|
|
822
|
+
async setStyleOf(selector, style) {
|
|
823
|
+
const styleString = Object.entries(style).map(([key, value]) => `${key}: ${value};`).join(" ");
|
|
824
|
+
const script = `
|
|
825
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
826
|
+
elements.forEach(el => {
|
|
827
|
+
el.style.cssText += ${JSON.stringify(styleString)};
|
|
828
|
+
});
|
|
829
|
+
`;
|
|
830
|
+
await this.evaluateJavaScript(script);
|
|
831
|
+
this.emit("style-updated", { selector, style });
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
/**
|
|
835
|
+
* Sets an attribute of elements matching a CSS selector. Returns a Promise that resolves when the attribute has been set.
|
|
836
|
+
* @param {string} selector The CSS selector for the elements to update.
|
|
837
|
+
* @param {string} attribute The name of the attribute to set.
|
|
838
|
+
* @param {string} value The value to set for the attribute.
|
|
839
|
+
* @returns {Promise<void>} A Promise that resolves when the attribute has been set.
|
|
840
|
+
*/
|
|
841
|
+
async setAttributeOf(selector, attribute, value) {
|
|
842
|
+
const script = `
|
|
843
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
844
|
+
elements.forEach(el => {
|
|
845
|
+
el.setAttribute(${JSON.stringify(attribute)}, ${JSON.stringify(value)});
|
|
846
|
+
});
|
|
847
|
+
`;
|
|
848
|
+
await this.evaluateJavaScript(script);
|
|
849
|
+
this.emit("attribute-updated", { selector, attribute, value });
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* Removes an attribute from elements matching a CSS selector. Returns a Promise that resolves when the attribute has been removed.
|
|
854
|
+
* @param {string} selector The CSS selector for the elements to update.
|
|
855
|
+
* @param {string} attribute The name of the attribute to remove.
|
|
856
|
+
* @returns {Promise<void>} A Promise that resolves when the attribute has been removed.
|
|
857
|
+
*/
|
|
858
|
+
async removeAttributeOf(selector, attribute) {
|
|
859
|
+
const script = `
|
|
860
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
861
|
+
elements.forEach(el => {
|
|
862
|
+
el.removeAttribute(${JSON.stringify(attribute)});
|
|
863
|
+
});
|
|
864
|
+
`;
|
|
865
|
+
await this.evaluateJavaScript(script);
|
|
866
|
+
this.emit("attribute-removed", { selector, attribute });
|
|
867
|
+
}
|
|
868
|
+
|
|
869
|
+
/**
|
|
870
|
+
* Removes specific style properties from elements matching a CSS selector. Returns a Promise that resolves when the styles have been removed.
|
|
871
|
+
* @param {string} selector The CSS selector for the elements to update.
|
|
872
|
+
* @param {string[]} styleProperties The style properties to remove.
|
|
873
|
+
* @returns {Promise<void>} A Promise that resolves when the styles have been removed.
|
|
874
|
+
*/
|
|
875
|
+
async removeStyleOf(selector, styleProperties) {
|
|
876
|
+
const propertiesString = styleProperties.map(prop => `${prop}:`).join("|");
|
|
877
|
+
const script = `
|
|
878
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
879
|
+
elements.forEach(el => {
|
|
880
|
+
el.style.cssText = el.style.cssText.split(";").filter(rule => {
|
|
881
|
+
return !${JSON.stringify(propertiesString)}.includes(rule.trim().split(":")[0] + ":");
|
|
882
|
+
}).join(";");
|
|
883
|
+
});
|
|
884
|
+
`;
|
|
885
|
+
await this.evaluateJavaScript(script);
|
|
886
|
+
this.emit("style-removed", { selector, styleProperties });
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
/**
|
|
890
|
+
* Adds or replaces click handlers that emit IPC events.
|
|
891
|
+
*
|
|
892
|
+
* @param {string} selector - The CSS selector for the elements to attach the click handlers to.
|
|
893
|
+
* @param {string} channel - The IPC channel to emit events on when the elements are clicked.
|
|
894
|
+
* @param {{ replace?: boolean }} [options] - Optional settings for the click handlers. If `replace` is true, any existing IPC click handlers on the elements will be removed before adding the new handler. If false or omitted, the new handler will be added alongside existing handlers without removing them.
|
|
895
|
+
* @returns {Promise<void>} A Promise that resolves when the click handlers have been added.
|
|
896
|
+
*/
|
|
897
|
+
async onClick(selector, channel, { replace = true } = {}) {
|
|
898
|
+
const script = `
|
|
899
|
+
const selector = ${JSON.stringify(selector)};
|
|
900
|
+
const channel = ${JSON.stringify(channel)};
|
|
901
|
+
const replace = ${replace};
|
|
902
|
+
|
|
903
|
+
const elements = document.querySelectorAll(selector);
|
|
904
|
+
|
|
905
|
+
elements.forEach(el => {
|
|
906
|
+
if (replace) {
|
|
907
|
+
el.onclick = () => {
|
|
908
|
+
window.ipc.send(channel, { selector });
|
|
909
|
+
}
|
|
910
|
+
} else {
|
|
911
|
+
el.addEventListener("click", () => {
|
|
912
|
+
window.ipc.send(channel, { selector });
|
|
913
|
+
});
|
|
914
|
+
}
|
|
915
|
+
})
|
|
916
|
+
`;
|
|
917
|
+
|
|
918
|
+
await this.evaluateJavaScript(script);
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
/**
|
|
922
|
+
* Removes click handlers that emit IPC events from elements matching the specified CSS selector. This will remove all click handlers that were added via the onClick method for the given selector, regardless of the channel or whether they were set to replace existing handlers.
|
|
923
|
+
* @param {string} selector The CSS selector for the elements to remove click handlers from.
|
|
924
|
+
* @returns {Promise<void>} A Promise that resolves when the click handlers have been removed.
|
|
925
|
+
*/
|
|
926
|
+
async removeOnClick(selector) {
|
|
927
|
+
const script = `
|
|
928
|
+
const elements = document.querySelectorAll(${JSON.stringify(selector)});
|
|
929
|
+
elements.forEach(el => {
|
|
930
|
+
el.onclick = null;
|
|
931
|
+
});
|
|
932
|
+
`;
|
|
933
|
+
await this.evaluateJavaScript(script);
|
|
934
|
+
}
|
|
935
|
+
|
|
775
936
|
}
|
|
776
937
|
|
|
777
938
|
const app = {
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
"name": "positron.js",
|
|
3
3
|
"description": "A lightweight cross-platform hybrid application framework designed to build desktop applications using a native compiled runtime driven by a Node.js main process.",
|
|
4
4
|
"repository": {
|
|
5
|
-
"url": "https://github.com/systemsoftware/positron
|
|
5
|
+
"url": "https://github.com/systemsoftware/positron.js"
|
|
6
6
|
},
|
|
7
7
|
"homepage": "https://positronjs.gitbook.io",
|
|
8
|
-
"version": "1.0.
|
|
8
|
+
"version": "1.0.2",
|
|
9
9
|
"main": "index.js",
|
|
10
10
|
"scripts": {
|
|
11
11
|
"test": "node --test"
|
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@yao-pkg/pkg": "^6.20.0",
|
|
23
23
|
"esbuild": "^0.28.0",
|
|
24
|
-
"resedit": "^3.0.2",
|
|
25
24
|
"ws": "^8.20.1"
|
|
26
25
|
},
|
|
27
26
|
"bin": {
|