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: support for dynamic placeholders #2238

Draft
wants to merge 29 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c6636b7
Chore: added transparent_attribue
Sep 15, 2024
87cea7a
Partial improvements
Sep 17, 2024
d5e4f55
Feat: support for dynamic placeholders
Sep 18, 2024
8f4b0da
Fix: web cannot use dynamic placeholders
Sep 18, 2024
14fe2b3
Chore: removed isLink check since is unnecessary
Sep 18, 2024
811c9a0
Chore: renamed builder param to builders for make more sense to its f…
Sep 18, 2024
c9c5a3d
Chore: dart format .
Sep 18, 2024
98f817f
Chore: fix example in doc comment
Sep 18, 2024
d2a0a63
Chore: renamed PlaceholderConfiguration to PlaceholderArguments
Sep 18, 2024
dd81cc2
Feat: support custom attributes for placeholders
Sep 18, 2024
82b3780
Chore: dart format
Sep 18, 2024
5741e6b
Chore: added late keyword for blackList
Sep 18, 2024
d6edcd2
Feat: added support for cursor placeholder
Sep 18, 2024
8d274a0
Fix: make CursorParagraphPlaceholderConfiguration nullable
Sep 18, 2024
88e9cca
Chore: improve placeholders examples
Sep 18, 2024
1485ba6
Merge branch 'master' into improve_headers
CatHood0 Sep 18, 2024
7be52d5
Fix(merge): missing trailing comma on EditableTextLine constructor
Sep 18, 2024
abf8363
Chore: run before_push to fix formats
Sep 18, 2024
d7c5d14
Chore: make placeholderComponentsConfiguration fully optional
Sep 18, 2024
1eefa9d
Fix: add validation to avoid show component placeholder when found co…
Sep 19, 2024
385586e
Merge branch 'master' into improve_headers
CatHood0 Sep 19, 2024
cece358
Chore: dart format
Sep 19, 2024
aa131a4
Chore: revert change where the placeholder is hiden if node contains …
Sep 20, 2024
f2db22b
Fix: placeholder does not show the aligned applied in the line
Sep 20, 2024
b90f3af
Chore: removed unused imports
Sep 20, 2024
2fb3131
Chore: fix curly issue with flutter analysis
Sep 20, 2024
e412236
Chore: add internal annotations and make strutStyle optional in build…
Sep 20, 2024
3f2d4e4
Fix: widget span is generating unexpected behaviors with the caret
Sep 20, 2024
8556861
Fix: placeholder text span is not taking the style of the dev
Sep 20, 2024
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
26 changes: 26 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,32 @@ class _QuillScreenState extends State<QuillScreen> {
child: MyQuillEditor(
controller: _controller,
configurations: QuillEditorConfigurations(
cursorParagrahPlaceholderConfiguration:
CursorParagrahPlaceholderConfiguration
.withPlaceholder(),
placeholderComponentsConfiguration:
PlaceholderComponentsConfiguration(
builders: {
Attribute.header.key: (Attribute attr, style) {
final values = [32, 30, 24, 22, 18, 16];
final level = attr.value as int?;
if (level == null) return null;
final fontSize =
values[(level - 1 <= 0 ? 0 : level - 1)];
return PlaceholderArguments(
placeholderText: 'Header $level',
style: TextStyle(fontSize: fontSize.toDouble())
.merge(style.copyWith(color: Colors.grey)),
);
},
Attribute.list.key: (Attribute attr, style) {
return PlaceholderArguments(
placeholderText: 'List item',
style: style.copyWith(color: Colors.grey),
);
},
},
),
characterShortcutEvents: standardCharactersShortcutEvents,
spaceShortcutEvents: standardSpaceShorcutEvents,
searchConfigurations: const QuillSearchConfigurations(
Expand Down
2 changes: 2 additions & 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/builders/placeholder/placeholder_configuration.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';
Expand All @@ -30,6 +31,7 @@ export 'src/editor/spellchecker/spellchecker_service.dart';
export 'src/editor/spellchecker/spellchecker_service_provider.dart';
export 'src/editor/style_widgets/style_widgets.dart';
export 'src/editor/widgets/cursor.dart';
export 'src/editor/widgets/cursor_configuration/cursor_configuration.dart';
export 'src/editor/widgets/default_styles.dart';
export 'src/editor/widgets/link.dart';
export 'src/editor/widgets/text/utils/text_block_utils.dart';
Expand Down
46 changes: 46 additions & 0 deletions lib/src/editor/config/editor_configurations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ 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/builders/placeholder/placeholder_configuration.dart';
import '../raw_editor/config/events/events.dart';
import '../raw_editor/raw_editor.dart';
import '../widgets/cursor_configuration/cursor_configuration.dart';
import '../widgets/default_styles.dart';
import '../widgets/delegate.dart';
import '../widgets/link.dart';
Expand All @@ -36,6 +38,8 @@ class QuillEditorConfigurations extends Equatable {
this.padding = EdgeInsets.zero,
this.characterShortcutEvents = const [],
this.spaceShortcutEvents = const [],
this.placeholderComponentsConfiguration,
this.cursorParagrahPlaceholderConfiguration,
this.autoFocus = false,
this.expands = false,
this.placeholder,
Expand Down Expand Up @@ -153,6 +157,40 @@ class QuillEditorConfigurations extends Equatable {
///```
final List<SpaceShortcutEvent> spaceShortcutEvents;

/// Whether the line is empty, this let us add a placeholder
///
/// _Note: these placeholders are limited only to lines with block styles_
///
/// ### Example
///
/// Assume that you want to show only a placeholder text for the header items,
/// so, we only need to configure `placeholderComponentsConfiguration` like:
///
///```dart
///final configuration = PlaceholderComponentsConfiguration(
/// builders: <String, PlaceholderConfigurationBuilder>{
/// Attribute.header.key: (Attribute attr, style) {
/// final values = [30, 27, 22];
/// final level = attr.value as int?;
/// if (level == null) return null;
/// final fontSize = values[level - 1];
/// return PlaceholderArguments(
/// placeholderText: 'Header $level',
/// style: TextStyle(fontSize: fontSize.toDouble()).merge(style),
/// );
/// },
/// },
/// // if you use custom attribute keys into the `builders` param, them you will need to implement that
/// // here too
/// customBlockAttributesKeys: null,
///),
///```
final PlaceholderComponentsConfiguration? placeholderComponentsConfiguration;

/// This argument configure how will be showed the placeholder at right or left of the cursor
final CursorParagrahPlaceholderConfiguration?
cursorParagrahPlaceholderConfiguration;

/// Whether the text can be changed.
///
/// When this is set to `true`, the text cannot be modified
Expand Down Expand Up @@ -519,9 +557,12 @@ class QuillEditorConfigurations extends Equatable {
GlobalKey<EditorState>? editorKey,
TextSelectionThemeData? textSelectionThemeData,
LeadingBlockNodeBuilder? customLeadingBlockBuilder,
PlaceholderComponentsConfiguration? placeholderComponentsConfiguration,
bool? requestKeyboardFocusOnCheckListChanged,
QuillEditorElementOptions? elementOptions,
QuillEditorBuilder? builder,
CursorParagrahPlaceholderConfiguration?
cursorParagrahPlaceholderConfiguration,
TextMagnifierConfiguration? magnifierConfiguration,
TextInputAction? textInputAction,
bool? enableScribble,
Expand All @@ -531,6 +572,11 @@ class QuillEditorConfigurations extends Equatable {
}) {
return QuillEditorConfigurations(
sharedConfigurations: sharedConfigurations ?? this.sharedConfigurations,
cursorParagrahPlaceholderConfiguration:
cursorParagrahPlaceholderConfiguration ??
this.cursorParagrahPlaceholderConfiguration,
placeholderComponentsConfiguration: placeholderComponentsConfiguration ??
this.placeholderComponentsConfiguration,
customLeadingBlockBuilder:
customLeadingBlockBuilder ?? this.customLeadingBlockBuilder,
// ignore: deprecated_member_use_from_same_package
Expand Down
12 changes: 12 additions & 0 deletions lib/src/editor/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import 'config/editor_configurations.dart';
import 'editor_builder.dart';
import 'embed/embed_editor_builder.dart';
import 'provider.dart';
import 'raw_editor/builders/placeholder/placeholder_builder_internal.dart';
import 'raw_editor/config/raw_editor_configurations.dart';
import 'raw_editor/raw_editor.dart';
import 'widgets/box.dart';
Expand Down Expand Up @@ -286,6 +287,13 @@ class QuillEditorState extends State<QuillEditor>
final showSelectionToolbar = configurations.enableInteractiveSelection &&
configurations.enableSelectionToolbar;

final placeholderBuilder =
widget.configurations.placeholderComponentsConfiguration == null
? null
: PlaceholderBuilder(
configuration:
widget.configurations.placeholderComponentsConfiguration!);

final child = FlutterQuillLocalizationsWidget(
child: QuillEditorProvider(
controller: controller,
Expand All @@ -295,6 +303,9 @@ class QuillEditorState extends State<QuillEditor>
key: _editorKey,
controller: controller,
configurations: QuillRawEditorConfigurations(
cursorParagrahPlaceholderConfiguration:
widget.configurations.cursorParagrahPlaceholderConfiguration,
placeholderBuilder: placeholderBuilder,
characterShortcutEvents:
widget.configurations.characterShortcutEvents,
spaceShortcutEvents: widget.configurations.spaceShortcutEvents,
Expand Down Expand Up @@ -1447,6 +1458,7 @@ class RenderEditor extends RenderEditableContainerBox
child.getCaretPrototype(child.globalToLocalPosition(textPosition));
_floatingCursorRect =
sizeAdjustment.inflateRect(caretPrototype).shift(boundedOffset);

_cursorController
.setFloatingCursorTextPosition(_floatingCursorTextPosition);
} else {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
// This file is only for internal use
import 'package:flutter/material.dart'
show
Align,
Alignment,
CrossAxisAlignment,
Expanded,
MainAxisAlignment,
PlaceholderAlignment,
Row,
StrutStyle,
Text,
TextAlign,
TextBaseline,
TextDirection,
TextStyle,
TextWidthBasis,
WidgetSpan,
immutable;
import 'package:meta/meta.dart';
import '../../../../document/attribute.dart' show Attribute, AttributeScope;
import '../../../../document/nodes/line.dart';
import 'placeholder_configuration.dart';

/// This is the black list of the keys that cannot be
/// used or permitted by the builder
// ignore: unnecessary_late
late final List<String> _blackList = List.unmodifiable(<String>[
Attribute.align.key,
Attribute.direction.key,
Attribute.lineHeight.key,
Attribute.indent.key,
...Attribute.inlineKeys,
...Attribute.ignoreKeys,
]);

@experimental
@internal
@immutable
class PlaceholderBuilder {
const PlaceholderBuilder({
required this.configuration,
});

final PlaceholderComponentsConfiguration configuration;

Map<String, PlaceholderConfigurationBuilder> get builders =>
configuration.builders;
Set<String>? get customBlockAttributesKeys =>
configuration.customBlockAttributesKeys;

/// Check if this node need to show a placeholder
@experimental
@internal
(bool, String) shouldShowPlaceholder(Line node) {
if (builders.isEmpty) return (false, '');
var shouldShow = false;
var key = '';
for (final exclusiveKey in <dynamic>{
...Attribute.exclusiveBlockKeys,
...?customBlockAttributesKeys
}) {
if (node.style.containsKey(exclusiveKey) &&
node.style.attributes[exclusiveKey]?.scope == AttributeScope.block &&
!_blackList.contains(exclusiveKey)) {
shouldShow = true;
key = exclusiveKey;
break;
}
}
// we return if should show placeholders and the key of the attr that matches to get it directly
// avoiding an unnecessary traverse into the attributes of the node
return (node.isEmpty && shouldShow, key);
}

/// Build is similar to build method from any widget but
/// this only has the responsability of create a WidgetSpan to be showed
/// by the line when the node is empty
///
/// Before use this, we should always use [shouldShowPlaceholder] to avoid
/// show any placeholder where is not needed
@experimental
@internal
WidgetSpan? build({
required Attribute blockAttribute,
required TextStyle lineStyle,
required TextDirection textDirection,
required TextAlign align,
StrutStyle? strutStyle,
}) {
if (builders.isEmpty) return null;
final configuration =
builders[blockAttribute.key]?.call(blockAttribute, lineStyle);
// we don't need to add a placeholder that is null or contains a empty text
if (configuration == null || configuration.placeholderText.trim().isEmpty) {
return null;
}
final textWidget = Text(
configuration.placeholderText,
style: configuration.style,
textDirection: textDirection,
softWrap: true,
strutStyle: strutStyle,
textAlign: align,
textWidthBasis: TextWidthBasis.longestLine,
);
// we use [Row] widget with [Expanded] to take whole the available width
// when the line has not defined an alignment.
//
// this behavior is different when the align is left or justify, because
// if we align the line to the center (example), row will take the whole
// width, creating a visual unexpected behavior where the caret being putted
// at the offset 0 (you can think this like the caret appears at the first char
// of the line when it is aligned at the left side instead appears at the middle
// if the line is centered)
//
// Note:
// this code is subject to changes because we need to get a better solution
// to this implementation
return WidgetSpan(
style: lineStyle,
child: align == TextAlign.end || align == TextAlign.center
? textWidget
: Row(
children: [
Expanded(
child: textWidget,
),
],
),
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import 'package:flutter/material.dart' show TextStyle, immutable;
import '../../../../document/attribute.dart';

typedef PlaceholderConfigurationBuilder = PlaceholderArguments? Function(
Attribute, TextStyle);

@immutable
class PlaceholderComponentsConfiguration {
const PlaceholderComponentsConfiguration({
required this.builders,
this.customBlockAttributesKeys,
});

factory PlaceholderComponentsConfiguration.base() {
return const PlaceholderComponentsConfiguration(builders: {});
}

/// These attributes are used with the default ones
/// to let us add placeholder to custom block attributes
final Set<String>? customBlockAttributesKeys;
final Map<String, PlaceholderConfigurationBuilder> builders;

PlaceholderComponentsConfiguration copyWith({
Map<String, PlaceholderConfigurationBuilder>? builders,
Set<String>? customBlockAttributesKeys,
}) {
return PlaceholderComponentsConfiguration(
builders: builders ?? this.builders,
customBlockAttributesKeys:
customBlockAttributesKeys ?? this.customBlockAttributesKeys,
);
}
}

@immutable

/// Represents the arguments that builds the text that will
/// be displayed
class PlaceholderArguments {
const PlaceholderArguments({
required this.placeholderText,
required this.style,
});

final String placeholderText;
final TextStyle style;
}
12 changes: 12 additions & 0 deletions lib/src/editor/raw_editor/config/raw_editor_configurations.dart
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,9 @@ import '../../../editor/widgets/default_styles.dart';
import '../../../editor/widgets/delegate.dart';
import '../../../editor/widgets/link.dart';
import '../../../toolbar/theme/quill_dialog_theme.dart';
import '../../widgets/cursor_configuration/cursor_configuration.dart';
import '../builders/leading_block_builder.dart';
import '../builders/placeholder/placeholder_builder_internal.dart';
import 'events/events.dart';

@immutable
Expand All @@ -52,6 +54,8 @@ class QuillRawEditorConfigurations extends Equatable {
required this.autoFocus,
required this.characterShortcutEvents,
required this.spaceShortcutEvents,
this.placeholderBuilder,
this.cursorParagrahPlaceholderConfiguration,
@Deprecated(
'controller should be passed directly to the editor - this parameter will be removed in future versions.')
this.controller,
Expand Down Expand Up @@ -137,6 +141,10 @@ class QuillRawEditorConfigurations extends Equatable {
///```
final List<CharacterShortcutEvent> characterShortcutEvents;

/// Contains all necessary logic to build the placeholder
/// given for the devs
final PlaceholderBuilder? placeholderBuilder;

/// Contains all the events that will be handled when
/// space key is pressed
///
Expand All @@ -159,6 +167,10 @@ class QuillRawEditorConfigurations extends Equatable {
///```
final List<SpaceShortcutEvent> spaceShortcutEvents;

/// This argument configure how will be showed the placeholder at right or left of the cursor
final CursorParagrahPlaceholderConfiguration?
cursorParagrahPlaceholderConfiguration;

/// Additional space around the editor contents.
final EdgeInsetsGeometry padding;

Expand Down
Loading
Loading