positron.js 1.0.1 → 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.
@@ -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
@@ -329,6 +329,17 @@ private void StartNodeProcess(string workingDirectory)
329
329
  window.Title = webView.CoreWebView2.DocumentTitle;
330
330
  };
331
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
+
332
343
  webView.CoreWebView2.ContextMenuRequested += (s, e) =>
333
344
  {
334
345
  if (LayoutMap.TryGetValue(windowId, out var l) && l.ContextMenu != null)
package/index.js CHANGED
@@ -294,16 +294,6 @@ class Window extends Events.EventEmitter {
294
294
  this.emit("title-updated", title);
295
295
  }
296
296
 
297
- /**
298
- * Loads a remote URL in the window. Emits "url-loaded" and "navigated" events with the URL as data.
299
- * @param {string} url The URL to load.
300
- */
301
- loadURL(url) {
302
- this.sendCommand("loadURL", [url]);
303
- this.emit("url-loaded", url);
304
- this.emit("navigated", url);
305
- }
306
-
307
297
  /**
308
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.
309
299
  */
@@ -322,13 +312,25 @@ class Window extends Events.EventEmitter {
322
312
  }
323
313
 
324
314
  /**
325
- * Loads a local file in the window. Emits "file-loaded" and "navigated" events with the file path as data.
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.
326
316
  * @param {string} path The path to the file to load.
327
317
  */
328
- loadFile(path) {
329
- this.sendCommand("loadFile", [path]);
330
- this.emit("file-loaded", path);
331
- 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;
332
334
  }
333
335
 
334
336
  /**
@@ -802,6 +804,135 @@ const res = await this.request("evaluateJS", `evaluateJS-reply-${this.id}`, scri
802
804
  return res;
803
805
  }
804
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
+
805
936
  }
806
937
 
807
938
  const app = {
package/package.json CHANGED
@@ -5,7 +5,7 @@
5
5
  "url": "https://github.com/systemsoftware/positron.js"
6
6
  },
7
7
  "homepage": "https://positronjs.gitbook.io",
8
- "version": "1.0.1",
8
+ "version": "1.0.2",
9
9
  "main": "index.js",
10
10
  "scripts": {
11
11
  "test": "node --test"