Allow link button to enter text

pull/602/head
X Code 3 years ago
parent c89779b4a6
commit df418e590d
  1. 8
      lib/src/models/rules/insert.dart
  2. 14
      lib/src/translations/toolbar.i18n.dart
  3. 2
      lib/src/widgets/toolbar/link_dialog.dart
  4. 124
      lib/src/widgets/toolbar/link_style_button.dart

@ -320,7 +320,7 @@ class AutoFormatMultipleLinksRule extends InsertRule {
// URL generator tool (https://www.randomlists.com/urls) is used. // URL generator tool (https://www.randomlists.com/urls) is used.
static const _linkPattern = static const _linkPattern =
r'(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?'; r'(https?:\/\/|www\.)[\w-\.]+\.[\w-\.]+(\/([\S]+)?)?';
static final _linkRegExp = RegExp(_linkPattern); static final linkRegExp = RegExp(_linkPattern);
@override @override
Delta? applyRule( Delta? applyRule(
@ -364,7 +364,7 @@ class AutoFormatMultipleLinksRule extends InsertRule {
final affectedWords = '$leftWordPart$data$rightWordPart'; final affectedWords = '$leftWordPart$data$rightWordPart';
// Check for URL pattern. // 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 there are no matches, do not apply any format.
if (matches.isEmpty) return null; 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 // Keep the leading segment of text and add link with its proper
// attribute. // attribute.
formatterDelta formatterDelta
..retain(separationLength, LinkAttribute(null).toJson()) ..retain(separationLength, Attribute.link.toJson())
..retain(link.length, LinkAttribute(link).toJson()); ..retain(link.length, LinkAttribute(link).toJson());
// Update reference index. // Update reference index.
@ -405,7 +405,7 @@ class AutoFormatMultipleLinksRule extends InsertRule {
final remainingLength = affectedWords.length - previousLinkEndRelativeIndex; final remainingLength = affectedWords.length - previousLinkEndRelativeIndex;
// Remove links from remaining non-link text. // 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. // Build and return resulting change delta.
return baseDelta.compose(formatterDelta); return baseDelta.compose(formatterDelta);

@ -18,6 +18,7 @@ extension Localization on String {
'Zoom': 'Zoom', 'Zoom': 'Zoom',
'Saved': 'Saved', 'Saved': 'Saved',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'ar': { 'ar': {
'Paste a link': 'نسخ الرابط', 'Paste a link': 'نسخ الرابط',
@ -34,6 +35,7 @@ extension Localization on String {
'Zoom': 'تكبير', 'Zoom': 'تكبير',
'Saved': 'أنقذ', 'Saved': 'أنقذ',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'da': { 'da': {
'Paste a link': 'Indsæt link', 'Paste a link': 'Indsæt link',
@ -50,6 +52,7 @@ extension Localization on String {
'Zoom': 'Zoom ind', 'Zoom': 'Zoom ind',
'Saved': 'Gemt', 'Saved': 'Gemt',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'de': { 'de': {
'Paste a link': 'Link hinzufügen', 'Paste a link': 'Link hinzufügen',
@ -67,6 +70,7 @@ extension Localization on String {
'Zoom': 'Zoomen', 'Zoom': 'Zoomen',
'Saved': 'Gerettet', 'Saved': 'Gerettet',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'fr': { 'fr': {
'Paste a link': 'Coller un lien', 'Paste a link': 'Coller un lien',
@ -83,6 +87,7 @@ extension Localization on String {
'Zoom': 'Zoom', 'Zoom': 'Zoom',
'Saved': 'Enregistrée', 'Saved': 'Enregistrée',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'zh_CN': { 'zh_CN': {
'Paste a link': '粘贴链接', 'Paste a link': '粘贴链接',
@ -99,6 +104,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',
}, },
'ko': { 'ko': {
'Paste a link': '링크를 붙여넣어 주세요.', 'Paste a link': '링크를 붙여넣어 주세요.',
@ -115,6 +121,7 @@ extension Localization on String {
'Zoom': '확대하기', 'Zoom': '확대하기',
'Saved': '저장되었습니다.', 'Saved': '저장되었습니다.',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'ru': { 'ru': {
'Paste a link': 'Вставить ссылку', 'Paste a link': 'Вставить ссылку',
@ -131,6 +138,7 @@ extension Localization on String {
'Zoom': 'Увеличить', 'Zoom': 'Увеличить',
'Saved': 'Сохранено', 'Saved': 'Сохранено',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'es': { 'es': {
'Paste a link': 'Pega un enlace', 'Paste a link': 'Pega un enlace',
@ -148,6 +156,7 @@ extension Localization on String {
'Zoom': 'Zoom', 'Zoom': 'Zoom',
'Saved': 'Salvado', 'Saved': 'Salvado',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'tr': { 'tr': {
'Paste a link': 'Bağlantıyı Yapıştır', 'Paste a link': 'Bağlantıyı Yapıştır',
@ -164,6 +173,7 @@ extension Localization on String {
'Zoom': 'yakınlaştır', 'Zoom': 'yakınlaştır',
'Saved': 'kaydedildi', 'Saved': 'kaydedildi',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'uk': { 'uk': {
'Paste a link': 'Вставити посилання', 'Paste a link': 'Вставити посилання',
@ -180,6 +190,7 @@ extension Localization on String {
'Zoom': 'Збільшити', 'Zoom': 'Збільшити',
'Saved': 'Збережено', 'Saved': 'Збережено',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'pt': { 'pt': {
'Paste a link': 'Colar um link', 'Paste a link': 'Colar um link',
@ -197,6 +208,7 @@ extension Localization on String {
'Zoom': 'Ampliação', 'Zoom': 'Ampliação',
'Saved': 'Salvou', 'Saved': 'Salvou',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'pl': { 'pl': {
'Paste a link': 'Wklej link', 'Paste a link': 'Wklej link',
@ -213,6 +225,7 @@ extension Localization on String {
'Zoom': 'Powiększenie', 'Zoom': 'Powiększenie',
'Saved': 'Zapisane', 'Saved': 'Zapisane',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
'vi': { 'vi': {
'Paste a link': 'Chèn liên kết', 'Paste a link': 'Chèn liên kết',
@ -229,6 +242,7 @@ extension Localization on String {
'Zoom': 'Thu phóng', 'Zoom': 'Thu phóng',
'Saved': 'Đã lưu', 'Saved': 'Đã lưu',
'Text': 'Text', 'Text': 'Text',
'What is entered is not a link': 'What is entered is not a link',
}, },
}; };

@ -31,7 +31,7 @@ class LinkDialogState extends State<LinkDialog> {
content: TextField( content: TextField(
style: widget.dialogTheme?.inputTextStyle, style: widget.dialogTheme?.inputTextStyle,
decoration: InputDecoration( decoration: InputDecoration(
labelText: 'Link'.i18n, labelText: 'Paste a link'.i18n,
labelStyle: widget.dialogTheme?.labelTextStyle, labelStyle: widget.dialogTheme?.labelTextStyle,
floatingLabelStyle: widget.dialogTheme?.labelTextStyle), floatingLabelStyle: widget.dialogTheme?.labelTextStyle),
autofocus: true, autofocus: true,

@ -1,12 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:tuple/tuple.dart';
import '../../models/documents/attribute.dart'; import '../../models/documents/attribute.dart';
import '../../models/rules/insert.dart';
import '../../models/themes/quill_dialog_theme.dart'; import '../../models/themes/quill_dialog_theme.dart';
import '../../models/themes/quill_icon_theme.dart'; import '../../models/themes/quill_icon_theme.dart';
import '../../translations/toolbar.i18n.dart'; import '../../translations/toolbar.i18n.dart';
import '../controller.dart'; import '../controller.dart';
import '../toolbar.dart'; import '../toolbar.dart';
import 'link_dialog.dart';
class LinkStyleButton extends StatefulWidget { class LinkStyleButton extends StatefulWidget {
const LinkStyleButton({ const LinkStyleButton({
@ -96,11 +97,19 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
} }
void _openLinkDialog(BuildContext context) { void _openLinkDialog(BuildContext context) {
showDialog<String>( showDialog<dynamic>(
context: context, context: context,
builder: (ctx) { builder: (ctx) {
final link = _getLinkAttributeValue(); 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); ).then(_linkSubmitted);
} }
@ -112,10 +121,111 @@ class _LinkStyleButtonState extends State<LinkStyleButton> {
?.value; ?.value;
} }
void _linkSubmitted(String? value) { void _linkSubmitted(dynamic value) {
if (value == null || value.isEmpty) { // text.isNotEmpty && link.isNotEmpty
return; 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));
} }
} }

Loading…
Cancel
Save