Skip to content

Commit

Permalink
Added CustomBlockEmbed and customElementsEmbedBuilder (#877)
Browse files Browse the repository at this point in the history
  • Loading branch information
LucazzP authored Jul 21, 2022
1 parent d26b24d commit 48f8932
Show file tree
Hide file tree
Showing 11 changed files with 203 additions and 50 deletions.
2 changes: 2 additions & 0 deletions example/ios/Runner/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -43,5 +43,7 @@
</array>
<key>UIViewControllerBasedStatusBarAppearance</key>
<false/>
<key>CADisableMinimumFrameDurationOnPhone</key>
<true/>
</dict>
</plist>
140 changes: 117 additions & 23 deletions example/lib/pages/home_page.dart
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,12 @@ class _HomePageState extends State<HomePage> {
title: const Text(
'Flutter Quill',
),
actions: [],
actions: [
IconButton(
onPressed: () => _addEditNote(context),
icon: const Icon(Icons.note_add),
),
],
),
drawer: Container(
constraints:
Expand Down Expand Up @@ -92,28 +97,30 @@ class _HomePageState extends State<HomePage> {

Widget _buildWelcomeEditor(BuildContext context) {
var quillEditor = QuillEditor(
controller: _controller!,
scrollController: ScrollController(),
scrollable: true,
focusNode: _focusNode,
autoFocus: false,
readOnly: false,
placeholder: 'Add content',
expands: false,
padding: EdgeInsets.zero,
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9),
));
controller: _controller!,
scrollController: ScrollController(),
scrollable: true,
focusNode: _focusNode,
autoFocus: false,
readOnly: false,
placeholder: 'Add content',
expands: false,
padding: EdgeInsets.zero,
customStyles: DefaultStyles(
h1: DefaultTextBlockStyle(
const TextStyle(
fontSize: 32,
color: Colors.black,
height: 1.15,
fontWeight: FontWeight.w300,
),
const Tuple2(16, 0),
const Tuple2(0, 0),
null),
sizeSmall: const TextStyle(fontSize: 9),
),
customElementsEmbedBuilder: customElementsEmbedBuilder,
);
if (kIsWeb) {
quillEditor = QuillEditor(
controller: _controller!,
Expand Down Expand Up @@ -304,4 +311,91 @@ class _HomePageState extends State<HomePage> {
),
);
}

Future<void> _addEditNote(BuildContext context, {Document? document}) async {
final isEditing = document != null;
final quillEditorController = QuillController(
document: document ?? Document(),
selection: const TextSelection.collapsed(offset: 0),
);

await showDialog(
context: context,
builder: (context) => AlertDialog(
titlePadding: const EdgeInsets.only(left: 16, top: 8),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text('${isEditing ? 'Edit' : 'Add'} note'),
IconButton(
onPressed: () => Navigator.of(context).pop(),
icon: const Icon(Icons.close),
)
],
),
content: QuillEditor.basic(
controller: quillEditorController,
readOnly: false,
),
),
);

if (quillEditorController.document.isEmpty()) return;

final block = BlockEmbed.custom(
NotesBlockEmbed.fromDocument(quillEditorController.document),
);
final controller = _controller!;
final index = controller.selection.baseOffset;
final length = controller.selection.extentOffset - index;

if (isEditing) {
final offset = getEmbedNode(controller, controller.selection.start).item1;
controller.replaceText(
offset, 1, block, TextSelection.collapsed(offset: offset));
} else {
controller.replaceText(index, length, block, null);
}
}

Widget customElementsEmbedBuilder(
BuildContext context,
QuillController controller,
CustomBlockEmbed block,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
) {
switch (block.type) {
case 'notes':
final notes = NotesBlockEmbed(block.data).document;

return Material(
color: Colors.transparent,
child: ListTile(
title: Text(
notes.toPlainText().replaceAll('\n', ' '),
maxLines: 3,
overflow: TextOverflow.ellipsis,
),
leading: const Icon(Icons.notes),
onTap: () => _addEditNote(context, document: notes),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
side: const BorderSide(color: Colors.grey),
),
),
);
default:
return const SizedBox();
}
}
}

