Skip to content

Commit

Permalink
[SuperEditor][SuperReader] - Added support for inline widgets (Resolves
Browse files Browse the repository at this point in the history
  • Loading branch information
matthew-carroll authored Dec 20, 2024
1 parent 92daf49 commit 4c41c93
Show file tree
Hide file tree
Showing 117 changed files with 1,185 additions and 704 deletions.
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

0 comments on commit 4c41c93

Please sign in to comment.