[iOS] Add JavaScript dialog delegate to WkWebView (#4254)

* basic implementation of js alerts for wkwebview

* add ui test and adjust to mimic uiwebview alerts

* fix proj file

* rename navigation delegate

* Add WkWebView Gallery

* input cancel should return false; dedup logic

* Load correct galley for WkWebView

Co-Authored-By: alanag13 <alan.grgic@gmail.com>

* ensure wkwebview and webview galleries render similarly

* corrections
- Fixes #4253
This commit is contained in:
Alan Grgic 2018-11-20 15:29:16 -06:00 коммит произвёл Shane Neuville
Родитель 288c73d3f4
Коммит 63b371422d
6 изменённых файлов: 272 добавлений и 8 удалений

Просмотреть файл

@ -42,3 +42,4 @@ using Xamarin.Forms.Controls;
// Deliberately broken image source and handler so we can test handling of image loading errors
[assembly: ExportImageSourceHandler(typeof(FailImageSource), typeof(BrokenImageSourceHandler))]
[assembly: ExportRenderer(typeof(WkWebView), typeof(Xamarin.Forms.Platform.iOS.WkWebViewRenderer))]

Просмотреть файл

@ -29,6 +29,10 @@ namespace Xamarin.Forms.Controls.Issues
var buttonState = new Button() { Text = "?", BackgroundColor = Color.LightBlue, AutomationId = "buttonState" };
var buttonStop = new Button() { Text = "X", BackgroundColor = Color.LightBlue, AutomationId = "buttonStop" };
var buttonJsAlert = new Button() { Text = "ALERT", BackgroundColor = Color.LightBlue, AutomationId = "buttonJsAlert" };
var buttonJsPrompt = new Button() { Text = "PROMPT", BackgroundColor = Color.LightBlue, AutomationId = "buttonJsPrompt" };
var buttonJsConfirm = new Button() { Text = "CONFIRM", BackgroundColor = Color.LightBlue, AutomationId = "buttonJsConfirm" };
var buttonA = new Button() { Text = "GO", BackgroundColor = Color.LightBlue, AutomationId = "buttonA" };
var buttonB = new Button() { Text = "HTML", BackgroundColor = Color.LightBlue, AutomationId = "buttonB" };
var buttonC = new Button() { Text = "EVAL", BackgroundColor = Color.LightBlue, AutomationId = "buttonC" };
@ -52,9 +56,13 @@ namespace Xamarin.Forms.Controls.Issues
var entry = new Entry() { AutomationId = "entry" };
entry.BackgroundColor = Color.Wheat;
var jsAlerts = new Grid();
jsAlerts.Children.AddHorizontal(new[] { buttonJsAlert, buttonJsPrompt, buttonJsConfirm });
var buttons = new Grid();
buttons.Children.AddVertical(vcr);
buttons.Children.AddVertical(evals);
buttons.Children.AddVertical(jsAlerts);
buttons.Children.AddVertical(entry);
var console = new Label()
@ -108,6 +116,10 @@ namespace Xamarin.Forms.Controls.Issues
log($"Source: {webView.Source.ToString()}");
};
buttonJsAlert.Clicked += async (s, e) => { await webView.EvaluateJavaScriptAsync("alert('foo')"); };
buttonJsPrompt.Clicked += async (s, e) => { log($"{await webView.EvaluateJavaScriptAsync("prompt('enter something:')")} was enterred"); };
buttonJsConfirm.Clicked += async (s, e) => { log($"{await webView.EvaluateJavaScriptAsync("confirm('choose')")} was chosen"); };
bool navigating = false;
webView.Navigating += (s, e) =>
{

Просмотреть файл

@ -324,6 +324,7 @@ namespace Xamarin.Forms.Controls
new GalleryPageFactory(() => new TableViewCoreGalleryPage(), "TableView Gallery"),
new GalleryPageFactory(() => new TimePickerCoreGalleryPage(), "TimePicker Gallery"),
new GalleryPageFactory(() => new WebViewCoreGalleryPage(), "WebView Gallery"),
new GalleryPageFactory(() => new WkWebViewCoreGalleryPage(), "WkWebView Gallery"),
//pages
new GalleryPageFactory(() => new RootContentPage ("Content"), "RootPages Gallery"),
new GalleryPageFactory(() => new MasterDetailPageTabletPage(), "MasterDetailPage Tablet Page"),

Просмотреть файл

@ -30,7 +30,10 @@ namespace Xamarin.Forms.Controls
}
);
const string html = "<html><div class=\"test\"><h2>I am raw html</h2></div></html>";
const string html = "<!DOCTYPE html><html>" +
"<head><meta name='viewport' content='width=device-width,initial-scale=1.0'></head>" +
"<body><div class=\"test\"><h2>I am raw html</h2></div></body></html>";
var htmlWebViewSourceContainer = new ViewContainer<WebView> (Test.WebView.HtmlWebViewSource,
new WebView {
Source = new HtmlWebViewSource { Html = html },
@ -40,9 +43,11 @@ namespace Xamarin.Forms.Controls
var htmlFileWebSourceContainer = new ViewContainer<WebView> (Test.WebView.LoadHtml,
new WebView {
Source = new HtmlWebViewSource {
Html = @"<html>
Source = new HtmlWebViewSource
{
Html = @"<!DOCTYPE html><html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<link rel=""stylesheet"" href=""default.css"">
</head>
<body>
@ -90,8 +95,9 @@ namespace Xamarin.Forms.Controls
{
Source = new HtmlWebViewSource
{
Html = @"<html>
Html = @"<!DOCTYPE html><html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<link rel=""stylesheet"" href=""default.css"">
</head>
<body>

Просмотреть файл

@ -0,0 +1,142 @@
using Xamarin.Forms.CustomAttributes;
using Xamarin.Forms.PlatformConfiguration;
using Xamarin.Forms.PlatformConfiguration.AndroidSpecific;
using Xamarin.Forms.PlatformConfiguration.WindowsSpecific;
namespace Xamarin.Forms.Controls
{
internal class WkWebViewCoreGalleryPage : CoreGalleryPage<WkWebView>
{
protected override bool SupportsFocus
{
get { return false; }
}
protected override void InitializeElement (WkWebView element)
{
element.HeightRequest = 200;
element.Source = new UrlWebViewSource { Url = "http://xamarin.com/" };
}
protected override void Build (StackLayout stackLayout)
{
base.Build (stackLayout);
var urlWebViewSourceContainer = new ViewContainer<WkWebView> (Test.WebView.UrlWebViewSource,
new WkWebView {
Source = new UrlWebViewSource { Url = "https://www.google.com/" },
HeightRequest = 200
}
);
const string html = "<!DOCTYPE html><html>" +
"<head><meta name='viewport' content='width=device-width,initial-scale=1.0'></head>" +
"<body><div class=\"test\"><h2>I am raw html</h2></div></body></html>";
var htmlWebViewSourceContainer = new ViewContainer<WkWebView> (Test.WebView.HtmlWebViewSource,
new WkWebView {
Source = new HtmlWebViewSource { Html = html },
HeightRequest = 200
}
);
var htmlFileWebSourceContainer = new ViewContainer<WkWebView> (Test.WebView.LoadHtml,
new WkWebView {
Source = new HtmlWebViewSource {
Html = @"<!DOCTYPE html><html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<link rel=""stylesheet"" href=""default.css"">
</head>
<body>
<h1>Xamarin.Forms</h1>
<p>The CSS and image are loaded from local files!</p>
<img src='WebImages/XamarinLogo.png'/>
<p><a href=""local.html"">next page</a></p>
</body>
</html>"
},
HeightRequest = 200
}
);
// NOTE: Currently the ability to programmatically enable/disable mixed content only exists on Android
if (Device.RuntimePlatform == Device.Android)
{
var mixedContentTestPage = "https://mixed-content-test.appspot.com/";
var mixedContentDisallowedWebView = new WkWebView() { HeightRequest = 1000 };
mixedContentDisallowedWebView.On<Android>().SetMixedContentMode(MixedContentHandling.NeverAllow);
mixedContentDisallowedWebView.Source = new UrlWebViewSource
{
Url = mixedContentTestPage
};
var mixedContentAllowedWebView = new WkWebView() { HeightRequest = 1000 };
mixedContentAllowedWebView.On<Android>().SetMixedContentMode(MixedContentHandling.AlwaysAllow);
mixedContentAllowedWebView.Source = new UrlWebViewSource
{
Url = mixedContentTestPage
};
var mixedContentDisallowedContainer = new ViewContainer<WkWebView>(Test.WebView.MixedContentDisallowed,
mixedContentDisallowedWebView);
var mixedContentAllowedContainer = new ViewContainer<WkWebView>(Test.WebView.MixedContentAllowed,
mixedContentAllowedWebView);
Add(mixedContentDisallowedContainer);
Add(mixedContentAllowedContainer);
}
var jsAlertWebView = new WkWebView
{
Source = new HtmlWebViewSource
{
Html = @"<!DOCTYPE html><html>
<head>
<meta name='viewport' content='width=device-width,initial-scale=1.0'>
<link rel=""stylesheet"" href=""default.css"">
</head>
<body>
<button onclick=""window.alert('foo');"">Click</button>
</body>
</html>"
},
HeightRequest = 200
};
jsAlertWebView.On<Windows>().SetIsJavaScriptAlertEnabled(true);
var javascriptAlertWebSourceContainer = new ViewContainer<WkWebView>(Test.WebView.JavaScriptAlert,
jsAlertWebView
);
var evaluateJsWebView = new WkWebView
{
Source = new UrlWebViewSource { Url = "https://www.google.com/" },
HeightRequest = 50
};
var evaluateJsWebViewSourceContainer = new ViewContainer<WkWebView>(Test.WebView.EvaluateJavaScript,
evaluateJsWebView
);
var resultsLabel = new Label();
var execButton = new Button();
execButton.Text = "Evaluate Javascript";
execButton.Command = new Command(async() => resultsLabel.Text = await evaluateJsWebView.EvaluateJavaScriptAsync(
"var test = function(){ return 'This string came from Javascript!'; }; test();"));
evaluateJsWebViewSourceContainer.ContainerLayout.Children.Add(resultsLabel);
evaluateJsWebViewSourceContainer.ContainerLayout.Children.Add(execButton);
Add (urlWebViewSourceContainer);
Add (htmlWebViewSourceContainer);
Add (htmlFileWebSourceContainer);
Add (javascriptAlertWebSourceContainer);
Add (evaluateJsWebViewSourceContainer);
}
}
}

Просмотреть файл

@ -3,6 +3,7 @@ using System.ComponentModel;
using System.Drawing;
using System.Threading.Tasks;
using Foundation;
using ObjCRuntime;
using UIKit;
using WebKit;
using Xamarin.Forms.Internals;
@ -44,7 +45,8 @@ namespace Xamarin.Forms.Platform.iOS
WebView.GoBackRequested += OnGoBackRequested;
WebView.GoForwardRequested += OnGoForwardRequested;
WebView.ReloadRequested += OnReloadRequested;
NavigationDelegate = new CustomWebViewDelegate(this);
NavigationDelegate = new CustomWebViewNavigationDelegate(this);
UIDelegate = new CustomWebViewUIDelegate();
BackgroundColor = UIColor.Clear;
@ -151,7 +153,7 @@ namespace Xamarin.Forms.Platform.iOS
async Task<string> OnEvaluateJavaScriptRequested(string script)
{
var result = await EvaluateJavaScriptAsync(script);
return result.ToString();
return result?.ToString();
}
void OnGoBackRequested(object sender, EventArgs eventArgs)
@ -187,12 +189,12 @@ namespace Xamarin.Forms.Platform.iOS
((IWebViewController)WebView).CanGoForward = CanGoForward;
}
class CustomWebViewDelegate : WKNavigationDelegate
class CustomWebViewNavigationDelegate : WKNavigationDelegate
{
readonly WkWebViewRenderer _renderer;
WebNavigationEvent _lastEvent;
public CustomWebViewDelegate(WkWebViewRenderer renderer)
public CustomWebViewNavigationDelegate(WkWebViewRenderer renderer)
{
if (renderer == null)
throw new ArgumentNullException("renderer");
@ -274,6 +276,106 @@ namespace Xamarin.Forms.Platform.iOS
}
}
class CustomWebViewUIDelegate : WKUIDelegate
{
static string LocalOK = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("OK");
static string LocalCancel = NSBundle.FromIdentifier("com.apple.UIKit").GetLocalizedString("Cancel");
public override void RunJavaScriptAlertPanel(WKWebView webView, string message, WKFrameInfo frame, Action completionHandler)
{
PresentAlertController(
webView,
message,
okAction: _ => completionHandler()
);
}
public override void RunJavaScriptConfirmPanel(WKWebView webView, string message, WKFrameInfo frame, Action<bool> completionHandler)
{
PresentAlertController(
webView,
message,
okAction: _ => completionHandler(true),
cancelAction: _ => completionHandler(false)
);
}
public override void RunJavaScriptTextInputPanel(
WKWebView webView, string prompt, string defaultText, WKFrameInfo frame, Action<string> completionHandler)
{
PresentAlertController(
webView,
prompt,
defaultText: defaultText,
okAction: x => completionHandler(x.TextFields[0].Text),
cancelAction: _ => completionHandler(null)
);
}
static string GetJsAlertTitle(WKWebView webView)
{
// Emulate the behavior of UIWebView dialogs.
// The scheme and host are used unless local html content is what the webview is displaying,
// in which case the bundle file name is used.
if (webView.Url != null && webView.Url.AbsoluteString != $"file://{NSBundle.MainBundle.BundlePath}/")
return $"{webView.Url.Scheme}://{webView.Url.Host}";
return new NSString(NSBundle.MainBundle.BundlePath).LastPathComponent;
}
static UIAlertAction AddOkAction(UIAlertController controller, Action handler)
{
var action = UIAlertAction.Create(LocalOK, UIAlertActionStyle.Default, (_) => handler());
controller.AddAction(action);
controller.PreferredAction = action;
return action;
}
static UIAlertAction AddCancelAction(UIAlertController controller, Action handler)
{
var action = UIAlertAction.Create(LocalCancel, UIAlertActionStyle.Cancel, (_) => handler());
controller.AddAction(action);
return action;
}
static void PresentAlertController(
WKWebView webView,
string message,
string defaultText = null,
Action<UIAlertController> okAction = null,
Action<UIAlertController> cancelAction = null)
{
var controller = UIAlertController.Create(GetJsAlertTitle(webView), message, UIAlertControllerStyle.Alert);
if (defaultText != null)
controller.AddTextField((textField) => textField.Text = defaultText);
if (okAction != null)
AddOkAction(controller, () => okAction(controller));
if (cancelAction != null)
AddCancelAction(controller, () => cancelAction(controller));
GetTopViewController(UIApplication.SharedApplication.KeyWindow.RootViewController)
.PresentViewController(controller, true, null);
}
static UIViewController GetTopViewController(UIViewController viewController)
{
if (viewController is UINavigationController navigationController)
return GetTopViewController(navigationController.VisibleViewController);
if (viewController is UITabBarController tabBarController)
return GetTopViewController(tabBarController.SelectedViewController);
if (viewController.PresentedViewController != null)
return GetTopViewController(viewController.PresentedViewController);
return viewController;
}
}
#region IPlatformRenderer implementation
public UIView NativeView