Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/url_launcher/url_launcher_web/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
# 0.1.5

- Added the web implementation of the Link widget.

# 0.1.4+2

- Move `lib/third_party` to `lib/src/third_party`.
Expand Down
10 changes: 10 additions & 0 deletions packages/url_launcher/url_launcher_web/analysis_options.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# This is a temporary file to allow us to unblock the flutter/plugins repo CI.
# It disables some of lints that were disabled inline. Disabling lints inline
# is no longer possible, so this file is required.
# TODO(ditman) https://github.com/flutter/flutter/issues/55000 (clean this up)

include: ../../../analysis_options.yaml

analyzer:
errors:
undefined_prefixed_name: ignore
295 changes: 295 additions & 0 deletions packages/url_launcher/url_launcher_web/lib/src/link.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,295 @@
// Copyright 2017 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:async';
import 'dart:html' as html;
import 'dart:js_util';

import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';

import 'package:url_launcher_platform_interface/link.dart';

/// The unique identifier for the view type to be used for link platform views.
const String linkViewType = '__url_launcher::link';

/// The name of the property used to set the viewId on the DOM element.
const String linkViewIdProperty = '__url_launcher::link::viewId';

/// Signature for a function that takes a unique [id] and creates an HTML element.
typedef HtmlViewFactory = html.Element Function(int viewId);

/// Factory that returns the link DOM element for each unique view id.
HtmlViewFactory get linkViewFactory => LinkViewController._viewFactory;

/// The delegate for building the [Link] widget on the web.
///
/// It uses a platform view to render an anchor element in the DOM.
class WebLinkDelegate extends StatefulWidget {
/// Creates a delegate for the given [link].
const WebLinkDelegate(this.link);

/// Information about the link built by the app.
final LinkInfo link;

@override
WebLinkDelegateState createState() => WebLinkDelegateState();
}

/// The link delegate used on the web platform.
///
/// For external URIs, it lets the browser do its thing. For app route names, it
/// pushes the route name to the framework.
class WebLinkDelegateState extends State<WebLinkDelegate> {
LinkViewController _controller;

@override
void didUpdateWidget(WebLinkDelegate oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.link.uri != oldWidget.link.uri) {
_controller?.setUri(widget.link.uri);
}
if (widget.link.target != oldWidget.link.target) {
_controller?.setTarget(widget.link.target);
}
}

Future<void> _followLink() {
LinkViewController.registerHitTest(_controller);
return Future<void>.value();
}

@override
Widget build(BuildContext context) {
return Stack(
children: <Widget>[
widget.link.builder(
context,
widget.link.isDisabled ? null : _followLink,
),
Positioned.fill(
child: PlatformViewLink(
viewType: linkViewType,
onCreatePlatformView: (PlatformViewCreationParams params) {
_controller = LinkViewController.fromParams(params, context);
return _controller
..setUri(widget.link.uri)
..setTarget(widget.link.target);
},
surfaceFactory:
(BuildContext context, PlatformViewController controller) {
return PlatformViewSurface(
controller: controller,
gestureRecognizers:
Set<Factory<OneSequenceGestureRecognizer>>(),
hitTestBehavior: PlatformViewHitTestBehavior.transparent,
);
},
),
),
],
);
}
}

