|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+//
|
|
|
2
|
+// IndeedJobBrowserWindowController.swift
|
|
|
3
|
+// App for Indeed
|
|
|
4
|
+//
|
|
|
5
|
+
|
|
|
6
|
+import Cocoa
|
|
|
7
|
+import WebKit
|
|
|
8
|
+
|
|
|
9
|
+/// Indeed job listing and apply flow in a `WKWebView`, embedded in the dashboard main panel or hosted in a window.
|
|
|
10
|
+final class IndeedJobBrowserViewController: NSViewController, WKNavigationDelegate, WKUIDelegate {
|
|
|
11
|
+ /// When set, a leading **Done** control calls this so the host can hide the embedded browser (same-window UX).
|
|
|
12
|
+ var onDismissEmbedded: (() -> Void)?
|
|
|
13
|
+ private let webView: WKWebView = {
|
|
|
14
|
+ let configuration = WKWebViewConfiguration()
|
|
|
15
|
+ configuration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
|
16
|
+ return WKWebView(frame: .zero, configuration: configuration)
|
|
|
17
|
+ }()
|
|
|
18
|
+
|
|
|
19
|
+ private var pendingURL: URL?
|
|
|
20
|
+
|
|
|
21
|
+ private let backButton = NSButton()
|
|
|
22
|
+ private let forwardButton = NSButton()
|
|
|
23
|
+ private let reloadButton = NSButton()
|
|
|
24
|
+ private let openExternallyButton = NSButton(title: "Open in Browser", target: nil, action: nil)
|
|
|
25
|
+ private let dismissEmbeddedButton = NSButton(title: "Done", target: nil, action: nil)
|
|
|
26
|
+
|
|
|
27
|
+ override func loadView() {
|
|
|
28
|
+ view = NSView(frame: NSRect(x: 0, y: 0, width: 920, height: 720))
|
|
|
29
|
+ }
|
|
|
30
|
+
|
|
|
31
|
+ override func viewDidLoad() {
|
|
|
32
|
+ super.viewDidLoad()
|
|
|
33
|
+ webView.translatesAutoresizingMaskIntoConstraints = false
|
|
|
34
|
+ webView.navigationDelegate = self
|
|
|
35
|
+ webView.uiDelegate = self
|
|
|
36
|
+ webView.customUserAgent = Self.desktopSafariLikeUserAgent
|
|
|
37
|
+
|
|
|
38
|
+ configureToolbarButton(backButton, symbolName: "chevron.backward", action: #selector(goBack))
|
|
|
39
|
+ configureToolbarButton(forwardButton, symbolName: "chevron.forward", action: #selector(goForward))
|
|
|
40
|
+ configureToolbarButton(reloadButton, symbolName: "arrow.clockwise", action: #selector(reload))
|
|
|
41
|
+
|
|
|
42
|
+ openExternallyButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
43
|
+ openExternallyButton.bezelStyle = .rounded
|
|
|
44
|
+ openExternallyButton.isBordered = true
|
|
|
45
|
+ openExternallyButton.target = self
|
|
|
46
|
+ openExternallyButton.action = #selector(openInDefaultBrowser)
|
|
|
47
|
+ openExternallyButton.toolTip = "Open this page in your default web browser"
|
|
|
48
|
+
|
|
|
49
|
+ dismissEmbeddedButton.translatesAutoresizingMaskIntoConstraints = false
|
|
|
50
|
+ dismissEmbeddedButton.bezelStyle = .rounded
|
|
|
51
|
+ dismissEmbeddedButton.isBordered = true
|
|
|
52
|
+ dismissEmbeddedButton.target = self
|
|
|
53
|
+ dismissEmbeddedButton.action = #selector(dismissEmbedded)
|
|
|
54
|
+ dismissEmbeddedButton.toolTip = "Return to the previous screen"
|
|
|
55
|
+
|
|
|
56
|
+ let toolbar = NSView()
|
|
|
57
|
+ toolbar.translatesAutoresizingMaskIntoConstraints = false
|
|
|
58
|
+ toolbar.wantsLayer = true
|
|
|
59
|
+ toolbar.layer?.backgroundColor = NSColor(srgbRed: 247 / 255, green: 247 / 255, blue: 247 / 255, alpha: 1).cgColor
|
|
|
60
|
+
|
|
|
61
|
+ let barStack: NSStackView
|
|
|
62
|
+ if onDismissEmbedded != nil {
|
|
|
63
|
+ barStack = NSStackView(views: [dismissEmbeddedButton, backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
|
|
|
64
|
+ } else {
|
|
|
65
|
+ barStack = NSStackView(views: [backButton, forwardButton, reloadButton, NSView(), openExternallyButton])
|
|
|
66
|
+ }
|
|
|
67
|
+ barStack.orientation = .horizontal
|
|
|
68
|
+ barStack.spacing = 8
|
|
|
69
|
+ barStack.alignment = .centerY
|
|
|
70
|
+ barStack.distribution = .fill
|
|
|
71
|
+ barStack.translatesAutoresizingMaskIntoConstraints = false
|
|
|
72
|
+
|
|
|
73
|
+ toolbar.addSubview(barStack)
|
|
|
74
|
+ view.addSubview(toolbar)
|
|
|
75
|
+ view.addSubview(webView)
|
|
|
76
|
+
|
|
|
77
|
+ var layoutConstraints: [NSLayoutConstraint] = [
|
|
|
78
|
+ toolbar.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
79
|
+ toolbar.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
80
|
+ toolbar.topAnchor.constraint(equalTo: view.topAnchor),
|
|
|
81
|
+ toolbar.heightAnchor.constraint(equalToConstant: 48),
|
|
|
82
|
+
|
|
|
83
|
+ barStack.leadingAnchor.constraint(equalTo: toolbar.leadingAnchor, constant: 12),
|
|
|
84
|
+ barStack.trailingAnchor.constraint(equalTo: toolbar.trailingAnchor, constant: -12),
|
|
|
85
|
+ barStack.centerYAnchor.constraint(equalTo: toolbar.centerYAnchor),
|
|
|
86
|
+
|
|
|
87
|
+ backButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
88
|
+ backButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
89
|
+ forwardButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
90
|
+ forwardButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
91
|
+ reloadButton.widthAnchor.constraint(equalToConstant: 32),
|
|
|
92
|
+ reloadButton.heightAnchor.constraint(equalToConstant: 28),
|
|
|
93
|
+
|
|
|
94
|
+ webView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
|
95
|
+ webView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
|
96
|
+ webView.topAnchor.constraint(equalTo: toolbar.bottomAnchor),
|
|
|
97
|
+ webView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
|
|
98
|
+ ]
|
|
|
99
|
+ if onDismissEmbedded != nil {
|
|
|
100
|
+ layoutConstraints.append(dismissEmbeddedButton.heightAnchor.constraint(equalToConstant: 28))
|
|
|
101
|
+ }
|
|
|
102
|
+ NSLayoutConstraint.activate(layoutConstraints)
|
|
|
103
|
+
|
|
|
104
|
+ updateNavigationButtons()
|
|
|
105
|
+
|
|
|
106
|
+ if let pendingURL {
|
|
|
107
|
+ webView.load(URLRequest(url: pendingURL))
|
|
|
108
|
+ self.pendingURL = nil
|
|
|
109
|
+ }
|
|
|
110
|
+ }
|
|
|
111
|
+
|
|
|
112
|
+ func loadPage(_ url: URL) {
|
|
|
113
|
+ if isViewLoaded {
|
|
|
114
|
+ webView.load(URLRequest(url: url))
|
|
|
115
|
+ } else {
|
|
|
116
|
+ pendingURL = url
|
|
|
117
|
+ }
|
|
|
118
|
+ updateNavigationButtons()
|
|
|
119
|
+ }
|
|
|
120
|
+
|
|
|
121
|
+ /// Adds this controller as a child of `parent` and pins `view` to `host` (used by the dashboard main panel).
|
|
|
122
|
+ func embed(in host: NSView, parent: NSViewController) {
|
|
|
123
|
+ parent.addChild(self)
|
|
|
124
|
+ view.translatesAutoresizingMaskIntoConstraints = false
|
|
|
125
|
+ host.addSubview(view)
|
|
|
126
|
+ NSLayoutConstraint.activate([
|
|
|
127
|
+ view.leadingAnchor.constraint(equalTo: host.leadingAnchor),
|
|
|
128
|
+ view.trailingAnchor.constraint(equalTo: host.trailingAnchor),
|
|
|
129
|
+ view.topAnchor.constraint(equalTo: host.topAnchor),
|
|
|
130
|
+ view.bottomAnchor.constraint(equalTo: host.bottomAnchor)
|
|
|
131
|
+ ])
|
|
|
132
|
+ }
|
|
|
133
|
+
|
|
|
134
|
+ private func configureToolbarButton(_ button: NSButton, symbolName: String, action: Selector) {
|
|
|
135
|
+ button.translatesAutoresizingMaskIntoConstraints = false
|
|
|
136
|
+ button.bezelStyle = .texturedRounded
|
|
|
137
|
+ button.isBordered = true
|
|
|
138
|
+ button.image = NSImage(systemSymbolName: symbolName, accessibilityDescription: nil)
|
|
|
139
|
+ button.imagePosition = .imageOnly
|
|
|
140
|
+ button.target = self
|
|
|
141
|
+ button.action = action
|
|
|
142
|
+ }
|
|
|
143
|
+
|
|
|
144
|
+ private func updateNavigationButtons() {
|
|
|
145
|
+ backButton.isEnabled = webView.canGoBack
|
|
|
146
|
+ forwardButton.isEnabled = webView.canGoForward
|
|
|
147
|
+ }
|
|
|
148
|
+
|
|
|
149
|
+ @objc private func goBack() {
|
|
|
150
|
+ webView.goBack()
|
|
|
151
|
+ }
|
|
|
152
|
+
|
|
|
153
|
+ @objc private func goForward() {
|
|
|
154
|
+ webView.goForward()
|
|
|
155
|
+ }
|
|
|
156
|
+
|
|
|
157
|
+ @objc private func reload() {
|
|
|
158
|
+ webView.reload()
|
|
|
159
|
+ }
|
|
|
160
|
+
|
|
|
161
|
+ @objc private func openInDefaultBrowser() {
|
|
|
162
|
+ guard let url = webView.url else { return }
|
|
|
163
|
+ NSWorkspace.shared.open(url)
|
|
|
164
|
+ }
|
|
|
165
|
+
|
|
|
166
|
+ @objc private func dismissEmbedded() {
|
|
|
167
|
+ onDismissEmbedded?()
|
|
|
168
|
+ }
|
|
|
169
|
+
|
|
|
170
|
+ func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) {
|
|
|
171
|
+ updateNavigationButtons()
|
|
|
172
|
+ }
|
|
|
173
|
+
|
|
|
174
|
+ func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
|
|
|
175
|
+ updateNavigationButtons()
|
|
|
176
|
+ }
|
|
|
177
|
+
|
|
|
178
|
+ /// Target=_blank / `window.open` without a frame: load in this view so apply flows stay in-app.
|
|
|
179
|
+ func webView(_ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures) -> WKWebView? {
|
|
|
180
|
+ if navigationAction.targetFrame == nil {
|
|
|
181
|
+ webView.load(navigationAction.request)
|
|
|
182
|
+ }
|
|
|
183
|
+ return nil
|
|
|
184
|
+ }
|
|
|
185
|
+
|
|
|
186
|
+ /// Desktop Safari UA helps Indeed serve a full desktop apply experience.
|
|
|
187
|
+ private static let desktopSafariLikeUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.4 Safari/605.1.15"
|
|
|
188
|
+}
|