class NotesBlockEmbed extends CustomBlockEmbed {
const NotesBlockEmbed(String value) : super('notes', value);

static NotesBlockEmbed fromDocument(Document document) =>
NotesBlockEmbed(jsonEncode(document.toDelta().toJson()));

Document get document => Document.fromJson(jsonDecode(data));
}
1 change: 1 addition & 0 deletions lib/flutter_quill.dart
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export 'src/models/quill_delta.dart';
export 'src/models/themes/quill_custom_icon.dart';
export 'src/models/themes/quill_dialog_theme.dart';
export 'src/models/themes/quill_icon_theme.dart';
export 'src/utils/embeds.dart';
export 'src/widgets/controller.dart';
export 'src/widgets/default_styles.dart';
export 'src/widgets/editor.dart';
Expand Down
17 changes: 17 additions & 0 deletions lib/src/models/documents/nodes/embeddable.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:convert';

/// An object which can be embedded into a Quill document.
///
/// See also:
Expand Down Expand Up @@ -35,4 +37,19 @@ class BlockEmbed extends Embeddable {

static const String videoType = 'video';
static BlockEmbed video(String videoUrl) => BlockEmbed(videoType, videoUrl);

static const String customType = 'custom';
static BlockEmbed custom(CustomBlockEmbed customBlock) =>
BlockEmbed(customType, customBlock.toJsonString());
}

