diff --git a/CHANGELOG.md b/CHANGELOG.md index de0c0b8..d0910db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +## [2.3.0] - 2023/03/24 + +* Add support for custom action buttons +* Add dialog mode on pictures +* Remove Google dependency + ## [2.2.0] - 2023/01/02 * Change the way of adding links to the input. Now, you need to select the text you want to be the label, then click the link button. diff --git a/README.md b/README.md index cc8da95..d31803f 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,8 @@ MarkdownEditableTextInput is a TextField Widget that allow you to convert easily - [x] Convert to Code, Quote, Links - [x] Convert to Heading (H1, H2, H3, H4, H5, H6) and Links - [x] Support text direction +- [x] Dialog mode to enter link and picture +- [x] Possibility to add custom buttons to the action bar ## Demo ![](pictures/test_edition.gif) @@ -37,6 +39,10 @@ The color of the MarkdownTextInput is defined by the color set in your Theme : | TextEditingController controller | TextEditingController() | Pass your own controller. Can be used to clear the input for example | | TextStyle textStyle | Theme.of(context).textTheme.bodyText2 | Overrides input text style | | bool insertLinksByDialog; | true | Choose to use dialog or not to insert link | +| bool insertImageByDialog; | true | Choose to use dialog or not to insert an image | +| bool insertImageByDialog; | true | Choose to use dialog or not to insert an image | +| bool customCancelDialogText; | String? | Text used by dialog for close dialog action | +| bool customSubmitDialogText; | String? | Text used by dialog for validate dialog action | ### Example You can see an example of how to use this package [here](https://github.com/playmoweb/markdown-editable-textinput/tree/master/example) diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..da44bc1 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.3-all.zip diff --git a/example/lib/main.dart b/example/lib/main.dart index 675f09e..2bdb0bd 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -57,6 +57,9 @@ class _MyAppState extends State { actions: MarkdownType.values, controller: controller, textStyle: TextStyle(fontSize: 16), + optionnalActionButtons: [ + ActionButton(widget: Icon(Icons.add), action: () => controller.text = '${controller.text} test ') + ], ), TextButton( onPressed: () { diff --git a/example/pubspec.lock b/example/pubspec.lock index 79c7c48..e790cc5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -95,20 +95,6 @@ packages: description: flutter source: sdk version: "0.0.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" markdown: dependency: transitive description: @@ -122,7 +108,7 @@ packages: path: ".." relative: true source: path - version: "3.0.0" + version: "2.3.0" matcher: dependency: transitive description: @@ -191,13 +177,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.3" - translator: - dependency: transitive - description: - name: translator - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.7" typed_data: dependency: transitive description: diff --git a/lib/format_markdown.dart b/lib/format_markdown.dart index 72c9403..55d4ad4 100644 --- a/lib/format_markdown.dart +++ b/lib/format_markdown.dart @@ -66,7 +66,7 @@ class FormatMarkdown { replaceCursorIndex = 0; break; case MarkdownType.image: - changedData = '![${data.substring(fromIndex, toIndex)}](${data.substring(fromIndex, toIndex)})'; + changedData = '![$selectedText](${link ?? selectedText})'; replaceCursorIndex = 3; break; } @@ -132,6 +132,17 @@ enum MarkdownType { image, } +/// Custom button object +class ActionButton { + /// [widget] is the icon in the action bar + final Widget widget; + /// Action to perform when button is pressed + final Function() action; + + /// return [ActionButton] + ActionButton({required this.widget, required this.action}); +} + /// Add data to [MarkdownType] enum extension MarkownTypeExtension on MarkdownType { /// Get String used in widget's key diff --git a/lib/markdown_text_input.dart b/lib/markdown_text_input.dart index 1690566..7a33e8b 100644 --- a/lib/markdown_text_input.dart +++ b/lib/markdown_text_input.dart @@ -5,7 +5,6 @@ import 'package:expandable/expandable.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:markdown_editable_textinput/format_markdown.dart'; -import 'package:translator/translator.dart'; /// Widget with markdown buttons class MarkdownTextInput extends StatefulWidget { @@ -30,6 +29,9 @@ class MarkdownTextInput extends StatefulWidget { /// List of action the component can handle final List actions; + /// List of custom action buttons + final List optionnalActionButtons; + /// Optional controller to manage the input final TextEditingController? controller; @@ -40,25 +42,46 @@ class MarkdownTextInput extends StatefulWidget { /// Default value is true. final bool insertLinksByDialog; + /// If you prefer to use the dialog to insert image, you can choose to use the markdown syntax directly by setting [insertImageByDialog] to false. In this case, the selected text will be used as label and link. + /// Default value is true. + final bool insertImageByDialog; + + /// InputDecoration for the text input of the link dialog + final InputDecoration? linkDialogLinkDecoration; + + /// InputDecoration for the link input of the link dialog + final InputDecoration? linkDialogTextDecoration; + + /// InputDecoration for the text input of the image dialog + final InputDecoration? imageDialogLinkDecoration; + + /// InputDecoration for the link input of the image dialog + final InputDecoration? imageDialogTextDecoration; + + /// Custom text for cancel button in dialogs + final String? customCancelDialogText; + + /// Custom text for submit button in dialogs + final String? customSubmitDialogText; + /// Constructor for [MarkdownTextInput] - MarkdownTextInput( - this.onTextChanged, - this.initialValue, { - this.label = '', - this.validators, - this.textDirection = TextDirection.ltr, - this.maxLines = 10, - this.actions = const [ - MarkdownType.bold, - MarkdownType.italic, - MarkdownType.title, - MarkdownType.link, - MarkdownType.list - ], - this.textStyle, - this.controller, - this.insertLinksByDialog = true, - }); + MarkdownTextInput(this.onTextChanged, this.initialValue, + {this.label = '', + this.validators, + this.textDirection = TextDirection.ltr, + this.maxLines = 10, + this.actions = const [MarkdownType.bold, MarkdownType.italic, MarkdownType.title, MarkdownType.link, MarkdownType.list], + this.textStyle, + this.controller, + this.insertLinksByDialog = true, + this.insertImageByDialog = true, + this.linkDialogLinkDecoration, + this.linkDialogTextDecoration, + this.imageDialogLinkDecoration, + this.imageDialogTextDecoration, + this.customCancelDialogText, + this.customSubmitDialogText, + this.optionnalActionButtons = const []}); @override _MarkdownTextInputState createState() => _MarkdownTextInputState(controller ?? TextEditingController()); @@ -78,11 +101,10 @@ class _MarkdownTextInputState extends State { var fromIndex = textSelection.baseOffset; var toIndex = textSelection.extentOffset; - final result = FormatMarkdown.convertToMarkdown(type, _controller.text, fromIndex, toIndex, - titleSize: titleSize, link: link, selectedText: selectedText ?? _controller.text.substring(fromIndex, toIndex)); + final result = + FormatMarkdown.convertToMarkdown(type, _controller.text, fromIndex, toIndex, titleSize: titleSize, link: link, selectedText: selectedText ?? _controller.text.substring(fromIndex, toIndex)); - _controller.value = _controller.value - .copyWith(text: result.data, selection: TextSelection.collapsed(offset: basePosition + result.cursorIndex)); + _controller.value = _controller.value.copyWith(text: result.data, selection: TextSelection.collapsed(offset: basePosition + result.cursorIndex)); if (noTextSelected) { _controller.selection = TextSelection.collapsed(offset: _controller.selection.end - result.replaceCursorIndex); @@ -128,10 +150,8 @@ class _MarkdownTextInputState extends State { cursorColor: Theme.of(context).primaryColor, textDirection: widget.textDirection, decoration: InputDecoration( - enabledBorder: - UnderlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary)), - focusedBorder: - UnderlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary)), + enabledBorder: UnderlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary)), + focusedBorder: UnderlineInputBorder(borderSide: BorderSide(color: Theme.of(context).colorScheme.secondary)), hintText: widget.label, hintStyle: const TextStyle(color: Color.fromRGBO(63, 61, 86, 0.5)), contentPadding: const EdgeInsets.symmetric(vertical: 15, horizontal: 10), @@ -144,9 +164,9 @@ class _MarkdownTextInputState extends State { borderRadius: const BorderRadius.only(bottomLeft: Radius.circular(10), bottomRight: Radius.circular(10)), child: ListView( scrollDirection: Axis.horizontal, - children: widget.actions.map((type) { - switch (type) { - case MarkdownType.title: + children: [ + ...widget.actions.map((type) { + if (type == MarkdownType.title) { return ExpandableNotifier( child: Expandable( key: Key('H#_button'), @@ -190,105 +210,32 @@ class _MarkdownTextInputState extends State { ), ), ); - case MarkdownType.link: + } else if (type == MarkdownType.link || type == MarkdownType.image) { return _basicInkwell( type, - customOnTap: !widget.insertLinksByDialog + customOnTap: (type == MarkdownType.link ? !widget.insertLinksByDialog : !widget.insertImageByDialog) ? null : () async { - var text = - _controller.text.substring(textSelection.baseOffset, textSelection.extentOffset); + var text = _controller.text.substring(textSelection.baseOffset, textSelection.extentOffset); var textController = TextEditingController()..text = text; var linkController = TextEditingController(); - var textFocus = FocusNode(); - var linkFocus = FocusNode(); var color = Theme.of(context).colorScheme.secondary; - var language = - kIsWeb ? window.locale.languageCode : Platform.localeName.substring(0, 2); - - var textLabel = 'Text'; - var linkLabel = 'Link'; - try { - var textTranslation = await GoogleTranslator().translate(textLabel, to: language); - textLabel = textTranslation.text; - - var linkTranslation = await GoogleTranslator().translate(linkLabel, to: language); - linkLabel = linkTranslation.text; - } catch (e) { - textLabel = 'Text'; - linkLabel = 'Link'; - } - - await showDialog( - context: context, - builder: (context) { - return AlertDialog( - title: Row( - mainAxisAlignment: MainAxisAlignment.end, - children: [ - GestureDetector( - child: Icon(Icons.close), onTap: () => Navigator.pop(context)) - ], - ), - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - TextField( - controller: textController, - decoration: InputDecoration( - hintText: 'example', - label: Text(textLabel), - labelStyle: TextStyle(color: color), - focusedBorder: - OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), - enabledBorder: - OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), - ), - autofocus: text.isEmpty, - focusNode: textFocus, - textInputAction: TextInputAction.next, - onSubmitted: (value) { - textFocus.unfocus(); - FocusScope.of(context).requestFocus(linkFocus); - }, - ), - SizedBox(height: 10), - TextField( - controller: linkController, - decoration: InputDecoration( - hintText: 'https://example.com', - label: Text(linkLabel), - labelStyle: TextStyle(color: color), - focusedBorder: - OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), - enabledBorder: - OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), - ), - autofocus: text.isNotEmpty, - focusNode: linkFocus, - ), - ], - ), - contentPadding: EdgeInsets.fromLTRB(24.0, 20.0, 24.0, 0), - actions: [ - TextButton( - onPressed: () { - onTap(type, link: linkController.text, selectedText: textController.text); - Navigator.pop(context); - }, - child: const Text('OK'), - ), - ], - ); - }); + + await _basicDialog(textController, linkController, color, text, type); }, ); - default: + } else { return _basicInkwell(type); - } - }).toList(), + } + }).toList(), + + + ...widget.optionnalActionButtons.map((ActionButton optionActionButton) { + return _basicInkwell(optionActionButton, customOnTap: optionActionButton.action); + }).toList() + ], ), ), ) @@ -297,14 +244,102 @@ class _MarkdownTextInputState extends State { ); } - Widget _basicInkwell(MarkdownType type, {Function? customOnTap}) { - return InkWell( - key: Key(type.key), - onTap: () => customOnTap != null ? customOnTap() : onTap(type), - child: Padding( - padding: EdgeInsets.all(10), - child: Icon(type.icon), - ), - ); + Widget _basicInkwell(dynamic item, {Function? customOnTap}) { + Widget widgetToReturn = SizedBox.shrink(); + + if (item is MarkdownType) { + return InkWell( + key: Key(item.key), + onTap: () => customOnTap != null ? customOnTap() : onTap(item), + child: Padding( + padding: EdgeInsets.all(10), + child: Icon(item.icon), + ), + ); + } else if (item is ActionButton) { + return InkWell( + onTap: item.action, + child: Padding( + padding: EdgeInsets.all(10), + child: item.widget, + ), + ); + } + + return widgetToReturn; + } + + Future _basicDialog( + TextEditingController textController, + TextEditingController linkController, + Color color, + String text, + MarkdownType type, + ) async { + var finalTextInputDecoration = type == MarkdownType.link ? widget.linkDialogTextDecoration : widget.imageDialogTextDecoration; + var finalLinkInputDecoration = type == MarkdownType.link ? widget.linkDialogLinkDecoration : widget.imageDialogLinkDecoration; + + var textFocus = FocusNode(); + var linkFocus = FocusNode(); + + return await showDialog( + context: context, + builder: (context) { + return AlertDialog( + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: textController, + decoration: finalTextInputDecoration ?? + InputDecoration( + hintText: 'Example text', + label: Text('Text'), + labelStyle: TextStyle(color: color), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), + ), + autofocus: text.isEmpty, + focusNode: textFocus, + textInputAction: TextInputAction.next, + onSubmitted: (value) { + textFocus.unfocus(); + FocusScope.of(context).requestFocus(linkFocus); + }, + ), + SizedBox(height: 10), + TextField( + controller: linkController, + decoration: finalLinkInputDecoration ?? + InputDecoration( + hintText: 'https://example.com', + label: Text('Link'), + labelStyle: TextStyle(color: color), + focusedBorder: OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), + enabledBorder: OutlineInputBorder(borderSide: BorderSide(color: color, width: 2)), + ), + autofocus: text.isNotEmpty, + focusNode: linkFocus, + ), + ], + ), + contentPadding: EdgeInsets.fromLTRB(24.0, 32.0, 24.0, 12.0), + actions: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(widget.customCancelDialogText ?? 'Cancel'), + ), + TextButton( + onPressed: () { + onTap(type, link: linkController.text, selectedText: textController.text); + Navigator.pop(context); + }, + child: Text(widget.customSubmitDialogText ?? 'OK'), + ), + ], + ); + }); } } diff --git a/pubspec.lock b/pubspec.lock index cef1c63..c925182 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,20 +74,6 @@ packages: description: flutter source: sdk version: "0.0.0" - http: - dependency: transitive - description: - name: http - url: "https://pub.dartlang.org" - source: hosted - version: "0.13.5" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.dartlang.org" - source: hosted - version: "4.0.2" matcher: dependency: transitive description: @@ -156,13 +142,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.3" - translator: - dependency: "direct main" - description: - name: translator - url: "https://pub.dartlang.org" - source: hosted - version: "0.1.7" typed_data: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 7dd57c6..ec5204d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: markdown_editable_textinput description: A TextField Widget that allow you to convert easily what's in the TextField to Markdown. -version: 2.2.0 +version: 2.3.0 homepage: https://github.com/playmoweb/markdown-editable-textinput repository: https://github.com/playmoweb/markdown-editable-textinput @@ -12,8 +12,6 @@ dependencies: sdk: flutter effective_dart: ^1.3.2 expandable: ^5.0.1 - translator: ^0.1.7 - dev_dependencies: flutter_test: