如何修改或覆盖 Mac 上 WKWebView 的上下文菜单?
How can the context menu in WKWebView on the Mac be modified or overridden?
我在 Mac OS X 应用程序中使用 WKWebView
。我想覆盖当用户 Control + 在 WKWebView
中单击或右键单击时出现的上下文菜单,但我找不到完成此操作的方法。
应该注意的是,上下文菜单会根据 WKWebView 的状态以及调用上下文菜单时鼠标下方的元素而变化。例如,当鼠标悬停在内容的 "empty" 部分时,上下文菜单只有一个 "Reload" 项目,而右键单击 link 会显示选项 "Open Link", "Open Link In New Window",依此类推。如果可能的话,对这些不同的菜单进行精细控制会很有帮助。
较早的 WebUIDelegate
提供了 - webView:contextMenuItemsForElement:defaultMenuItems:
方法,允许您自定义 WebView
实例的上下文菜单;我本质上是在为 WKWebView
寻找与此方法类似的方法,或任何复制功能的方法。
您可以通过拦截 javascript 中的 contextmenu
事件,通过 scriptMessageHandler 将事件报告回 OSX 容器,然后从 OSX。您可以通过脚本消息的正文字段传回上下文以显示适当的菜单,或者为每个菜单使用不同的处理程序。
在 Objective C 中设置回调处理程序:
WKUserContentController *contentController = [[WKUserContentController alloc]init];
[contentController addScriptMessageHandler:self name:@"callbackHandler"];
config.userContentController = contentController;
self.mainWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
Javascript 使用 jquery 的代码:
$(nodeId).on("contextmenu", function (evt) {
window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
evt.preventDefault();
});
来自Objective C的回应:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"callbackHandler"]) {
[self popupMenu:message.body];
}
}
-(void)popupMenu:(NSString *)context {
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@"Context Menu"];
[theMenu insertItemWithTitle:@"Beep" action:@selector(beep:) keyEquivalent:@"" atIndex:0];
[theMenu insertItemWithTitle:@"Honk" action:@selector(honk:) keyEquivalent:@"" atIndex:1];
[theMenu popUpMenuPositioningItem:theMenu.itemArray[0] atLocation:NSPointFromCGPoint(CGPointMake(0,0)) inView:self.view];
}
-(void)beep:(id)val {
NSLog(@"got beep %@", val);
}
-(void)honk:(id)val {
NSLog(@"got honk %@", val);
}
您可以拦截 WKWebView class 的上下文菜单项,方法是子class 并实现 willOpenMenu 方法,如下所示:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(_:))
menuItem.target = self
}
}
}
@objc func menuClick(_ sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
除此之外,您还可以使用 menuItem.isHidden = true
简单地隐藏菜单项
检测所选择的菜单项是一回事,但了解用户在 WKWebView 控件中实际点击了什么是下一个挑战:)
也可以向 menu.items
数组添加新的菜单项。
根据已经给出的答案,我能够修改菜单并找到一种方法来获取用户选择的 URL。我想这种方法也可以用来选择图像或任何其他类似的内容,我希望这可以帮助其他人。
这是使用 Swift 5
编写的
此方法包括执行菜单项“复制 Link”中的操作,以便将 URL 复制到粘贴板中,然后从粘贴板以在新菜单项上使用它。
注意:从粘贴板中检索 URL 需要在异步闭包上调用,以便有时间将 URL 首先复制到其中。
final class WebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
menu.items.first { [=10=].identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
guard let action = [=10=].action else { return }
NSApp.sendAction(action, to: [=10=].target, from: [=10=])
DispatchQueue.main.async { [weak self] in
let newTab = NSMenuItem(title: "Open Link in New Tab", action: #selector(self?.openInNewTab), keyEquivalent: "")
newTab.target = self
newTab.representedObject = NSPasteboard.general.string(forType: .string)
menu.items.append(newTab)
}
}
}
@objc private func openInNewTab(_ item: NSMenuItem) {
print(item.representedObject as? String)
}
}
Objective C解决方案。最好的解决方案是继承 WKWebView 并拦截鼠标点击。效果很好。
@implementation WKReportWebView
// Ctrl+click seems to send this not rightMouse
-(void)mouseDown:(NSEvent *)event
{
if(event.modifierFlags & NSEventModifierFlagControl)
return [self rightMouseDown:event];
[super mouseDown:event]; // Catch scrollbar mouse events
}
-(void)rightMouseDown:(NSEvent *)theEvent
{
NSMenu *rightClickMenu = [[NSMenu alloc] initWithTitle:@"Print Menu"];
[rightClickMenu insertItemWithTitle:NSLocalizedString(@"Print", nil) action:@selector(print:) keyEquivalent:@"" atIndex:0];
[NSMenu popUpContextMenu:rightClickMenu withEvent:theEvent forView:self];
}
@end
此答案基于此话题中的优秀答案。
使用 WKWebView 上下文菜单的挑战是:
- 只能在WKWebView的子class中操作
- WebKit 不会公开有关用户右键单击的 HTML 元素的任何信息。因此,有关元素的信息必须在 JavaScript 中截取并返回到 Swift.
通过在呈现之前将 JavaScript 注入页面,然后在 Swift 中建立回调来拦截和查找有关用户单击的元素的信息。这是我为此编写的 class。它适用于 WKWebView 的配置对象。它还假定一次只有一个上下文菜单可用:
class GlobalScriptMessageHandler: NSObject, WKScriptMessageHandler {
public private(set) static var instance = GlobalScriptMessageHandler()
public private(set) var contextMenu_nodeName: String?
public private(set) var contextMenu_nodeId: String?
public private(set) var contextMenu_hrefNodeName: String?
public private(set) var contextMenu_hrefNodeId: String?
public private(set) var contextMenu_href: String?
static private var WHOLE_PAGE_SCRIPT = """
window.oncontextmenu = (event) => {
var target = event.target
var href = target.href
var parentElement = target
while (href == null && parentElement.parentElement != null) {
parentElement = parentElement.parentElement
href = parentElement.href
}
if (href == null) {
parentElement = null;
}
window.webkit.messageHandlers.oncontextmenu.postMessage({
nodeName: target.nodeName,
id: target.id,
hrefNodeName: parentElement?.nodeName,
hrefId: parentElement?.id,
href
});
}
"""
private override init() {
super.init()
}
public func ensureHandles(configuration: WKWebViewConfiguration) {
var alreadyHandling = false
for userScript in configuration.userContentController.userScripts {
if userScript.source == GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT {
alreadyHandling = true
}
}
if !alreadyHandling {
let userContentController = configuration.userContentController
userContentController.add(self, name: "oncontextmenu")
let userScript = WKUserScript(source: GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? NSDictionary {
contextMenu_nodeName = body["nodeName"] as? String
contextMenu_nodeId = body["id"] as? String
contextMenu_hrefNodeName = body["hrefNodeName"] as? String
contextMenu_hrefNodeId = body["hrefId"] as? String
contextMenu_href = body["href"] as? String
}
}
接下来,要在您的 WKWebView 中启用它,您必须子class它并在您的构造函数中调用 GlobalScriptMessageHandler.instance.ensureHandles:
class WebView: WKWebView {
public var webViewDelegate: WebViewDelegate?
init() {
super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
}
最后,(正如其他答案所指出的那样),您重写了上下文菜单处理程序。在这种情况下,我更改了目标中“打开 Link”菜单项的操作。您可以根据需要更改它们:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for index in 0...(menu.items.count - 1) {
let menuItem = menu.items[index]
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" {
menuItem.action = #selector(openLink(_:))
menuItem.target = self
然后,在你处理菜单项的方法中,使用GlobalScriptMessageHandler.instance.contextMenu_href得到用户右击的URL:
@objc func openLink(_ sender: AnyObject) {
if let url = GlobalScriptMessageHandler.instance.contextMenu_href {
let url = URL(string: url)!
self.load(URLRequest(url: url))
}
}
我在 Mac OS X 应用程序中使用 WKWebView
。我想覆盖当用户 Control + 在 WKWebView
中单击或右键单击时出现的上下文菜单,但我找不到完成此操作的方法。
应该注意的是,上下文菜单会根据 WKWebView 的状态以及调用上下文菜单时鼠标下方的元素而变化。例如,当鼠标悬停在内容的 "empty" 部分时,上下文菜单只有一个 "Reload" 项目,而右键单击 link 会显示选项 "Open Link", "Open Link In New Window",依此类推。如果可能的话,对这些不同的菜单进行精细控制会很有帮助。
较早的 WebUIDelegate
提供了 - webView:contextMenuItemsForElement:defaultMenuItems:
方法,允许您自定义 WebView
实例的上下文菜单;我本质上是在为 WKWebView
寻找与此方法类似的方法,或任何复制功能的方法。
您可以通过拦截 javascript 中的 contextmenu
事件,通过 scriptMessageHandler 将事件报告回 OSX 容器,然后从 OSX。您可以通过脚本消息的正文字段传回上下文以显示适当的菜单,或者为每个菜单使用不同的处理程序。
在 Objective C 中设置回调处理程序:
WKUserContentController *contentController = [[WKUserContentController alloc]init];
[contentController addScriptMessageHandler:self name:@"callbackHandler"];
config.userContentController = contentController;
self.mainWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:config];
Javascript 使用 jquery 的代码:
$(nodeId).on("contextmenu", function (evt) {
window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
evt.preventDefault();
});
来自Objective C的回应:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"callbackHandler"]) {
[self popupMenu:message.body];
}
}
-(void)popupMenu:(NSString *)context {
NSMenu *theMenu = [[NSMenu alloc] initWithTitle:@"Context Menu"];
[theMenu insertItemWithTitle:@"Beep" action:@selector(beep:) keyEquivalent:@"" atIndex:0];
[theMenu insertItemWithTitle:@"Honk" action:@selector(honk:) keyEquivalent:@"" atIndex:1];
[theMenu popUpMenuPositioningItem:theMenu.itemArray[0] atLocation:NSPointFromCGPoint(CGPointMake(0,0)) inView:self.view];
}
-(void)beep:(id)val {
NSLog(@"got beep %@", val);
}
-(void)honk:(id)val {
NSLog(@"got honk %@", val);
}
您可以拦截 WKWebView class 的上下文菜单项,方法是子class 并实现 willOpenMenu 方法,如下所示:
class MyWebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for menuItem in menu.items {
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadImage" ||
menuItem.identifier?.rawValue == "WKMenuItemIdentifierDownloadLinkedFile" {
menuItem.action = #selector(menuClick(_:))
menuItem.target = self
}
}
}
@objc func menuClick(_ sender: AnyObject) {
if let menuItem = sender as? NSMenuItem {
Swift.print("Menu \(menuItem.title) clicked")
}
}
}
除此之外,您还可以使用 menuItem.isHidden = true
检测所选择的菜单项是一回事,但了解用户在 WKWebView 控件中实际点击了什么是下一个挑战:)
也可以向 menu.items
数组添加新的菜单项。
根据已经给出的答案,我能够修改菜单并找到一种方法来获取用户选择的 URL。我想这种方法也可以用来选择图像或任何其他类似的内容,我希望这可以帮助其他人。 这是使用 Swift 5
编写的此方法包括执行菜单项“复制 Link”中的操作,以便将 URL 复制到粘贴板中,然后从粘贴板以在新菜单项上使用它。
注意:从粘贴板中检索 URL 需要在异步闭包上调用,以便有时间将 URL 首先复制到其中。
final class WebView: WKWebView {
override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
menu.items.first { [=10=].identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
guard let action = [=10=].action else { return }
NSApp.sendAction(action, to: [=10=].target, from: [=10=])
DispatchQueue.main.async { [weak self] in
let newTab = NSMenuItem(title: "Open Link in New Tab", action: #selector(self?.openInNewTab), keyEquivalent: "")
newTab.target = self
newTab.representedObject = NSPasteboard.general.string(forType: .string)
menu.items.append(newTab)
}
}
}
@objc private func openInNewTab(_ item: NSMenuItem) {
print(item.representedObject as? String)
}
}
Objective C解决方案。最好的解决方案是继承 WKWebView 并拦截鼠标点击。效果很好。
@implementation WKReportWebView
// Ctrl+click seems to send this not rightMouse
-(void)mouseDown:(NSEvent *)event
{
if(event.modifierFlags & NSEventModifierFlagControl)
return [self rightMouseDown:event];
[super mouseDown:event]; // Catch scrollbar mouse events
}
-(void)rightMouseDown:(NSEvent *)theEvent
{
NSMenu *rightClickMenu = [[NSMenu alloc] initWithTitle:@"Print Menu"];
[rightClickMenu insertItemWithTitle:NSLocalizedString(@"Print", nil) action:@selector(print:) keyEquivalent:@"" atIndex:0];
[NSMenu popUpContextMenu:rightClickMenu withEvent:theEvent forView:self];
}
@end
此答案基于此话题中的优秀答案。
使用 WKWebView 上下文菜单的挑战是:
- 只能在WKWebView的子class中操作
- WebKit 不会公开有关用户右键单击的 HTML 元素的任何信息。因此,有关元素的信息必须在 JavaScript 中截取并返回到 Swift.
通过在呈现之前将 JavaScript 注入页面,然后在 Swift 中建立回调来拦截和查找有关用户单击的元素的信息。这是我为此编写的 class。它适用于 WKWebView 的配置对象。它还假定一次只有一个上下文菜单可用:
class GlobalScriptMessageHandler: NSObject, WKScriptMessageHandler {
public private(set) static var instance = GlobalScriptMessageHandler()
public private(set) var contextMenu_nodeName: String?
public private(set) var contextMenu_nodeId: String?
public private(set) var contextMenu_hrefNodeName: String?
public private(set) var contextMenu_hrefNodeId: String?
public private(set) var contextMenu_href: String?
static private var WHOLE_PAGE_SCRIPT = """
window.oncontextmenu = (event) => {
var target = event.target
var href = target.href
var parentElement = target
while (href == null && parentElement.parentElement != null) {
parentElement = parentElement.parentElement
href = parentElement.href
}
if (href == null) {
parentElement = null;
}
window.webkit.messageHandlers.oncontextmenu.postMessage({
nodeName: target.nodeName,
id: target.id,
hrefNodeName: parentElement?.nodeName,
hrefId: parentElement?.id,
href
});
}
"""
private override init() {
super.init()
}
public func ensureHandles(configuration: WKWebViewConfiguration) {
var alreadyHandling = false
for userScript in configuration.userContentController.userScripts {
if userScript.source == GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT {
alreadyHandling = true
}
}
if !alreadyHandling {
let userContentController = configuration.userContentController
userContentController.add(self, name: "oncontextmenu")
let userScript = WKUserScript(source: GlobalScriptMessageHandler.WHOLE_PAGE_SCRIPT, injectionTime: .atDocumentStart, forMainFrameOnly: false)
userContentController.addUserScript(userScript)
}
}
func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
if let body = message.body as? NSDictionary {
contextMenu_nodeName = body["nodeName"] as? String
contextMenu_nodeId = body["id"] as? String
contextMenu_hrefNodeName = body["hrefNodeName"] as? String
contextMenu_hrefNodeId = body["hrefId"] as? String
contextMenu_href = body["href"] as? String
}
}
接下来,要在您的 WKWebView 中启用它,您必须子class它并在您的构造函数中调用 GlobalScriptMessageHandler.instance.ensureHandles:
class WebView: WKWebView {
public var webViewDelegate: WebViewDelegate?
init() {
super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
}
最后,(正如其他答案所指出的那样),您重写了上下文菜单处理程序。在这种情况下,我更改了目标中“打开 Link”菜单项的操作。您可以根据需要更改它们:
override func willOpenMenu(_ menu: NSMenu, with event: NSEvent) {
for index in 0...(menu.items.count - 1) {
let menuItem = menu.items[index]
if menuItem.identifier?.rawValue == "WKMenuItemIdentifierOpenLink" {
menuItem.action = #selector(openLink(_:))
menuItem.target = self
然后,在你处理菜单项的方法中,使用GlobalScriptMessageHandler.instance.contextMenu_href得到用户右击的URL:
@objc func openLink(_ sender: AnyObject) {
if let url = GlobalScriptMessageHandler.instance.contextMenu_href {
let url = URL(string: url)!
self.load(URLRequest(url: url))
}
}