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

[SuperEditor][SuperReader] - Added support for inline widgets (Resolves #2442) #2450

Merged
merged 6 commits into from
Dec 20, 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
4 changes: 4 additions & 0 deletions super_editor/clones/quill/lib/editor/code_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ class CodeBlockComponentViewModel extends SingleColumnLayoutComponentViewModel w
super.padding = EdgeInsets.zero,
required this.text,
required this.textStyleBuilder,
this.inlineWidgetBuilders = const [],
this.textDirection = TextDirection.ltr,
this.textAlignment = TextAlign.left,
required this.backgroundColor,
Expand All @@ -97,6 +98,8 @@ class CodeBlockComponentViewModel extends SingleColumnLayoutComponentViewModel w
@override
AttributionStyleBuilder textStyleBuilder;
@override
InlineWidgetBuilderChain inlineWidgetBuilders;
@override
TextDirection textDirection;
@override
TextAlign textAlignment;
Expand Down Expand Up @@ -125,6 +128,7 @@ class CodeBlockComponentViewModel extends SingleColumnLayoutComponentViewModel w
padding: padding,
text: text,
textStyleBuilder: textStyleBuilder,
inlineWidgetBuilders: inlineWidgetBuilders,
textDirection: textDirection,
textAlignment: textAlignment,
backgroundColor: backgroundColor,
Expand Down
7 changes: 4 additions & 3 deletions super_editor/example/lib/demos/example_editor/_toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -390,7 +390,7 @@ class _EditorToolbarState extends State<EditorToolbar> {
/// Takes the text from the [urlController] and applies it as a link
/// attribution to the currently selected text.
void _applyLink() {
final url = _urlController!.text.text;
final url = _urlController!.text.toPlainText(includePlaceholders: false);

final selection = widget.composer.selection!;
final baseOffset = (selection.base.nodePosition as TextPosition).offset;
Expand Down Expand Up @@ -438,10 +438,11 @@ class _EditorToolbarState extends State<EditorToolbar> {
int startOffset = range.start;
int endOffset = range.end;

while (startOffset < range.end && text.text[startOffset] == ' ') {
final plainText = text.toPlainText();
while (startOffset < range.end && plainText[startOffset] == ' ') {
startOffset += 1;
}
while (endOffset > startOffset && text.text[endOffset] == ' ') {
while (endOffset > startOffset && plainText[endOffset] == ' ') {
endOffset -= 1;
}

Expand Down
87 changes: 54 additions & 33 deletions super_editor/example/lib/demos/mobile_chat/demo_mobile_chat.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import 'package:example/demos/mobile_chat/giphy_keyboard_panel.dart';
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';

Expand Down Expand Up @@ -157,7 +158,7 @@ class _MobileChatDemoState extends State<MobileChatDemo> {

Widget _buildCommentEditor() {
return Opacity(
opacity: 0.75,
opacity: 1.0,
// ^ opacity is for testing, so we can see the chat behind it.
child: KeyboardPanelScaffold(
controller: _keyboardPanelController,
Expand All @@ -176,6 +177,10 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
color: Colors.red,
height: double.infinity,
);
case _Panel.giphy:
return GiphyKeyboardPanel(
editor: _editor,
);
default:
return const SizedBox();
}
Expand Down Expand Up @@ -267,6 +272,11 @@ class _MobileChatDemoState extends State<MobileChatDemo> {
icon: Icons.account_circle,
onPressed: () => _showBottomSheetWithOptions(context),
),
const SizedBox(width: 16),
_PanelButton(
icon: Icons.gif_box_outlined,
onPressed: () => _togglePanel(_Panel.giphy),
),
const Spacer(),
GestureDetector(
onTap: _keyboardPanelController.closeKeyboardAndPanel,
Expand All @@ -293,7 +303,8 @@ class _MobileChatDemoState extends State<MobileChatDemo> {

enum _Panel {
panel1,
panel2;
panel2,
giphy;
}

class _PanelButton extends StatelessWidget {
Expand Down Expand Up @@ -325,37 +336,47 @@ class _PanelButton extends StatelessWidget {
}
}

final _chatStylesheet = defaultStylesheet.copyWith(
addRulesBefore: [
StyleRule(
BlockSelector.all,
(doc, docNode) {
return {
Styles.maxWidth: double.infinity,
Styles.padding: const CascadingPadding.symmetric(horizontal: 24),
};
},
),
],
addRulesAfter: [
StyleRule(
BlockSelector.all.first(),
(doc, docNode) {
return {
Styles.padding: const CascadingPadding.only(top: 12),
};
},
),
StyleRule(
BlockSelector.all.last(),
(doc, docNode) {
return {
Styles.padding: const CascadingPadding.only(bottom: 12),
};
},
),
],
);
Stylesheet get _chatStylesheet => defaultStylesheet.copyWith(
addRulesBefore: [
StyleRule(
BlockSelector.all,
(doc, docNode) {
return {
Styles.maxWidth: double.infinity,
Styles.padding: const CascadingPadding.symmetric(horizontal: 24),
};
},
),
],
addRulesAfter: [
StyleRule(
BlockSelector.all,
(doc, docNode) {
return {
Styles.textStyle: TextStyle(
fontSize: 18,
),
};
},
),
StyleRule(
BlockSelector.all.first(),
(doc, docNode) {
return {
Styles.padding: const CascadingPadding.only(top: 12),
};
},
),
StyleRule(
BlockSelector.all.last(),
(doc, docNode) {
return {
Styles.padding: const CascadingPadding.only(bottom: 12),
};
},
),
],
);

Future<void> _showBottomSheetWithOptions(BuildContext context) async {
return showModalBottomSheet(
Expand Down
102 changes: 102 additions & 0 deletions super_editor/example/lib/demos/mobile_chat/giphy_keyboard_panel.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import 'package:flutter/material.dart';
import 'package:super_editor/super_editor.dart';
import 'package:super_text_layout/super_text_layout.dart';

class GiphyKeyboardPanel extends StatefulWidget {
const GiphyKeyboardPanel({
super.key,
required this.editor,
});

final Editor editor;

@override
State<GiphyKeyboardPanel> createState() => _GiphyKeyboardPanelState();
}

class _GiphyKeyboardPanelState extends State<GiphyKeyboardPanel> {
void _onGifPressed(String url) {
final selection = widget.editor.context.composer.selection;
if (selection == null) {
return;
}
if (selection.base.nodePosition is! TextNodePosition) {
return;
}

widget.editor.execute([
if (!selection.isCollapsed) //
DeleteContentRequest(
documentRange: selection.normalize(widget.editor.context.document),
),
InsertAttributedTextRequest(
selection.base,
AttributedText("", null, {
0: InlineNetworkImagePlaceholder(url),
}),
),
ChangeSelectionRequest(
DocumentSelection.collapsed(
position: selection.base.copyWith(
nodePosition: TextNodePosition(offset: (selection.base.nodePosition as TextNodePosition).offset + 1),
),
),
SelectionChangeType.alteredContent,
SelectionReason.userInteraction,
),
]);
}

@override
Widget build(BuildContext context) {
return GridView(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
),
children: _giphyEmojis.map(_buildEmoji).toList(),
);
}

Widget _buildEmoji(String url) {
return GestureDetector(
onTap: () => _onGifPressed(url),
child: Image.network(url),
);
}
}

const _giphyEmojis = [
// Thumbs up.
"https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExZHBwdGgwYXYydTJiYmV1aGZ6dWZraGZsZzIzNmNkZGdiMGJyYW40dSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/ehz3LfVj7NvpY8jYUY/giphy.webp",
// Fire.
"https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExcHpjemk5eGVza29iOHNlaHJkbWJjamxpZW82MzEwM2F4bDV1NTJkaiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/J2awouDsf23R2vo2p5/giphy.webp",
// Flexing muscle.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExY3NxOWFuanlvOXk3Y2V5bmFjaGQ2Z3c4aHQ5aDI5dXlwdzRpd25uMyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/SvLQ270MWY0GpztVjo/giphy.webp",
// Clapping hands.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExMjhncWRqbHBmNDVvZ3Q2ZHYzN2VkbXdoZGt0Z2d4eTI2ZTV5aTR2dyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/ZdNlmHHr7czumQPvNE/giphy.webp",
// Prayer hands.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExeGszYXh0djNieXJhZW1zbjJ5NjExd3RqcHppYjB0dHgxemk0d2loMSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/WqR7WfQVrpXNcmrm81/giphy.webp",
// Heart.
"https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExZGI5bTEwcTg4dXd2a29sc3BxdTFlMHEwOHI2b3ozYWgxNHAycnBmaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9Zw/xUA7aWi4gtOdAaX9q8/giphy.webp",
// OMG face.
"https://media3.giphy.com/media/v1.Y2lkPTc5MGI3NjExYXJyOGhudTBiNm4wZnR6bTdrNGwwOWtpYWtnbXlxYml0N3ZrMDl0NSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/j2NFnjcXwni0E9KcdI/giphy.webp",
// Popping hearts.
"https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExeXd4bHEwaWRxYm41dWhhc21neDFxZ2p6YXAxY2ZnM20wcDZwaG5wcCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/QUGf8x31iMVSdbNn00/giphy.webp",
// Awkward face.
"https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExMWd5dDh1djVlbWhnMmV3dzR2emtqNDdxZHZqeGNrem9zZnE5MjI3aSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/XHdW0gCDj6KiFmKFCZ/giphy.webp",
// Fuming face.
"https://media1.giphy.com/media/v1.Y2lkPTc5MGI3NjExa2ZsbmZib2hleno0dTV4dzMyMmtoZ3JocThlZHFkdnYxeHJ1b21idiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/kyQfR7MlQQ9Gb8URKG/giphy.webp",
// Angry face.
"https://media0.giphy.com/media/v1.Y2lkPTc5MGI3NjExcGFrd2ZqaGM2ZmVveHU1bWZ5b25ocDV5M2J1MG9nbGplampsOGdibSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/QU3wZZG8x351iQAbfm/giphy.webp",
// Deflate face.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExNnJxeHR3MmJiNmhiYmdtaWt3bDVmcHJlbXBibzNyazluZmE4dTBnZSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/H4cBu6XqKJtGujEXll/giphy.webp",
// Dumpster fire.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExOHJ1dWFtazNoeTVrcGthMHE2ZWI1aDlyOWpkZHY4MzZyMXJsZDFwbiZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9ZQ/jOsoGmmWGSloPU8fMH/giphy.webp",

// Disappointed baby.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExcWo4cnV2dW1sem9hMzk5cWd5cW4zcW80ejU3YnJuZjF5amdpMGF5ZyZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/tr4TTyG4BjxfDioymO/giphy.webp",
// Chihuahua face.
"https://media4.giphy.com/media/v1.Y2lkPTc5MGI3NjExdXR2ZGoxZDBkemJpZzdtOXBpc292OXp0d2cyMzdqemlpZnJocjdiaSZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/3oKIPfZAisBaUuybcs/giphy.webp",
// South Park - Randy crying.
"https://media2.giphy.com/media/v1.Y2lkPTc5MGI3NjExa3EybmZxazIwaXgzY3lpcmpjdTMwcXh0c3Fsd28wbW5xZTBhNGZ3NCZlcD12MV9pbnRlcm5hbF9naWZfYnlfaWQmY3Q9cw/PaVz5Z1dot5FIPS50w/giphy.webp",
];
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,9 @@ class _InteractiveTextFieldDemoState extends State<InteractiveTextFieldDemo> {
TextButton(
onPressed: () {
Clipboard.setData(ClipboardData(
text: _textFieldController.selection.textInside(_textFieldController.text.text),
text: _textFieldController.selection.textInside(
_textFieldController.text.toPlainText(includePlaceholders: false),
),
));
_closePopup();
},
Expand Down
13 changes: 7 additions & 6 deletions super_editor/example/lib/demos/supertextfield/_robot.dart
Original file line number Diff line number Diff line change
Expand Up @@ -138,8 +138,9 @@ class TypeTextCommand implements RobotCommand {

focusNode!.requestFocus();

for (int i = 0; i < textToType.text.length; ++i) {
_typeCharacter(textController, i);
final plainText = textToType.toPlainText();
for (int i = 0; i < plainText.length; ++i) {
_typeCharacter(textController, i, plainText[i]);

await _waitForCharacterDelay();

Expand All @@ -149,9 +150,9 @@ class TypeTextCommand implements RobotCommand {
}
}

void _typeCharacter(AttributedTextEditingController textController, int offset) {
void _typeCharacter(AttributedTextEditingController textController, int offset, String character) {
textController.text = textController.text.insertString(
textToInsert: textToType.text[offset], // TODO: support insertion of attributed text
textToInsert: character,
startOffset: textController.selection.extentOffset,
);

Expand Down Expand Up @@ -246,12 +247,12 @@ class DeleteCharactersCommand implements RobotCommand {
if (direction == TextAffinity.downstream) {
// Delete the character after the offset
deleteStartIndex = offset;
deleteEndIndex = getCharacterEndBounds(textController.text.text, offset);
deleteEndIndex = getCharacterEndBounds(textController.text.toPlainText(), offset);
deletedCodePointCount = deleteEndIndex - deleteStartIndex;
newSelectionIndex = deleteStartIndex;
} else {
// Delete the character before the offset
deleteStartIndex = getCharacterStartBounds(textController.text.text, offset);
deleteStartIndex = getCharacterStartBounds(textController.text.toPlainText(), offset);
deleteEndIndex = offset + 1;
deletedCodePointCount = offset - deleteStartIndex;
newSelectionIndex = deleteStartIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class _IncrementDecrementFieldState extends State<IncrementDecrementField> {

void _onPerformAction(TextInputAction action) {
if (action == TextInputAction.done) {
final value = int.tryParse(_controller.text.text.trim());
final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
if (value != null) {
widget.onChange(value);
}
Expand All @@ -72,7 +72,7 @@ class _IncrementDecrementFieldState extends State<IncrementDecrementField> {
}

void _onIncrement() {
final value = int.tryParse(_controller.text.text.trim());
final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
if (value == null) {
return;
}
Expand All @@ -81,7 +81,7 @@ class _IncrementDecrementFieldState extends State<IncrementDecrementField> {
}

void _onDecrement() {
final value = int.tryParse(_controller.text.text.trim());
final value = int.tryParse(_controller.text.toPlainText(includePlaceholders: false).trim());
if (value == null) {
return;
}
Expand Down
7 changes: 4 additions & 3 deletions super_editor/example_docs/lib/toolbar.dart
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ class _DocsEditorToolbarState extends State<DocsEditorToolbar> {
/// Applies the link entered on the URL textfield to the current
/// selected range.
void _applyLink() {
final url = _urlController!.text.text;
final url = _urlController!.text.toPlainText(includePlaceholders: false);

final selection = widget.composer.selection!;
final baseOffset = (selection.base.nodePosition as TextPosition).offset;
Expand Down Expand Up @@ -440,10 +440,11 @@ class _DocsEditorToolbarState extends State<DocsEditorToolbar> {
int startOffset = range.start;
int endOffset = range.end;

while (startOffset < range.end && text.text[startOffset] == ' ') {
final plainText = text.toPlainText();
while (startOffset < range.end && plainText[startOffset] == ' ') {
startOffset += 1;
}
while (endOffset > startOffset && text.text[endOffset] == ' ') {
while (endOffset > startOffset && plainText[endOffset] == ' ') {
endOffset -= 1;
}

Expand Down
Loading
Loading