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

Added CustomBlockEmbed and customElementsEmbedBuilder #877

Merged
merged 3 commits into from
Jul 21, 2022
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
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