Skip to content

Commit 289139b

Browse files
CatHood0CatHood0EchoEllet
authored
Feat: customizable character and space shortcut events (singerdmx#2228)
* Added character shortcut events class * Feat: support for character and space shortcut events * Chore: added new gif video url * Chore: improve and fix some parts of the new section in the README.md * Merge from master * Fix: typo in comment into the Shortcut event section * chore: add a message for validating an assert in handleFormatByWrappingWithSingleCharacter() * chore: fix analysis warning * Chore: moved shortcut docs to its own file * Chore: changed editor without shortcut gif url * Chore: added description to asserts * Chore: removed unnecessary default cases in format functions * Fix: characterShortcutEvents param in raw configs is showing as it is deprecated * Chore: dart format * Chore: fixed some comments that references double chars instead single chars * Fix: typo * Chore: improved doc comments on format functions * Chore: dart format --------- Co-authored-by: CatHood0 <[email protected]> Co-authored-by: Ellet <[email protected]>
1 parent 164c183 commit 289139b

19 files changed

+807
-46
lines changed

README.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,13 @@ You can join our [Slack Group] for discussion.
5858
- [📦 Embed Blocks](#-embed-blocks)
5959
- [🔄 Conversion to HTML](#-conversion-to-html)
6060
- [📝 Spelling checker](#-spelling-checker)
61+
- [✂️ Shortcut events](#-shortcut-events)
6162
- [🌐 Translation](#-translation)
6263
- [🧪 Testing](#-testing)
6364
- [🤝 Contributing](#-contributing)
6465
- [📜 Acknowledgments](#-acknowledgments)
6566

67+
6668
## 📸 Screenshots
6769

6870
<details>
@@ -290,6 +292,16 @@ It's implemented using the package `simple_spell_checker` in the [Example](./exa
290292

291293
Take a look at [Spelling Checker](./doc/spell_checker.md) page for more info.
292294

295+
## ✂️ Shortcut events
296+
297+
We can customize some Shorcut events, using the parameters `characterShortcutEvents` or `spaceShortcutEvents` from `QuillEditorConfigurations` to add more functionality to our editor.
298+
299+
> [!NOTE]
300+
>
301+
> You can get all standard shortcuts using `standardCharactersShortcutEvents` or `standardSpaceShorcutEvents`
302+
303+
To see an example of this, you can check [customizing_shortcuts](./doc/customizing_shortcuts.md)
304+
293305
## 🌐 Translation
294306

295307
The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your

doc/customizing_shortcuts.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# Shortcut events
2+
3+
We will use a simple example to illustrate how to quickly add a `CharacterShortcutEvent` event.
4+
5+
In this example, text that starts and ends with an asterisk ( * ) character will be rendered in italics for emphasis. So typing `*xxx*` will automatically be converted into _`xxx`_.
6+
7+
Let's start with a empty document:
8+
9+
```dart
10+
import 'package:flutter_quill/flutter_quill.dart';
11+
import 'package:flutter/material.dart';
12+
13+
class AsteriskToItalicStyle extends StatelessWidget {
14+
const AsteriskToItalicStyle({super.key});
15+
16+
@override
17+
Widget build(BuildContext context) {
18+
return QuillEditor(
19+
scrollController: <your_scrollController>,
20+
focusNode: <your_focusNode>,
21+
controller: <your_controller>,
22+
configurations: QuillEditorConfigurations(
23+
characterShortcutEvents: [],
24+
),
25+
);
26+
}
27+
}
28+
```
29+
30+
At this point, nothing magic will happen after typing `*xxx*`.
31+
32+
<p align="center">
33+
<img src="https://github.com/user-attachments/assets/c9ab15ec-2ada-4a84-96e8-55e6145e7925" width="800px" alt="Editor without shortcuts gif">
34+
</p>
35+
36+
To implement our shortcut event we will create a `CharacterShortcutEvent` instance to handle an asterisk input.
37+
38+
We need to define key and character in a `CharacterShortcutEvent` object to customize hotkeys. We recommend using the description of your event as a key. For example, if the asterisk `*` is defined to make text italic, the key can be 'Asterisk to italic'.
39+
40+
```dart
41+
import 'package:flutter_quill/flutter_quill.dart';
42+
import 'package:flutter/material.dart';
43+
44+
// [handleFormatByWrappingWithSingleCharacter] is a example function that contains
45+
// the necessary logic to replace asterisk characters and apply correctly the
46+
// style to the text around them
47+
48+
enum SingleCharacterFormatStyle {
49+
code,
50+
italic,
51+
strikethrough,
52+
}
53+
54+
CharacterShortcutEvent asteriskToItalicStyleEvent = CharacterShortcutEvent(
55+
key: 'Asterisk to italic',
56+
character: '*',
57+
handler: (QuillController controller) => handleFormatByWrappingWithSingleCharacter(
58+
controller: controller,
59+
character: '*',
60+
formatStyle: SingleCharacterFormatStyle.italic,
61+
),
62+
);
63+
```
64+
65+
Now our 'asterisk handler' function is done and the only task left is to inject it into the `QuillEditorConfigurations`.
66+
67+
```dart
68+
import 'package:flutter_quill/flutter_quill.dart';
69+
import 'package:flutter/material.dart';
70+
71+
class AsteriskToItalicStyle extends StatelessWidget {
72+
const AsteriskToItalicStyle({super.key});
73+
74+
@override
75+
Widget build(BuildContext context) {
76+
return QuillEditor(
77+
scrollController: <your_scrollController>,
78+
focusNode: <your_focusNode>,
79+
controller: <your_controller>,
80+
configurations: QuillEditorConfigurations(
81+
characterShortcutEvents: [
82+
asteriskToItalicStyleEvent,
83+
],
84+
),
85+
);
86+
}
87+
}
88+
89+
CharacterShortcutEvent asteriskToItalicStyleEvent = CharacterShortcutEvent(
90+
key: 'Asterisk to italic',
91+
character: '*',
92+
handler: (QuillController controller) => handleFormatByWrappingWithSingleCharacter(
93+
controller: controller,
94+
character: '*',
95+
formatStyle: SingleCharacterFormatStyle.italic,
96+
),
97+
);
98+
```
99+
<p align="center">
100+
<img src="https://github.com/user-attachments/assets/35e74cbf-1bd8-462d-bb90-50d712012c90" width="800px" alt="Editor with shortcuts gif">
101+
</p>

example/lib/screens/quill/quill_screen.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,6 +124,8 @@ class _QuillScreenState extends State<QuillScreen> {
124124
child: MyQuillEditor(
125125
controller: _controller,
126126
configurations: QuillEditorConfigurations(
127+
characterShortcutEvents: standardCharactersShortcutEvents,
128+
spaceShortcutEvents: standardSpaceShorcutEvents,
127129
searchConfigurations: const QuillSearchConfigurations(
128130
searchEmbedMode: SearchEmbedMode.plainText,
129131
),

lib/flutter_quill.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export 'src/editor/editor.dart';
2121
export 'src/editor/embed/embed_editor_builder.dart';
2222
export 'src/editor/provider.dart';
2323
export 'src/editor/raw_editor/builders/leading_block_builder.dart';
24+
export 'src/editor/raw_editor/config/events/events.dart';
2425
export 'src/editor/raw_editor/config/raw_editor_configurations.dart';
2526
export 'src/editor/raw_editor/quill_single_child_scroll_view.dart';
2627
export 'src/editor/raw_editor/raw_editor.dart';

lib/src/editor/config/editor_configurations.dart

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import '../../toolbar/theme/quill_dialog_theme.dart';
1212
import '../editor_builder.dart';
1313
import '../embed/embed_editor_builder.dart';
1414
import '../raw_editor/builders/leading_block_builder.dart';
15+
import '../raw_editor/config/events/events.dart';
1516
import '../raw_editor/raw_editor.dart';
1617
import '../widgets/default_styles.dart';
1718
import '../widgets/delegate.dart';
@@ -33,6 +34,8 @@ class QuillEditorConfigurations extends Equatable {
3334
this.sharedConfigurations = const QuillSharedConfigurations(),
3435
this.scrollable = true,
3536
this.padding = EdgeInsets.zero,
37+
this.characterShortcutEvents = const [],
38+
this.spaceShortcutEvents = const [],
3639
this.autoFocus = false,
3740
this.expands = false,
3841
this.placeholder,
@@ -57,6 +60,8 @@ class QuillEditorConfigurations extends Equatable {
5760
this.onSingleLongTapStart,
5861
this.onSingleLongTapMoveUpdate,
5962
this.onSingleLongTapEnd,
63+
@Deprecated(
64+
'Use space/char shortcut events instead - enableMarkdownStyleConversion will be removed in future releases.')
6065
this.enableMarkdownStyleConversion = true,
6166
this.enableAlwaysIndentOnTab = false,
6267
this.embedBuilders,
@@ -102,6 +107,52 @@ class QuillEditorConfigurations extends Equatable {
102107
/// The text placeholder in the quill editor
103108
final String? placeholder;
104109

110+
/// Contains all the events that will be handled when
111+
/// the exact characters satifies the condition. This mean
112+
/// if you press asterisk key, if you have a `CharacterShortcutEvent` with
113+
/// the asterisk then that event will be handled
114+
///
115+
/// Supported by:
116+
///
117+
/// - Web
118+
/// - Desktop
119+
/// ### Example
120+
///```dart
121+
/// // you can get also the default implemented shortcuts
122+
/// // calling [standardSpaceShorcutEvents]
123+
///final defaultShorcutsImplementation =
124+
/// List.from([...standardCharactersShortcutEvents])
125+
///
126+
///final boldFormat = CharacterShortcutEvent(
127+
/// key: 'Shortcut event that will format current wrapped text in asterisk'
128+
/// character: '*',
129+
/// handler: (controller) {...your implementation}
130+
///);
131+
///```
132+
final List<CharacterShortcutEvent> characterShortcutEvents;
133+
134+
/// Contains all the events that will be handled when
135+
/// space key is pressed
136+
///
137+
/// Supported by:
138+
///
139+
/// - Web
140+
/// - Desktop
141+
///
142+
/// ### Example
143+
///```dart
144+
/// // you can get also the default implemented shortcuts
145+
/// // calling [standardSpaceShorcutEvents]
146+
///final defaultShorcutsImplementation =
147+
/// List.from([...standardSpaceShorcutEvents])
148+
///
149+
///final spaceBulletList = SpaceShortcutEvent(
150+
/// character: '-',
151+
/// handler: (QuillText textNode, controller) {...your implementation}
152+
///);
153+
///```
154+
final List<SpaceShortcutEvent> spaceShortcutEvents;
155+
105156
/// Whether the text can be changed.
106157
///
107158
/// When this is set to `true`, the text cannot be modified
@@ -145,6 +196,10 @@ class QuillEditorConfigurations extends Equatable {
145196
/// This setting controls the behavior of input. Specifically, when enabled,
146197
/// entering '1.' followed by a space or '-' followed by a space
147198
/// will automatically convert the input into a Markdown list format.
199+
///
200+
/// ## !This functionality now does not work because was replaced by a more advanced using [SpaceShortcutEvent] and [CharacterShortcutEvent] classes
201+
@Deprecated(
202+
'enableMarkdownStyleConversion is no longer used and will be removed in future releases. Use space/char shortcut events instead.')
148203
final bool enableMarkdownStyleConversion;
149204

150205
/// Enables always indenting when the TAB key is pressed.
@@ -450,6 +505,8 @@ class QuillEditorConfigurations extends Equatable {
450505
LinkActionPickerDelegate? linkActionPickerDelegate,
451506
bool? floatingCursorDisabled,
452507
TextSelectionControls? textSelectionControls,
508+
List<CharacterShortcutEvent>? characterShortcutEvents,
509+
List<SpaceShortcutEvent>? spaceShortcutEvents,
453510
Future<String?> Function(Uint8List imageBytes)? onImagePaste,
454511
Future<String?> Function(Uint8List imageBytes)? onGifPaste,
455512
Map<ShortcutActivator, Intent>? customShortcuts,
@@ -483,8 +540,13 @@ class QuillEditorConfigurations extends Equatable {
483540
disableClipboard: disableClipboard ?? this.disableClipboard,
484541
scrollable: scrollable ?? this.scrollable,
485542
scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset,
543+
characterShortcutEvents:
544+
characterShortcutEvents ?? this.characterShortcutEvents,
545+
spaceShortcutEvents: spaceShortcutEvents ?? this.spaceShortcutEvents,
486546
padding: padding ?? this.padding,
547+
// ignore: deprecated_member_use_from_same_package
487548
enableMarkdownStyleConversion:
549+
// ignore: deprecated_member_use_from_same_package
488550
enableMarkdownStyleConversion ?? this.enableMarkdownStyleConversion,
489551
enableAlwaysIndentOnTab:
490552
enableAlwaysIndentOnTab ?? this.enableAlwaysIndentOnTab,

lib/src/editor/editor.dart

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -295,13 +295,14 @@ class QuillEditorState extends State<QuillEditor>
295295
key: _editorKey,
296296
controller: controller,
297297
configurations: QuillRawEditorConfigurations(
298+
characterShortcutEvents:
299+
widget.configurations.characterShortcutEvents,
300+
spaceShortcutEvents: widget.configurations.spaceShortcutEvents,
298301
customLeadingBuilder:
299302
widget.configurations.customLeadingBlockBuilder,
300303
focusNode: widget.focusNode,
301304
scrollController: widget.scrollController,
302305
scrollable: configurations.scrollable,
303-
enableMarkdownStyleConversion:
304-
configurations.enableMarkdownStyleConversion,
305306
enableAlwaysIndentOnTab: configurations.enableAlwaysIndentOnTab,
306307
scrollBottomInset: configurations.scrollBottomInset,
307308
padding: configurations.padding,
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
import 'package:equatable/equatable.dart';
2+
import 'package:meta/meta.dart';
3+
4+
import '../../../../../flutter_quill.dart';
5+
6+
typedef CharacterShortcutEventHandler = bool Function(
7+
QuillController controller);
8+
9+
/// Defines the implementation of shortcut event based on character.
10+
@immutable
11+
class CharacterShortcutEvent extends Equatable {
12+
const CharacterShortcutEvent({
13+
required this.key,
14+
required this.character,
15+
required this.handler,
16+
}) : assert(character.length == 1 && character != '\n',
17+
'character cannot be major than one char, and it must not be a new line');
18+
19+
final String key;
20+
final String character;
21+
final CharacterShortcutEventHandler handler;
22+
23+
bool execute(QuillController controller) {
24+
return handler(controller);
25+
}
26+
27+
CharacterShortcutEvent copyWith({
28+
String? key,
29+
String? character,
30+
CharacterShortcutEventHandler? handler,
31+
}) {
32+
return CharacterShortcutEvent(
33+
key: key ?? this.key,
34+
character: character ?? this.character,
35+
handler: handler ?? this.handler,
36+
);
37+
}
38+
39+
@override
40+
String toString() =>
41+
'CharacterShortcutEvent(key: $key, character: $character, handler: $handler)';
42+
43+
@override
44+
List<Object?> get props => [key, character, handler];
45+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
// event classes
2+
export 'character_shortcuts_events.dart';
3+
export 'space_shortcut_events.dart';
4+
// default implementation of the shortcuts
5+
export 'standard_char_shortcuts/block_shortcut_events_handlers.dart';
6+
export 'standard_char_shortcuts/double_character_shortcut_events.dart';
7+
export 'standard_char_shortcuts/single_character_shortcut_events.dart';
8+
// all available shortcuts
9+
export 'standard_char_shortcuts/standard_shortcut_events.dart';

0 commit comments

Comments
 (0)