Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: customizable character and space shortcut events #2228

Merged
merged 19 commits into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,11 +58,13 @@ You can join our [Slack Group] for discussion.
- [📦 Embed Blocks](#-embed-blocks)
- [🔄 Conversion to HTML](#-conversion-to-html)
- [📝 Spelling checker](#-spelling-checker)
- [✂️ Shortcut events](#-shortcut-events)
- [🌐 Translation](#-translation)
- [🧪 Testing](#-testing)
- [🤝 Contributing](#-contributing)
- [📜 Acknowledgments](#-acknowledgments)


## 📸 Screenshots

<details>
Expand Down Expand Up @@ -291,6 +293,16 @@ It's implemented using the package `simple_spell_checker` in the [Example](./exa

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

## ✂️ Shortcut events
EchoEllet marked this conversation as resolved.
Show resolved Hide resolved

We can customize some Shorcut events, using the parameters `characterShortcutEvents` or `spaceShortcutEvents` from `QuillEditorConfigurations` to add more functionality to our editor.

> [!NOTE]
>
> You can get all standard shortcuts using `standardCharactersShortcutEvents` or `standardSpaceShorcutEvents`

To see an example of this, you can check [customizing_shortcuts](./doc/customizing_shortcuts.md)

## 🌐 Translation

The package offers translations for the quill toolbar and editor, it will follow the system locale unless you set your
Expand Down
101 changes: 101 additions & 0 deletions doc/customizing_shortcuts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
# Shortcut events

We will use a simple example to illustrate how to quickly add a `CharacterShortcutEvent` event.

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`_.

Let's start with a empty document:

```dart
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/material.dart';

class AsteriskToItalicStyle extends StatelessWidget {
const AsteriskToItalicStyle({super.key});

@override
Widget build(BuildContext context) {
return QuillEditor(
scrollController: <your_scrollController>,
focusNode: <your_focusNode>,
controller: <your_controller>,
configurations: QuillEditorConfigurations(
characterShortcutEvents: [],
),
);
}
}
```

At this point, nothing magic will happen after typing `*xxx*`.

<p align="center">
<img src="https://github.com/user-attachments/assets/c9ab15ec-2ada-4a84-96e8-55e6145e7925" width="800px" alt="Editor without shortcuts gif">
</p>

To implement our shortcut event we will create a `CharacterShortcutEvent` instance to handle an asterisk input.

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'.

```dart
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/material.dart';

// [handleFormatByWrappingWithSingleCharacter] is a example function that contains
// the necessary logic to replace asterisk characters and apply correctly the
// style to the text around them

enum SingleCharacterFormatStyle {
code,
italic,
strikethrough,
}

CharacterShortcutEvent asteriskToItalicStyleEvent = CharacterShortcutEvent(
key: 'Asterisk to italic',
character: '*',
handler: (QuillController controller) => handleFormatByWrappingWithSingleCharacter(
controller: controller,
character: '*',
formatStyle: SingleCharacterFormatStyle.italic,
),
);
```

Now our 'asterisk handler' function is done and the only task left is to inject it into the `QuillEditorConfigurations`.

```dart
import 'package:flutter_quill/flutter_quill.dart';
import 'package:flutter/material.dart';

class AsteriskToItalicStyle extends StatelessWidget {
const AsteriskToItalicStyle({super.key});

@override
Widget build(BuildContext context) {
return QuillEditor(
scrollController: <your_scrollController>,
focusNode: <your_focusNode>,
controller: <your_controller>,
configurations: QuillEditorConfigurations(
characterShortcutEvents: [
asteriskToItalicStyleEvent,
],
),
);
}
}

CharacterShortcutEvent asteriskToItalicStyleEvent = CharacterShortcutEvent(
key: 'Asterisk to italic',
character: '*',
handler: (QuillController controller) => handleFormatByWrappingWithSingleCharacter(
controller: controller,
character: '*',
formatStyle: SingleCharacterFormatStyle.italic,
),
);
```
<p align="center">
<img src="https://github.com/user-attachments/assets/35e74cbf-1bd8-462d-bb90-50d712012c90" width="800px" alt="Editor with shortcuts gif">
</p>
2 changes: 2 additions & 0 deletions example/lib/screens/quill/quill_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ class _QuillScreenState extends State<QuillScreen> {
child: MyQuillEditor(
controller: _controller,
configurations: QuillEditorConfigurations(
characterShortcutEvents: standardCharactersShortcutEvents,
spaceShortcutEvents: standardSpaceShorcutEvents,
searchConfigurations: const QuillSearchConfigurations(
searchEmbedMode: SearchEmbedMode.plainText,
),
Expand Down
1 change: 1 addition & 0 deletions lib/flutter_quill.dart
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export 'src/editor/editor.dart';
export 'src/editor/embed/embed_editor_builder.dart';
export 'src/editor/provider.dart';
export 'src/editor/raw_editor/builders/leading_block_builder.dart';
export 'src/editor/raw_editor/config/events/events.dart';
export 'src/editor/raw_editor/config/raw_editor_configurations.dart';
export 'src/editor/raw_editor/quill_single_child_scroll_view.dart';
export 'src/editor/raw_editor/raw_editor.dart';
Expand Down
62 changes: 62 additions & 0 deletions lib/src/editor/config/editor_configurations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import '../../toolbar/theme/quill_dialog_theme.dart';
import '../editor_builder.dart';
import '../embed/embed_editor_builder.dart';
import '../raw_editor/builders/leading_block_builder.dart';
import '../raw_editor/config/events/events.dart';
import '../raw_editor/raw_editor.dart';
import '../widgets/default_styles.dart';
import '../widgets/delegate.dart';
Expand All @@ -33,6 +34,8 @@ class QuillEditorConfigurations extends Equatable {
this.sharedConfigurations = const QuillSharedConfigurations(),
this.scrollable = true,
this.padding = EdgeInsets.zero,
this.characterShortcutEvents = const [],
this.spaceShortcutEvents = const [],
this.autoFocus = false,
this.expands = false,
this.placeholder,
Expand All @@ -57,6 +60,8 @@ class QuillEditorConfigurations extends Equatable {
this.onSingleLongTapStart,
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
@Deprecated(
'Use space/char shortcut events instead - enableMarkdownStyleConversion will be removed in future releases.')
this.enableMarkdownStyleConversion = true,
this.enableAlwaysIndentOnTab = false,
this.embedBuilders,
Expand Down Expand Up @@ -102,6 +107,52 @@ class QuillEditorConfigurations extends Equatable {
/// The text placeholder in the quill editor
final String? placeholder;

/// Contains all the events that will be handled when
/// the exact characters satifies the condition. This mean
/// if you press asterisk key, if you have a `CharacterShortcutEvent` with
/// the asterisk then that event will be handled
///
/// Supported by:
///
/// - Web
/// - Desktop
/// ### Example
///```dart
/// // you can get also the default implemented shortcuts
/// // calling [standardSpaceShorcutEvents]
///final defaultShorcutsImplementation =
/// List.from([...standardCharactersShortcutEvents])
///
///final boldFormat = CharacterShortcutEvent(
/// key: 'Shortcut event that will format current wrapped text in asterisk'
/// character: '*',
/// handler: (controller) {...your implementation}
///);
///```
final List<CharacterShortcutEvent> characterShortcutEvents;

/// Contains all the events that will be handled when
/// space key is pressed
///
/// Supported by:
///
/// - Web
/// - Desktop
///
/// ### Example
///```dart
/// // you can get also the default implemented shortcuts
/// // calling [standardSpaceShorcutEvents]
///final defaultShorcutsImplementation =
/// List.from([...standardSpaceShorcutEvents])
///
///final spaceBulletList = SpaceShortcutEvent(
/// character: '-',
/// handler: (QuillText textNode, controller) {...your implementation}
///);
///```
final List<SpaceShortcutEvent> spaceShortcutEvents;

/// Whether the text can be changed.
///
/// When this is set to `true`, the text cannot be modified
Expand Down Expand Up @@ -145,6 +196,10 @@ class QuillEditorConfigurations extends Equatable {
/// This setting controls the behavior of input. Specifically, when enabled,
/// entering '1.' followed by a space or '-' followed by a space
/// will automatically convert the input into a Markdown list format.
///
/// ## !This functionality now does not work because was replaced by a more advanced using [SpaceShortcutEvent] and [CharacterShortcutEvent] classes
@Deprecated(
'enableMarkdownStyleConversion is no longer used and will be removed in future releases. Use space/char shortcut events instead.')
final bool enableMarkdownStyleConversion;

/// Enables always indenting when the TAB key is pressed.
Expand Down Expand Up @@ -450,6 +505,8 @@ class QuillEditorConfigurations extends Equatable {
LinkActionPickerDelegate? linkActionPickerDelegate,
bool? floatingCursorDisabled,
TextSelectionControls? textSelectionControls,
List<CharacterShortcutEvent>? characterShortcutEvents,
List<SpaceShortcutEvent>? spaceShortcutEvents,
Future<String?> Function(Uint8List imageBytes)? onImagePaste,
Future<String?> Function(Uint8List imageBytes)? onGifPaste,
Map<ShortcutActivator, Intent>? customShortcuts,
Expand Down Expand Up @@ -483,8 +540,13 @@ class QuillEditorConfigurations extends Equatable {
disableClipboard: disableClipboard ?? this.disableClipboard,
scrollable: scrollable ?? this.scrollable,
scrollBottomInset: scrollBottomInset ?? this.scrollBottomInset,
characterShortcutEvents:
characterShortcutEvents ?? this.characterShortcutEvents,
spaceShortcutEvents: spaceShortcutEvents ?? this.spaceShortcutEvents,
padding: padding ?? this.padding,
// ignore: deprecated_member_use_from_same_package
enableMarkdownStyleConversion:
// ignore: deprecated_member_use_from_same_package
enableMarkdownStyleConversion ?? this.enableMarkdownStyleConversion,
enableAlwaysIndentOnTab:
enableAlwaysIndentOnTab ?? this.enableAlwaysIndentOnTab,
Expand Down
5 changes: 3 additions & 2 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,13 +295,14 @@ class QuillEditorState extends State<QuillEditor>
key: _editorKey,
controller: controller,
configurations: QuillRawEditorConfigurations(
characterShortcutEvents:
widget.configurations.characterShortcutEvents,
spaceShortcutEvents: widget.configurations.spaceShortcutEvents,
customLeadingBuilder:
widget.configurations.customLeadingBlockBuilder,
focusNode: widget.focusNode,
scrollController: widget.scrollController,
scrollable: configurations.scrollable,
enableMarkdownStyleConversion:
configurations.enableMarkdownStyleConversion,
enableAlwaysIndentOnTab: configurations.enableAlwaysIndentOnTab,
scrollBottomInset: configurations.scrollBottomInset,
padding: configurations.padding,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import 'package:equatable/equatable.dart';
import 'package:meta/meta.dart';

import '../../../../../flutter_quill.dart';

typedef CharacterShortcutEventHandler = bool Function(
QuillController controller);

/// Defines the implementation of shortcut event based on character.
@immutable
class CharacterShortcutEvent extends Equatable {
const CharacterShortcutEvent({
required this.key,
required this.character,
required this.handler,
}) : assert(character.length == 1 && character != '\n',
'character cannot be major than one char, and it must not be a new line');

final String key;
final String character;
final CharacterShortcutEventHandler handler;

bool execute(QuillController controller) {
return handler(controller);
}

CharacterShortcutEvent copyWith({
String? key,
String? character,
CharacterShortcutEventHandler? handler,
}) {
return CharacterShortcutEvent(
key: key ?? this.key,
character: character ?? this.character,
handler: handler ?? this.handler,
);
}

@override
String toString() =>
'CharacterShortcutEvent(key: $key, character: $character, handler: $handler)';

@override
List<Object?> get props => [key, character, handler];
}
9 changes: 9 additions & 0 deletions lib/src/editor/raw_editor/config/events/events.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
// event classes
export 'character_shortcuts_events.dart';
export 'space_shortcut_events.dart';
// default implementation of the shortcuts
export 'standard_char_shortcuts/block_shortcut_events_handlers.dart';
export 'standard_char_shortcuts/double_character_shortcut_events.dart';
export 'standard_char_shortcuts/single_character_shortcut_events.dart';
// all available shortcuts
export 'standard_char_shortcuts/standard_shortcut_events.dart';
Loading