如何修改或覆盖 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))
    }
}