diff --git a/packages/zefyr/lib/src/widgets/buttons.dart b/packages/zefyr/lib/src/widgets/buttons.dart index 675f4ea38..2bd61f00a 100644 --- a/packages/zefyr/lib/src/widgets/buttons.dart +++ b/packages/zefyr/lib/src/widgets/buttons.dart @@ -411,10 +411,7 @@ class _LinkButtonState extends State { final editor = ZefyrToolbar.of(context).editor; var link = getLink(); assert(link != null); - if (await canLaunch(link)) { - editor.hideKeyboard(); - await launch(link, forceWebView: true); - } + await ZefyrToolbar.of(context).editor.onLaunchUrl(link); } void _handleInputChange() { diff --git a/packages/zefyr/lib/src/widgets/common.dart b/packages/zefyr/lib/src/widgets/common.dart index aabc8b8f1..380eb741e 100644 --- a/packages/zefyr/lib/src/widgets/common.dart +++ b/packages/zefyr/lib/src/widgets/common.dart @@ -1,9 +1,11 @@ // Copyright (c) 2018, the Zefyr project authors. Please see the AUTHORS file // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. + import 'package:flutter/rendering.dart'; import 'package:flutter/widgets.dart'; import 'package:notus/notus.dart'; +import 'package:flutter/gestures.dart'; import 'editable_box.dart'; import 'horizontal_rule.dart'; @@ -110,9 +112,20 @@ class _RawZefyrLineState extends State { TextSpan buildText(BuildContext context) { final theme = ZefyrTheme.of(context); - final List children = widget.node.children + + List children; + + if(ZefyrScope.of(context).isEditable) { + children = widget.node.children .map((node) => _segmentToTextSpan(node, theme)) .toList(growable: false); + } else { + children = widget.node.children + .map((node) => _segmentToTextSpanView(node, theme)) + .toList(growable: true); + children.add(TextSpan(text: ' ', style: TextStyle(fontSize: 1.0))); + } + return new TextSpan(style: widget.style, children: children); } @@ -126,6 +139,21 @@ class _RawZefyrLineState extends State { ); } + TextSpan _segmentToTextSpanView(Node node, ZefyrThemeData theme) { + final TextNode segment = node; + final attrs = segment.style; + + return new TextSpan( + text: segment.value, + style: _getTextStyle(attrs, theme), + recognizer: (attrs.contains(NotusAttribute.link) ? + (new TapGestureRecognizer()..onTap = () async { + var link = attrs.get(NotusAttribute.link).value; + await ZefyrScope.of(context).onLaunchUrl(link); + }) : null), + ); + } + TextStyle _getTextStyle(NotusStyle style, ZefyrThemeData theme) { TextStyle result = new TextStyle(); if (style.containsSame(NotusAttribute.bold)) { diff --git a/packages/zefyr/lib/src/widgets/editable_text.dart b/packages/zefyr/lib/src/widgets/editable_text.dart index 59f01a738..4d63a0539 100644 --- a/packages/zefyr/lib/src/widgets/editable_text.dart +++ b/packages/zefyr/lib/src/widgets/editable_text.dart @@ -33,6 +33,7 @@ class ZefyrEditableText extends StatefulWidget { @required this.controller, @required this.focusNode, @required this.imageDelegate, + @required this.onLaunchUrl, this.autofocus: true, this.enabled: true, this.padding: const EdgeInsets.symmetric(horizontal: 16.0), @@ -42,6 +43,7 @@ class ZefyrEditableText extends StatefulWidget { final ZefyrController controller; final FocusNode focusNode; final ZefyrImageDelegate imageDelegate; + final Function onLaunchUrl; final bool autofocus; final bool enabled; final ScrollPhysics physics; diff --git a/packages/zefyr/lib/src/widgets/editor.dart b/packages/zefyr/lib/src/widgets/editor.dart index 4a37337e8..bccaee43d 100644 --- a/packages/zefyr/lib/src/widgets/editor.dart +++ b/packages/zefyr/lib/src/widgets/editor.dart @@ -2,6 +2,7 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. import 'package:flutter/widgets.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'controller.dart'; import 'editable_text.dart'; @@ -22,6 +23,7 @@ class ZefyrEditor extends StatefulWidget { this.padding: const EdgeInsets.symmetric(horizontal: 16.0), this.toolbarDelegate, this.imageDelegate, + this.onLaunchUrl, this.physics, }) : super(key: key); @@ -31,6 +33,7 @@ class ZefyrEditor extends StatefulWidget { final bool enabled; final ZefyrToolbarDelegate toolbarDelegate; final ZefyrImageDelegate imageDelegate; + final Function onLaunchUrl; final ScrollPhysics physics; /// Padding around editable area. @@ -42,6 +45,7 @@ class ZefyrEditor extends StatefulWidget { class _ZefyrEditorState extends State { ZefyrImageDelegate _imageDelegate; + Function _onLaunchUrl; ZefyrScope _scope; ZefyrThemeData _themeData; GlobalKey _toolbarKey; @@ -85,10 +89,18 @@ class _ZefyrEditorState extends State { } } + Future _launchUrl(String link) async { + if(await canLaunch(link)) { + await launch(link, forceWebView: true); + } + } + @override void initState() { super.initState(); _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); + + _onLaunchUrl = widget.onLaunchUrl ?? _launchUrl; } @override @@ -100,6 +112,11 @@ class _ZefyrEditorState extends State { _imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); _scope.imageDelegate = _imageDelegate; } + + if (widget.onLaunchUrl != oldWidget.onLaunchUrl) { + _onLaunchUrl = widget.onLaunchUrl ?? _launchUrl; + _scope.onLaunchUrl = _onLaunchUrl; + } } @override @@ -114,6 +131,7 @@ class _ZefyrEditorState extends State { if (_scope == null) { _scope = ZefyrScope.editable( imageDelegate: _imageDelegate, + onLaunchUrl: _onLaunchUrl, controller: widget.controller, focusNode: widget.focusNode, focusScope: FocusScope.of(context), @@ -147,6 +165,7 @@ class _ZefyrEditorState extends State { controller: _scope.controller, focusNode: _scope.focusNode, imageDelegate: _scope.imageDelegate, + onLaunchUrl: _scope.onLaunchUrl, autofocus: widget.autofocus, enabled: widget.enabled, padding: widget.padding, diff --git a/packages/zefyr/lib/src/widgets/scope.dart b/packages/zefyr/lib/src/widgets/scope.dart index 2853a8a20..399c99a1d 100644 --- a/packages/zefyr/lib/src/widgets/scope.dart +++ b/packages/zefyr/lib/src/widgets/scope.dart @@ -24,10 +24,14 @@ class ZefyrScope extends ChangeNotifier { /// Creates a view-only scope. /// /// Normally used in [ZefyrView]. - ZefyrScope.view({@required ZefyrImageDelegate imageDelegate}) - : assert(imageDelegate != null), + ZefyrScope.view({ + @required ZefyrImageDelegate imageDelegate, + @required Future onLaunchUrl(String url), + }) : assert(imageDelegate != null), + assert(onLaunchUrl != null), isEditable = false, - _imageDelegate = imageDelegate; + _imageDelegate = imageDelegate, + _onLaunchUrl = onLaunchUrl; /// Creates editable scope. /// @@ -35,15 +39,18 @@ class ZefyrScope extends ChangeNotifier { ZefyrScope.editable({ @required ZefyrController controller, @required ZefyrImageDelegate imageDelegate, + @required Future onLaunchUrl(String url), @required FocusNode focusNode, @required FocusScopeNode focusScope, }) : assert(controller != null), assert(imageDelegate != null), + assert(onLaunchUrl != null), assert(focusNode != null), assert(focusScope != null), isEditable = true, _controller = controller, _imageDelegate = imageDelegate, + _onLaunchUrl = onLaunchUrl, _focusNode = focusNode, _focusScope = focusScope, _cursorTimer = CursorTimer(), @@ -70,6 +77,16 @@ class ZefyrScope extends ChangeNotifier { } } + Function _onLaunchUrl; + Function get onLaunchUrl => _onLaunchUrl; + set onLaunchUrl(Future func(String url)) { + assert(func != null); + if (_onLaunchUrl != func) { + _onLaunchUrl = func; + notifyListeners(); + } + } + ZefyrController _controller; ZefyrController get controller => _controller; set controller(ZefyrController value) { diff --git a/packages/zefyr/lib/src/widgets/view.dart b/packages/zefyr/lib/src/widgets/view.dart index d7b7b13e3..bbba576d8 100644 --- a/packages/zefyr/lib/src/widgets/view.dart +++ b/packages/zefyr/lib/src/widgets/view.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:meta/meta.dart'; import 'package:notus/notus.dart'; +import 'package:url_launcher/url_launcher.dart'; import 'code.dart'; import 'common.dart'; @@ -16,9 +17,14 @@ import 'theme.dart'; class ZefyrView extends StatefulWidget { final NotusDocument document; final ZefyrImageDelegate imageDelegate; + final Function onLaunchUrl; - const ZefyrView({Key key, @required this.document, this.imageDelegate}) - : super(key: key); + const ZefyrView({ + Key key, + @required this.document, + this.imageDelegate, + this.onLaunchUrl, + }) : super(key: key); @override ZefyrViewState createState() => ZefyrViewState(); @@ -29,17 +35,22 @@ class ZefyrViewState extends State { ZefyrThemeData _themeData; ZefyrImageDelegate get imageDelegate => widget.imageDelegate; + Function get onLaunchUrl => widget.onLaunchUrl; @override void initState() { super.initState(); - _scope = ZefyrScope.view(imageDelegate: widget.imageDelegate); + _scope = ZefyrScope.view( + imageDelegate: widget.imageDelegate ?? new ZefyrDefaultImageDelegate(), + onLaunchUrl: widget.onLaunchUrl ?? _launchUrl, + ); } @override void didUpdateWidget(ZefyrView oldWidget) { super.didUpdateWidget(oldWidget); - _scope.imageDelegate = widget.imageDelegate; + _scope.imageDelegate = widget.imageDelegate ?? new ZefyrDefaultImageDelegate(); + _scope.onLaunchUrl = widget.onLaunchUrl ?? _launchUrl; } @override @@ -104,4 +115,10 @@ class ZefyrViewState extends State { throw new UnimplementedError('Block format $blockStyle.'); } + + Future _launchUrl(String link) async { + if(await canLaunch(link)) { + await launch(link, forceWebView: true); + } + } }