From 14b7813243aa74da60d182d514fb94a9b099fcb0 Mon Sep 17 00:00:00 2001 From: Filip Krawczyk Date: Tue, 28 Jun 2022 00:40:37 +0200 Subject: [PATCH] add functionality to more buttons * add several extensions on TextEditingController for convinience * add "add link" dialog + functionality * add functionality to surround buttons: * italics, * stikethough, * superscript, * subscript, * code --- lib/widgets/editor/toolbar.dart | 199 ++++++++++++++++++++++++++------ 1 file changed, 164 insertions(+), 35 deletions(-) diff --git a/lib/widgets/editor/toolbar.dart b/lib/widgets/editor/toolbar.dart index 7788124..3c4a02b 100644 --- a/lib/widgets/editor/toolbar.dart +++ b/lib/widgets/editor/toolbar.dart @@ -1,31 +1,61 @@ import 'package:flutter/material.dart'; import 'package:flutter_hooks/flutter_hooks.dart'; +import '../../util/extensions/spaced.dart'; + +class Reformat { + final String text; + final int selectionBeginningShift; + final int selectionEndingShift; + Reformat({ + required this.text, + this.selectionBeginningShift = 0, + this.selectionEndingShift = 0, + }); +} + extension on TextEditingController { + String get selectionText => + text.substring(selection.baseOffset, selection.extentOffset); + String get beforeSelectionText => text.substring(0, selection.baseOffset); + String get afterSelectionText => text.substring(selection.extentOffset); + /// surroungs selection with given strings. If nothing is selected, placeholder is used in the middle - void surround(String before, String after, - [String placeholder = '[write text here]']) { + void surround( + String before, [ + String? after, + String placeholder = '[write text here]', + ]) { + after ??= before; final beg = text.substring(0, selection.baseOffset); - final mid = text.substring(selection.baseOffset, selection.extentOffset); + final mid = () { + final m = text.substring(selection.baseOffset, selection.extentOffset); + if (m.isEmpty) return placeholder; + return m; + }(); final end = text.substring(selection.extentOffset); - if (mid.isEmpty) { - value = value.copyWith( - text: '$beg$before$placeholder$after$end', + value = value.copyWith( + text: '$beg$before$mid$after$end', selection: selection.copyWith( baseOffset: selection.baseOffset + before.length, - extentOffset: - selection.baseOffset + before.length + placeholder.length, - ), - ); - } else { - value = value.copyWith( - text: '$beg$before$mid$after$end', - selection: selection.copyWith( - baseOffset: selection.baseOffset + before.length, - extentOffset: selection.extentOffset + before.length, - )); - } + extentOffset: selection.baseOffset + before.length + mid.length, + )); + } + + void reformat(Reformat Function(String selection) reformatter) { + final beg = beforeSelectionText; + final mid = selectionText; + final end = afterSelectionText; + + final r = reformatter(mid); + value = value.copyWith( + text: '$beg${r.text}$end', + selection: selection.copyWith( + baseOffset: selection.baseOffset + r.selectionBeginningShift, + extentOffset: selection.extentOffset + r.selectionEndingShift, + ), + ); } } @@ -46,31 +76,68 @@ class Toolbar extends HookWidget { child: Row( children: [ IconButton( - onPressed: () { - controller.surround('**', '**'); - }, - icon: const Icon(Icons.format_bold)), + onPressed: () => controller.surround('**'), + icon: const Icon(Icons.format_bold), + ), IconButton( - onPressed: () {}, icon: const Icon(Icons.format_italic)), - IconButton(onPressed: () {}, icon: const Icon(Icons.link)), - IconButton(onPressed: () {}, icon: const Icon(Icons.image)), + onPressed: () => controller.surround('*'), + icon: const Icon(Icons.format_italic), + ), IconButton( - onPressed: () {}, icon: const Icon(Icons.h_mobiledata)), + onPressed: () async { + final r = await AddLinkDialog.show( + context, controller.selectionText); + if (r != null) controller.reformat((_) => r); + }, + icon: const Icon(Icons.link), + ), IconButton( - onPressed: () {}, - icon: const Icon(Icons.format_strikethrough)), + onPressed: () {}, + icon: const Icon(Icons.image), + ), IconButton( - onPressed: () {}, icon: const Icon(Icons.format_quote)), + onPressed: () {}, + icon: const Icon(Icons.person), + ), + IconButton( + onPressed: () { + // + }, + icon: const Icon(Icons.home), + ), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.h_mobiledata), + ), + IconButton( + onPressed: () => controller.surround('~~'), + icon: const Icon(Icons.format_strikethrough), + ), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.format_quote), + ), IconButton( onPressed: () {}, icon: const Icon(Icons.format_list_bulleted)), - IconButton(onPressed: () {}, icon: const Icon(Icons.code)), - IconButton(onPressed: () {}, icon: const Icon(Icons.subscript)), - IconButton(onPressed: () {}, icon: const Icon(Icons.superscript)), - // IconButton(onPressed: () {}, icon: const Icon(Icons.warning)),//spoiler - IconButton(onPressed: () {}, icon: const Icon(Icons.superscript)), IconButton( - onPressed: () {}, icon: const Icon(Icons.question_mark)), + onPressed: () => controller.surround('`'), + icon: const Icon(Icons.code), + ), + IconButton( + onPressed: () => controller.surround('~'), + icon: const Icon(Icons.subscript), + ), + IconButton( + onPressed: () => controller.surround('^'), + icon: const Icon(Icons.superscript), + ), + //spoiler + IconButton(onPressed: () {}, icon: const Icon(Icons.warning)), + IconButton( + onPressed: () {}, + icon: const Icon(Icons.question_mark), + ), ], ), ), @@ -78,3 +145,65 @@ class Toolbar extends HookWidget { ); } } + +class AddLinkDialog extends HookWidget { + final String title; + final String url; + final String selection; + + AddLinkDialog(this.selection) + : title = selection.startsWith('http?s://') ? '' : selection, + url = selection.startsWith('http?s://') ? selection : ''; + + @override + Widget build(BuildContext context) { + final titleController = useTextEditingController(text: title); + final urlController = useTextEditingController(text: url); + + void submit() { + final finalString = '(${titleController.text})[${urlController.text}]'; + Navigator.of(context).pop(Reformat( + text: finalString, + selectionBeginningShift: finalString.length, + selectionEndingShift: finalString.length - selection.length, + )); + } + + return AlertDialog( + title: const Text('Add link'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + controller: titleController, + decoration: const InputDecoration(hintText: 'title'), + textInputAction: TextInputAction.next, + autofocus: true, + ), + TextField( + controller: urlController, + decoration: const InputDecoration(hintText: 'https://example.com'), + onEditingComplete: submit, + autocorrect: false, + ), + ].spaced(10), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(context).pop(), + child: const Text('Cancel')), + ElevatedButton( + onPressed: submit, + child: const Text('Add link'), + ) + ], + ); + } + + static Future show(BuildContext context, String selection) async { + return showDialog( + context: context, + builder: (context) => AddLinkDialog(selection), + ); + } +}