/// Controls link views.
class LinkViewController extends PlatformViewController {
/// Creates a [LinkViewController] instance with the unique [viewId].
LinkViewController(this.viewId, this.context) {
if (_instances.isEmpty) {
// This is the first controller being created, attach the global click
// listener.
_clickSubscription = html.window.onClick.listen(_onGlobalClick);
}
_instances[viewId] = this;
}

/// Creates and initializes a [LinkViewController] instance with the given
/// platform view [params].
factory LinkViewController.fromParams(
PlatformViewCreationParams params,
BuildContext context,
) {
final int viewId = params.id;
final LinkViewController controller = LinkViewController(viewId, context);
controller._initialize().then((_) {
params.onPlatformViewCreated(viewId);
});
return controller;
}

static Map<int, LinkViewController> _instances = <int, LinkViewController>{};

static html.Element _viewFactory(int viewId) {
return _instances[viewId]?._element;
}

static int _hitTestedViewId;

static StreamSubscription _clickSubscription;

static void _onGlobalClick(html.MouseEvent event) {
final int viewId = getViewIdFromTarget(event);
_instances[viewId]?._onDomClick(event);
// After the DOM click event has been received, clean up the hit test state
// so we can start fresh on the next click.
unregisterHitTest();
}

/// Call this method to indicate that a hit test has been registered for the
/// given [controller].
///
/// The [onClick] callback is invoked when the anchor element receives a
/// `click` from the browser.
static void registerHitTest(LinkViewController controller) {
_hitTestedViewId = controller.viewId;
}

/// Removes all information about previously registered hit tests.
static void unregisterHitTest() {
_hitTestedViewId = null;
}

@override
final int viewId;

/// The context of the [Link] widget that created this controller.
final BuildContext context;

html.Element _element;
bool get _isInitialized => _element != null;

Future<void> _initialize() async {
_element = html.Element.tag('a');
setProperty(_element, linkViewIdProperty, viewId);
_element.style
..opacity = '0'
..display = 'block'
..cursor = 'unset';

// This is recommended on MDN:
// - https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a#attr-target
_element.setAttribute('rel', 'noreferrer noopener');

final Map<String, dynamic> args = <String, dynamic>{
'id': viewId,
'viewType': linkViewType,
};
await SystemChannels.platform_views.invokeMethod<void>('create', args);
}

void _onDomClick(html.MouseEvent event) {
final bool isHitTested = _hitTestedViewId == viewId;
if (!isHitTested) {
// There was no hit test registered for this click. This means the click
// landed on the anchor element but not on the underlying widget. In this
// case, we prevent the browser from following the click.
event.preventDefault();
return;
}

if (_uri.hasScheme) {
// External links will be handled by the browser, so we don't have to do
// anything.
return;
}

// A uri that doesn't have a scheme is an internal route name. In this
// case, we push it via Flutter's navigation system instead of letting the
// browser handle it.
event.preventDefault();
final String routeName = _uri.toString();
pushRouteNameToFramework(context, routeName);
}

Uri _uri;

/// Set the [Uri] value for this link.
void setUri(Uri uri) {
assert(_isInitialized);
_uri = uri;
if (uri == null) {
_element.removeAttribute('href');
} else {
_element.setAttribute('href', uri.toString());
}
}

/// Set the [LinkTarget] value for this link.
void setTarget(LinkTarget target) {
assert(_isInitialized);
_element.setAttribute('target', _getHtmlTarget(target));
}

String _getHtmlTarget(LinkTarget target) {
switch (target) {
case LinkTarget.defaultTarget:
case LinkTarget.self:
return '_self';
case LinkTarget.blank:
return '_blank';
default:
throw Exception('Unknown LinkTarget value $target.');
}
}

@override
Future<void> clearFocus() async {
// Currently this does nothing on Flutter Web.
// TODO(het): Implement this. See https://github.com/flutter/flutter/issues/39496
}

@override
Future<void> dispatchPointerEvent(PointerEvent event) async {
// We do not dispatch pointer events to HTML views because they may contain
// cross-origin iframes, which only accept user-generated events.
}

@override
Future<void> dispose() async {
if (_isInitialized) {
assert(_instances[viewId] == this);
_instances.remove(viewId);
if (_instances.isEmpty) {
await _clickSubscription.cancel();
}
await SystemChannels.platform_views.invokeMethod<void>('dispose', viewId);
}
}
}

/// Finds the view id of the DOM element targeted by the [event].
int getViewIdFromTarget(html.Event event) {
final html.Element linkElement = getLinkElementFromTarget(event);
if (linkElement != null) {
return getProperty(linkElement, linkViewIdProperty);
}
return null;
}

/// Finds the targeted DOM element by the [event].
///
/// It handles the case where the target element is inside a shadow DOM too.
html.Element getLinkElementFromTarget(html.Event event) {
final html.Element target = event.target;
if (isLinkElement(target)) {
return target;
}
if (target.shadowRoot != null) {
final html.Element child = target.shadowRoot.lastChild;
if (isLinkElement(child)) {
return child;
}
}
return null;
}

/// Checks if the given [element] is a link that was created by
/// [LinkViewController].
bool isLinkElement(html.Element element) {
return element.tagName == 'A' && hasProperty(element, linkViewIdProperty);
}
10 changes: 10 additions & 0 deletions packages/url_launcher/url_launcher_web/lib/url_launcher_web.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,15 @@

import 'dart:async';
import 'dart:html' as html;
// ignore: undefined_shown_name
import 'dart:ui' as ui show platformViewRegistry;

import 'package:flutter_web_plugins/flutter_web_plugins.dart';
import 'package:meta/meta.dart';
import 'package:url_launcher_platform_interface/link.dart';
import 'package:url_launcher_platform_interface/url_launcher_platform_interface.dart';

import 'src/link.dart';
import 'src/third_party/platform_detect/browser.dart';

const _safariTargetTopSchemes = {
Expand Down Expand Up @@ -43,6 +47,12 @@ class UrlLauncherPlugin extends UrlLauncherPlatform {
/// Registers this class as the default instance of [UrlLauncherPlatform].
static void registerWith(Registrar registrar) {
UrlLauncherPlatform.instance = UrlLauncherPlugin();
ui.platformViewRegistry.registerViewFactory(linkViewType, linkViewFactory);
}

@override
LinkDelegate get linkDelegate {
return (LinkInfo linkInfo) => WebLinkDelegate(linkInfo);
}

/// Opens the given [url] in the specified [webOnlyWindowName].
Expand Down
4 changes: 2 additions & 2 deletions packages/url_launcher/url_launcher_web/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ homepage: https://github.com/flutter/plugins/tree/master/packages/url_launcher/u
# 0.1.y+z is compatible with 1.0.0, if you land a breaking change bump
# the version to 2.0.0.
# See more details: https://github.com/flutter/flutter/wiki/Package-migration-to-1.0.0
version: 0.1.4+2
version: 0.1.5

flutter:
plugin:
Expand All @@ -14,7 +14,7 @@ flutter:
fileName: url_launcher_web.dart

dependencies:
url_launcher_platform_interface: ^1.0.8
url_launcher_platform_interface: ^1.0.9
flutter:
sdk: flutter
flutter_web_plugins:
Expand Down
Loading