【问题标题】:How can the context menu in WKWebView on the Mac be modified or overridden?Mac上的WKWebView中的上下文菜单如何修改或覆盖?
【发布时间】:2015-03-02 00:29:34
【问题描述】:

我在 Mac OS X 应用程序中使用WKWebView。我想覆盖当用户 Control + 单击或右键单击 WKWebView 时出现的上下文菜单,但我找不到实现此目的的方法。

需要注意的是,上下文菜单的变化取决于WKWebView的状态以及调用上下文菜单时鼠标在什么元素下。例如,当鼠标悬停在内容的“空白”部分时,上下文菜单只有一个“重新加载”项,而右键单击链接会显示选项“打开链接”、“在新窗口中打开链接”和很快。如果可能的话,对这些不同的菜单进行精细控制会很有帮助。

较旧的WebUIDelegate 提供- webView:contextMenuItemsForElement:defaultMenuItems: 方法,允许您自定义WebView 实例的上下文菜单;我本质上是在为WKWebView 寻找这种方法的类似物,或任何复制功能的方式。

【问题讨论】:

    标签: macos osx-yosemite wkwebview


    【解决方案1】:

    您可以通过在您的 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];
    

    使用 jquery 的 Javascript 代码:

    $(nodeId).on("contextmenu", function (evt) {
       window.webkit.messageHandlers.callbackHandler.postMessage({body: "..."});
       evt.preventDefault();
    });
    

    从目标 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);
    }
    

    【讨论】:

    • 在 iOS 的WKWebView 上,contextmenu 处理程序不会在长按时被调用。
    • @adib :是的,上下文菜单处理程序不会在长按时被调用。你有什么解决办法吗?
    【解决方案2】:

    你可以截取 WKWebView 类的上下文菜单项,方法是继承它并实现 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 数组。

    【讨论】:

    • 我确实在菜单中添加了一些项目,并且相关操作被调用。
    • 我认为这是最好的答案,使用原生 API 而不是基于 JavaScript 的 hack。
    • 这很有帮助,在这种情况下,我将如何获得被右键单击的 url。帮助将不胜感激。
    • 我添加了一个类,可以帮助您了解用户在我的回答中点击了什么:stackoverflow.com/a/66836354/1711103。谢谢你的回答,对我来说很有价值。
    【解决方案3】:

    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
    

    【讨论】:

      【解决方案4】:

      按照已经给出的答案,我能够修改菜单,还找到了一种获取用户选择的 URL 的方法。我想这种方法也可以用来选择图像或任何其他类似的内容,我希望这可以帮助其他人。 这是使用 Swift 5 编写的

      此方法包括从菜单项“复制链接”执行操作,以便将 URL 复制到粘贴板,然后从粘贴板检索 URL 以在新菜单项上使用它。

      注意:从粘贴板检索 URL 需要在异步闭包上调用,以便有时间先将 URL 复制到其中。

      final class WebView: WKWebView {
      
          override func willOpenMenu(_ menu: NSMenu, with: NSEvent) {
              menu.items.first { $0.identifier?.rawValue == "WKMenuItemIdentifierCopyLink" }.map {
                  
                  guard let action = $0.action else { return }
                  
                  NSApp.sendAction(action, to: $0.target, from: $0)
                  
                  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)
          }
      }
      

      【讨论】:

      • 不知何故,我认为如果每次右键单击页面时,它都会覆盖粘贴板中的内容,用户会非常生气。我知道我会对这样的恶作剧应用程序进行 1 星评价
      【解决方案5】:

      此答案建立在此线程中的出色答案之上。

      使用 WKWebView 的上下文菜单的挑战是:

      • 只能在 WKWebView 的子类中操作
      • WebKit 不会公开有关用户右键单击的 HTML 元素的任何信息。因此,关于元素的信息必须在 JavaScript 中被截获并返回到 Swift 中。

      通过在渲染之前将 JavaScript 注入页面,然后在 Swift 中建立回调来拦截和查找有关用户单击的元素的信息。这是我为此编写的课程。它适用于 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 中启用此功能,您必须对其进行子类化并在构造函数中调用 GlobalScriptMessageHandler.instance.ensureHandles:

      class WebView: WKWebView {
      
      public var webViewDelegate: WebViewDelegate?
      
      init() {
          super.init(frame: CGRect(), configuration: WKWebViewConfiguration())
          GlobalScriptMessageHandler.instance.ensureHandles(configuration: self.configuration)
      }
      

      最后,(正如其他答案所指出的那样)您覆盖上下文菜单处理程序。在这种情况下,我更改了“打开链接”菜单项的目标操作。您可以根据需要更改它们:

          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))
          }
      }
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2015-09-17
        • 2011-11-13
        • 2013-10-09
        • 1970-01-01
        • 1970-01-01
        • 2011-09-24
        相关资源
        最近更新 更多