class CustomBlockEmbed extends BlockEmbed {
const CustomBlockEmbed(String type, String data) : super(type, data);

String toJsonString() => jsonEncode(toJson());

static CustomBlockEmbed fromJsonString(String data) {
final embeddable = Embeddable.fromJson(jsonDecode(data));
return CustomBlockEmbed(embeddable.type, embeddable.data);
}
}
8 changes: 4 additions & 4 deletions lib/src/translations/toolbar.i18n.dart
Original file line number Diff line number Diff line change
Expand Up @@ -273,10 +273,10 @@ extension Localization on String {
'Saved': 'Salvo',
'Text': 'Texto',
'What is entered is not a link': 'O link inserido não é válido',
'Resize': 'Resize',
'Width': 'Width',
'Height': 'Height',
'Size': 'Size',
'Resize': 'Redimencionar',
'Width': 'Largura',
'Height': 'Altura',
'Size': 'Tamanho',
},
'pl': {
'Paste a link': 'Wklej link',
Expand Down
19 changes: 19 additions & 0 deletions lib/src/utils/embeds.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import 'dart:math';

import 'package:tuple/tuple.dart';

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

Tuple2<int, Embed> getEmbedNode(QuillController controller, int offset) {
var offset = controller.selection.start;
var imageNode = controller.queryNode(offset);
if (imageNode == null || !(imageNode is Embed)) {
offset = max(0, offset - 1);
imageNode = controller.queryNode(offset);
}
if (imageNode != null && imageNode is Embed) {
return Tuple2(offset, imageNode);
}

return throw 'Embed node not found by offset $offset';
}
8 changes: 8 additions & 0 deletions lib/src/widgets/delegate.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@ typedef EmbedBuilder = Widget Function(
void Function(GlobalKey videoContainerKey)? onVideoInit,
);

typedef CustomEmbedBuilder = Widget Function(
BuildContext context,
QuillController controller,
CustomBlockEmbed block,
bool readOnly,
void Function(GlobalKey videoContainerKey)? onVideoInit,
);

typedef CustomStyleBuilder = TextStyle Function(Attribute attribute);

/// Delegate interface for the [EditorTextSelectionGestureDetectorBuilder].
Expand Down
30 changes: 29 additions & 1 deletion lib/src/widgets/editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import 'package:tuple/tuple.dart';

import '../models/documents/document.dart';
import '../models/documents/nodes/container.dart' as container_node;
import '../models/documents/nodes/embeddable.dart';
import '../models/documents/style.dart';
import '../utils/platform.dart';
import 'box.dart';
Expand Down Expand Up @@ -168,6 +169,7 @@ class QuillEditor extends StatefulWidget {
this.onSingleLongTapMoveUpdate,
this.onSingleLongTapEnd,
this.embedBuilder = defaultEmbedBuilder,
this.customElementsEmbedBuilder,
this.linkActionPickerDelegate = defaultLinkActionPickerDelegate,
this.customStyleBuilder,
this.locale,
Expand Down Expand Up @@ -340,6 +342,7 @@ class QuillEditor extends StatefulWidget {
onSingleLongTapEnd;

final EmbedBuilder embedBuilder;
final CustomEmbedBuilder? customElementsEmbedBuilder;
final CustomStyleBuilder? customStyleBuilder;

/// The locale to use for the editor toolbar, defaults to system locale
Expand Down Expand Up @@ -458,7 +461,32 @@ class QuillEditorState extends State<QuillEditor>
keyboardAppearance: widget.keyboardAppearance,
enableInteractiveSelection: widget.enableInteractiveSelection,
scrollPhysics: widget.scrollPhysics,
embedBuilder: widget.embedBuilder,
embedBuilder: (
context,
controller,
node,
readOnly,
onVideoInit,
) {
final customElementsEmbedBuilder = widget.customElementsEmbedBuilder;
final isCustomType = node.value.type == BlockEmbed.customType;
if (customElementsEmbedBuilder != null && isCustomType) {
return customElementsEmbedBuilder(
context,
controller,
CustomBlockEmbed.fromJsonString(node.value.data),
readOnly,
onVideoInit,
);
}
return widget.embedBuilder(
context,
controller,
node,
readOnly,
onVideoInit,
);
},
linkActionPickerDelegate: widget.linkActionPickerDelegate,
customStyleBuilder: widget.customStyleBuilder,
floatingCursorDisabled: widget.floatingCursorDisabled,
Expand Down
7 changes: 4 additions & 3 deletions lib/src/widgets/embeds/default_embed_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import '../../models/documents/attribute.dart';
import '../../models/documents/nodes/embeddable.dart';
import '../../models/documents/nodes/leaf.dart' as leaf;
import '../../translations/toolbar.i18n.dart';
import '../../utils/embeds.dart';
import '../../utils/platform.dart';
import '../../utils/string.dart';
import '../controller.dart';
Expand Down Expand Up @@ -79,7 +80,7 @@ Widget defaultEmbedBuilder(
final _screenSize = MediaQuery.of(context).size;
return ImageResizer(
onImageResize: (w, h) {
final res = getImageNode(
final res = getEmbedNode(
controller, controller.selection.start);
final attr = replaceStyleString(
getImageStyleString(controller), w, h);
Expand All @@ -99,7 +100,7 @@ Widget defaultEmbedBuilder(
text: 'Copy'.i18n,
onPressed: () {
final imageNode =
getImageNode(controller, controller.selection.start)
getEmbedNode(controller, controller.selection.start)
.item2;
final imageUrl = imageNode.value.data;
controller.copiedImageUrl =
Expand All @@ -113,7 +114,7 @@ Widget defaultEmbedBuilder(
text: 'Remove'.i18n,
onPressed: () {
final offset =
getImageNode(controller, controller.selection.start)
getEmbedNode(controller, controller.selection.start)
.item1;
controller.replaceText(offset, 1, '',
TextSelection.collapsed(offset: offset));
Expand Down
17 changes: 0 additions & 17 deletions lib/src/widgets/embeds/image.dart
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import 'dart:convert';
import 'dart:io' as io;
import 'dart:math';

import 'package:flutter/material.dart';
import 'package:photo_view/photo_view.dart';
import 'package:string_validator/string_validator.dart';
import 'package:tuple/tuple.dart';

import '../../models/documents/attribute.dart';
import '../../models/documents/nodes/leaf.dart';
import '../../models/documents/style.dart';
import '../controller.dart';

Expand All @@ -26,20 +23,6 @@ bool isImageBase64(String imageUrl) {
return !imageUrl.startsWith('http') && isBase64(imageUrl);
}

Tuple2<int, Embed> getImageNode(QuillController controller, int offset) {
var offset = controller.selection.start;
var imageNode = controller.queryNode(offset);
if (imageNode == null || !(imageNode is Embed)) {
offset = max(0, offset - 1);
imageNode = controller.queryNode(offset);
}
if (imageNode != null && imageNode is Embed) {
return Tuple2(offset, imageNode);
}

return throw 'Image node not found by offset $offset';
}

String getImageStyleString(QuillController controller) {
final String? s = controller
.getAllSelectionStyles()
Expand Down
Loading

0 comments on commit 48f8932

Please sign in to comment.