diff --git a/lib/src/models/rules/insert.dart b/lib/src/models/rules/insert.dart index 1556494a..9a200927 100644 --- a/lib/src/models/rules/insert.dart +++ b/lib/src/models/rules/insert.dart @@ -320,7 +320,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { // URL generator tool (https://www.randomlists.com/urls) is used. static const _linkPattern = r'(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?'; - static final _linkRegExp = RegExp(_linkPattern); + static final linkRegExp = RegExp(_linkPattern); @override Delta? applyRule( @@ -364,7 +364,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { final affectedWords = '$leftWordPart$data$rightWordPart'; // Check for URL pattern. - final matches = _linkRegExp.allMatches(affectedWords); + final matches = linkRegExp.allMatches(affectedWords); // If there are no matches, do not apply any format. if (matches.isEmpty) return null; @@ -394,7 +394,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { // Keep the leading segment of text and add link with its proper // attribute. formatterDelta - ..retain(separationLength, LinkAttribute(null).toJson()) + ..retain(separationLength, Attribute.link.toJson()) ..retain(link.length, LinkAttribute(link).toJson()); // Update reference index. @@ -405,7 +405,7 @@ class AutoFormatMultipleLinksRule extends InsertRule { final remainingLength = affectedWords.length - previousLinkEndRelativeIndex; // Remove links from remaining non-link text. - formatterDelta.retain(remainingLength, LinkAttribute(null).toJson()); + formatterDelta.retain(remainingLength, Attribute.link.toJson()); // Build and return resulting change delta. return baseDelta.compose(formatterDelta); diff --git a/lib/src/translations/toolbar.i18n.dart b/lib/src/translations/toolbar.i18n.dart index 0f735e78..155509ca 100644 --- a/lib/src/translations/toolbar.i18n.dart +++ b/lib/src/translations/toolbar.i18n.dart @@ -18,6 +18,7 @@ extension Localization on String { 'Zoom': 'Zoom', 'Saved': 'Saved', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'ar': { 'Paste a link': 'نسخ الرابط', @@ -34,6 +35,7 @@ extension Localization on String { 'Zoom': 'تكبير', 'Saved': 'أنقذ', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'da': { 'Paste a link': 'Indsæt link', @@ -50,6 +52,7 @@ extension Localization on String { 'Zoom': 'Zoom ind', 'Saved': 'Gemt', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'de': { 'Paste a link': 'Link hinzufügen', @@ -67,6 +70,7 @@ extension Localization on String { 'Zoom': 'Zoomen', 'Saved': 'Gerettet', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'fr': { 'Paste a link': 'Coller un lien', @@ -83,6 +87,7 @@ extension Localization on String { 'Zoom': 'Zoom', 'Saved': 'Enregistrée', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'zh_CN': { 'Paste a link': '粘贴链接', @@ -99,6 +104,7 @@ extension Localization on String { 'Zoom': '放大', 'Saved': '已保存', 'Text': '文字', + 'What is entered is not a link': 'What is entered is not a link', }, 'ko': { 'Paste a link': '링크를 붙여넣어 주세요.', @@ -115,6 +121,7 @@ extension Localization on String { 'Zoom': '확대하기', 'Saved': '저장되었습니다.', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'ru': { 'Paste a link': 'Вставить ссылку', @@ -131,6 +138,7 @@ extension Localization on String { 'Zoom': 'Увеличить', 'Saved': 'Сохранено', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'es': { 'Paste a link': 'Pega un enlace', @@ -148,6 +156,7 @@ extension Localization on String { 'Zoom': 'Zoom', 'Saved': 'Salvado', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'tr': { 'Paste a link': 'Bağlantıyı Yapıştır', @@ -164,6 +173,7 @@ extension Localization on String { 'Zoom': 'yakınlaştır', 'Saved': 'kaydedildi', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'uk': { 'Paste a link': 'Вставити посилання', @@ -180,6 +190,7 @@ extension Localization on String { 'Zoom': 'Збільшити', 'Saved': 'Збережено', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'pt': { 'Paste a link': 'Colar um link', @@ -197,6 +208,7 @@ extension Localization on String { 'Zoom': 'Ampliação', 'Saved': 'Salvou', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'pl': { 'Paste a link': 'Wklej link', @@ -213,6 +225,7 @@ extension Localization on String { 'Zoom': 'Powiększenie', 'Saved': 'Zapisane', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, 'vi': { 'Paste a link': 'Chèn liên kết', @@ -229,6 +242,7 @@ extension Localization on String { 'Zoom': 'Thu phóng', 'Saved': 'Đã lưu', 'Text': 'Text', + 'What is entered is not a link': 'What is entered is not a link', }, }; diff --git a/lib/src/widgets/toolbar/link_dialog.dart b/lib/src/widgets/toolbar/link_dialog.dart index ff13908e..cd1f1370 100644 --- a/lib/src/widgets/toolbar/link_dialog.dart +++ b/lib/src/widgets/toolbar/link_dialog.dart @@ -31,7 +31,7 @@ class LinkDialogState extends State { content: TextField( style: widget.dialogTheme?.inputTextStyle, decoration: InputDecoration( - labelText: 'Link'.i18n, + labelText: 'Paste a link'.i18n, labelStyle: widget.dialogTheme?.labelTextStyle, floatingLabelStyle: widget.dialogTheme?.labelTextStyle), autofocus: true, diff --git a/lib/src/widgets/toolbar/link_style_button.dart b/lib/src/widgets/toolbar/link_style_button.dart index 59ef3114..fab08bba 100644 --- a/lib/src/widgets/toolbar/link_style_button.dart +++ b/lib/src/widgets/toolbar/link_style_button.dart @@ -1,12 +1,13 @@ import 'package:flutter/material.dart'; +import 'package:tuple/tuple.dart'; import '../../models/documents/attribute.dart'; +import '../../models/rules/insert.dart'; import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_icon_theme.dart'; import '../../translations/toolbar.i18n.dart'; import '../controller.dart'; import '../toolbar.dart'; -import 'link_dialog.dart'; class LinkStyleButton extends StatefulWidget { const LinkStyleButton({ @@ -96,11 +97,19 @@ class _LinkStyleButtonState extends State { } void _openLinkDialog(BuildContext context) { - showDialog( + showDialog( context: context, builder: (ctx) { final link = _getLinkAttributeValue(); - return LinkDialog(dialogTheme: widget.dialogTheme, link: link); + if (link != null) { + // TODO: text should be the link's corresponding text, not selection + } + final index = widget.controller.selection.baseOffset; + final text = widget.controller.document + .toPlainText() + .substring(index, widget.controller.selection.extentOffset); + return _LinkDialog( + dialogTheme: widget.dialogTheme, link: link, text: text); }, ).then(_linkSubmitted); } @@ -112,10 +121,111 @@ class _LinkStyleButtonState extends State { ?.value; } - void _linkSubmitted(String? value) { - if (value == null || value.isEmpty) { - return; + void _linkSubmitted(dynamic value) { + // text.isNotEmpty && link.isNotEmpty + final String text = (value as Tuple2).item1; + final String link = value.item2; + + final index = widget.controller.selection.baseOffset; + final length = widget.controller.selection.extentOffset - index; + widget.controller.replaceText(index, length, text, null); + widget.controller.formatSelection(LinkAttribute(link)); + } +} + +class _LinkDialog extends StatefulWidget { + const _LinkDialog({this.dialogTheme, this.link, this.text, Key? key}) + : super(key: key); + + final QuillDialogTheme? dialogTheme; + final String? link; + final String? text; + + @override + _LinkDialogState createState() => _LinkDialogState(); +} + +class _LinkDialogState extends State<_LinkDialog> { + late String _link; + late String _text; + late TextEditingController _linkController; + late TextEditingController _textController; + + @override + void initState() { + super.initState(); + _link = widget.link ?? ''; + _text = widget.text ?? ''; + _linkController = TextEditingController(text: _link); + _textController = TextEditingController(text: _text); + } + + @override + Widget build(BuildContext context) { + return AlertDialog( + backgroundColor: widget.dialogTheme?.dialogBackgroundColor, + content: Column( + children: [ + TextField( + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( + labelText: 'Text'.i18n, + labelStyle: widget.dialogTheme?.labelTextStyle, + floatingLabelStyle: widget.dialogTheme?.labelTextStyle), + autofocus: true, + onChanged: _textChanged, + controller: _textController, + ), + TextField( + style: widget.dialogTheme?.inputTextStyle, + decoration: InputDecoration( + labelText: 'Link'.i18n, + labelStyle: widget.dialogTheme?.labelTextStyle, + floatingLabelStyle: widget.dialogTheme?.labelTextStyle), + autofocus: true, + onChanged: _linkChanged, + controller: _linkController, + ), + ], + ), + actions: [ + TextButton( + onPressed: _canPress() ? _applyLink : null, + child: Text( + 'Ok'.i18n, + style: widget.dialogTheme?.labelTextStyle, + ), + ), + ], + ); + } + + bool _canPress() { + if (_text.isEmpty || _link.isEmpty) { + return false; } - widget.controller.formatSelection(LinkAttribute(value)); + + if (!AutoFormatMultipleLinksRule.linkRegExp.hasMatch(_link)) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar(content: Text('What is entered is not a link'.i18n))); + return false; + } + return true; + } + + void _linkChanged(String value) { + setState(() { + _link = value; + }); + } + + void _textChanged(String value) { + setState(() { + _text = value; + }); + } + + void _applyLink() { + Navigator.pop(context, Tuple2(_text, _link)); } }