Как можно изменить или переопределить контекстное меню в WKWebView на Mac?

Я использую WKWebView в приложении Mac OS X. Я хочу переопределить контекстное меню, которое появляется, когда пользователь Control щелкает + или щелкает правой кнопкой мыши в WKWebView, но я не могу найти способ сделать это.

Следует отметить, что контекстное меню меняется в зависимости от состояния WKWebView и того, какой элемент находится под мышью при вызове контекстного меню. Например, контекстное меню имеет только один пункт «Обновить», когда мышь находится над «пустой» частью содержимого, тогда как щелчок правой кнопкой мыши по ссылке представляет параметры «Открыть ссылку», «Открыть ссылку в новом окне» и скоро. Было бы полезно иметь детальный контроль над этими различными меню, если это возможно.

Более старый WebUIDelegate предоставляет метод - webView:contextMenuItemsForElement:defaultMenuItems: , который позволяет настраивать контекстное меню для WebView экземпляров; Я по сути ищу аналог этому способу для WKWebView, или любой способ дублирования функционала.


person Justin Michael    schedule 02.03.2015    source источник


Ответы (5)


Вы можете сделать это, перехватив событие contextmenu в вашем javascript, сообщив о событии обратно в ваш контейнер OSX через scriptMessageHandler, а затем открыв меню из 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);
}
person Ken Cooper    schedule 11.03.2015
comment
В iOS WKWebView обработчик contextmenu не вызывается при длительном нажатии. - person adib; 26.09.2015
comment
@adib: да, обработчик контекстного меню не вызывается при длительном нажатии. у вас есть решение? - person Khurshid Ansari; 04.12.2018

Вы можете перехватить элементы контекстного меню класса 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.

person Ely    schedule 23.02.2017
comment
Я добавил некоторые элементы в меню, и соответствующие действия были вызваны нормально. - person AirXygène; 13.06.2018
comment
Я думаю, что это лучший ответ, использующий собственные API вместо хаков на основе JavaScript. - person Greg de J; 17.11.2019
comment
Это было очень полезно, как мне получить URL-адрес, по которому щелкнули правой кнопкой мыши, в этом случае. помощь будет оценена. - person Parminder; 17.07.2020
comment
Я добавил класс, который поможет вам узнать, на что щелкнул пользователь в моем ответе: stackoverflow.com/a/66836354/1711103. Спасибо за ваш ответ, он был очень ценен для меня. - person Andrew Rondeau; 28.03.2021

Следуя уже данным ответам, я смог изменить меню, а также нашел способ получить 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)
    }
}
person vauxhall    schedule 26.12.2020
comment
Почему-то я думаю, что если каждый раз, когда вы щелкаете правой кнопкой мыши по странице, она перезаписывает то, что находится на монтажном столе, пользователи будут очень злы. Я знаю, что поставил бы 1 звезду приложению, которое делает такие махинации - person Andrew Rondeau; 23.03.2021

Целевое решение 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
person Cliff Ribaudo    schedule 30.01.2021

Этот ответ основан на отличных ответах в этой теме.

Проблемы при работе с контекстным меню 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)
}

Наконец, (как указывалось в других ответах) вы переопределяете обработчик контекстного меню. В этом случае я изменил действие в target для пункта меню Open 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))
    }
}
person Andrew Rondeau    schedule 27.03.2021