invoice/lib/utils/super_editor/super_editor.dart

370 lines
11 KiB
Dart

import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:invoiceninja_flutter/utils/super_editor/toolbar.dart';
import 'package:super_editor/super_editor.dart';
/// Example of a rich text editor.
///
/// This editor will expand in functionality as package
/// capabilities expand.
class ExampleEditor extends StatefulWidget {
const ExampleEditor({
Key key,
@required this.value,
@required this.onChanged,
}) : super(key: key);
final String value;
final Function(String) onChanged;
@override
_ExampleEditorState createState() => _ExampleEditorState();
}
class _ExampleEditorState extends State<ExampleEditor> {
final GlobalKey _docLayoutKey = GlobalKey();
Document _doc;
DocumentEditor _docEditor;
DocumentComposer _composer;
CommonEditorOperations _docOps;
FocusNode _editorFocusNode;
ScrollController _scrollController;
OverlayEntry _textFormatBarOverlayEntry;
final _textSelectionAnchor = ValueNotifier<Offset>(null);
OverlayEntry _imageFormatBarOverlayEntry;
final _imageSelectionAnchor = ValueNotifier<Offset>(null);
@override
void initState() {
super.initState();
_doc = deserializeMarkdownToDocument(widget.value)
..addListener(_hideOrShowToolbar);
_docEditor = DocumentEditor(document: _doc as MutableDocument);
_composer = DocumentComposer()..addListener(_hideOrShowToolbar);
_docOps = CommonEditorOperations(
editor: _docEditor,
composer: _composer,
documentLayoutResolver: () =>
_docLayoutKey.currentState as DocumentLayout,
);
_editorFocusNode = FocusNode();
_scrollController = ScrollController()..addListener(_hideOrShowToolbar);
}
@override
void didUpdateWidget(ExampleEditor oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.value != oldWidget.value) {
_doc = deserializeMarkdownToDocument(widget.value)
..addListener(_hideOrShowToolbar);
}
}
@override
void dispose() {
if (_textFormatBarOverlayEntry != null) {
_textFormatBarOverlayEntry.remove();
}
_scrollController.dispose();
_editorFocusNode.dispose();
_composer.dispose();
super.dispose();
}
void _hideOrShowToolbar() {
final value = serializeDocumentToMarkdown(_doc);
widget.onChanged(value);
if (_gestureMode != DocumentGestureMode.mouse) {
// We only add our own toolbar when using mouse. On mobile, a bar
// is rendered for us.
return;
}
final selection = _composer.selection;
if (selection == null) {
// Nothing is selected. We don't want to show a toolbar
// in this case.
_hideEditorToolbar();
return;
}
if (selection.base.nodeId != selection.extent.nodeId) {
// More than one node is selected. We don't want to show
// a toolbar in this case.
_hideEditorToolbar();
_hideImageToolbar();
return;
}
if (selection.isCollapsed) {
// We only want to show the toolbar when a span of text
// is selected. Therefore, we ignore collapsed selections.
_hideEditorToolbar();
_hideImageToolbar();
return;
}
final selectedNode = _doc.getNodeById(selection.extent.nodeId);
if (selectedNode is ImageNode) {
// Show the editor's toolbar for image sizing.
_showImageToolbar();
_hideEditorToolbar();
return;
} else {
// The currently selected content is not an image. We don't
// want to show the image toolbar.
_hideImageToolbar();
}
if (selectedNode is TextNode) {
// Show the editor's toolbar for text styling.
_showEditorToolbar();
_hideImageToolbar();
return;
} else {
// The currently selected content is not a paragraph. We don't
// want to show a toolbar in this case.
_hideEditorToolbar();
}
}
void _showEditorToolbar() {
if (_textFormatBarOverlayEntry == null) {
_textFormatBarOverlayEntry ??= OverlayEntry(builder: (context) {
return EditorToolbar(
anchor: _textSelectionAnchor,
editor: _docEditor,
composer: _composer,
closeToolbar: _hideEditorToolbar,
);
});
// Display the toolbar in the application overlay.
final overlay = Overlay.of(context);
overlay.insert(_textFormatBarOverlayEntry);
}
// Schedule a callback after this frame to locate the selection
// bounds on the screen and display the toolbar near the selected
// text.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (_textFormatBarOverlayEntry == null) {
return;
}
final docBoundingBox = (_docLayoutKey.currentState as DocumentLayout)
.getRectForSelection(
_composer.selection.base, _composer.selection.extent);
final docBox =
_docLayoutKey.currentContext.findRenderObject() as RenderBox;
final overlayBoundingBox = Rect.fromPoints(
docBox.localToGlobal(docBoundingBox.topLeft),
docBox.localToGlobal(docBoundingBox.bottomRight),
);
_textSelectionAnchor.value = overlayBoundingBox.topCenter;
});
}
void _hideEditorToolbar() {
// Null out the selection anchor so that when it re-appears,
// the bar doesn't momentarily "flash" at its old anchor position.
_textSelectionAnchor.value = null;
if (_textFormatBarOverlayEntry != null) {
// Remove the toolbar overlay and null-out the entry.
// We null out the entry because we can't query whether
// or not the entry exists in the overlay, so in our
// case, null implies the entry is not in the overlay,
// and non-null implies the entry is in the overlay.
_textFormatBarOverlayEntry.remove();
_textFormatBarOverlayEntry = null;
}
// Ensure that focus returns to the editor.
//
// I tried explicitly unfocus()'ing the URL textfield
// in the toolbar but it didn't return focus to the
// editor. I'm not sure why.
_editorFocusNode.requestFocus();
}
DocumentGestureMode get _gestureMode {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return DocumentGestureMode.android;
case TargetPlatform.iOS:
return DocumentGestureMode.iOS;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return DocumentGestureMode.mouse;
}
return null;
}
bool get _isMobile => _gestureMode != DocumentGestureMode.mouse;
DocumentInputSource get _inputSource {
switch (defaultTargetPlatform) {
case TargetPlatform.android:
case TargetPlatform.iOS:
return DocumentInputSource.ime;
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.macOS:
case TargetPlatform.windows:
return DocumentInputSource.keyboard;
}
return null;
}
void _cut() => _docOps.cut();
void _copy() => _docOps.copy();
void _paste() => _docOps.paste();
void _selectAll() => _docOps.selectAll();
void _showImageToolbar() {
if (_imageFormatBarOverlayEntry == null) {
// Create an overlay entry to build the image toolbar.
_imageFormatBarOverlayEntry ??= OverlayEntry(builder: (context) {
return ImageFormatToolbar(
anchor: _imageSelectionAnchor,
composer: _composer,
setWidth: (dynamic nodeId, dynamic width) {
final node = _doc.getNodeById(nodeId);
final currentStyles =
SingleColumnLayoutComponentStyles.fromMetadata(node);
SingleColumnLayoutComponentStyles(
width: width,
padding: currentStyles.padding,
).applyTo(node);
},
closeToolbar: _hideImageToolbar,
);
});
// Display the toolbar in the application overlay.
final overlay = Overlay.of(context);
overlay.insert(_imageFormatBarOverlayEntry);
}
// Schedule a callback after this frame to locate the selection
// bounds on the screen and display the toolbar near the selected
// text.
WidgetsBinding.instance.addPostFrameCallback((timeStamp) {
if (_imageFormatBarOverlayEntry == null) {
return;
}
final docBoundingBox = (_docLayoutKey.currentState as DocumentLayout)
.getRectForSelection(
_composer.selection.base, _composer.selection.extent);
final docBox =
_docLayoutKey.currentContext.findRenderObject() as RenderBox;
final overlayBoundingBox = Rect.fromPoints(
docBox.localToGlobal(docBoundingBox.topLeft,
ancestor: context.findRenderObject()),
docBox.localToGlobal(docBoundingBox.bottomRight,
ancestor: context.findRenderObject()),
);
_imageSelectionAnchor.value = overlayBoundingBox.center;
});
}
void _hideImageToolbar() {
// Null out the selection anchor so that when the bar re-appears,
// it doesn't momentarily "flash" at its old anchor position.
_imageSelectionAnchor.value = null;
if (_imageFormatBarOverlayEntry != null) {
// Remove the image toolbar overlay and null-out the entry.
// We null out the entry because we can't query whether
// or not the entry exists in the overlay, so in our
// case, null implies the entry is not in the overlay,
// and non-null implies the entry is in the overlay.
_imageFormatBarOverlayEntry.remove();
_imageFormatBarOverlayEntry = null;
}
// Ensure that focus returns to the editor.
_editorFocusNode.requestFocus();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
Expanded(
child: _buildEditor(),
),
if (_isMobile) _buildMountedToolbar(),
],
);
}
Widget _buildEditor() {
return SuperEditor(
editor: _docEditor,
composer: _composer,
focusNode: _editorFocusNode,
scrollController: _scrollController,
documentLayoutKey: _docLayoutKey,
componentBuilders: [
...defaultComponentBuilders,
],
gestureMode: _gestureMode,
inputSource: _inputSource,
androidToolbarBuilder: (_) => AndroidTextEditingFloatingToolbar(
onCutPressed: _cut,
onCopyPressed: _copy,
onPastePressed: _paste,
onSelectAllPressed: _selectAll,
),
iOSToolbarBuilder: (_) => IOSTextEditingFloatingToolbar(
onCutPressed: _cut,
onCopyPressed: _copy,
onPastePressed: _paste,
),
);
}
Widget _buildMountedToolbar() {
return MultiListenableBuilder(
listenables: <Listenable>{
_doc,
_composer.selectionNotifier,
},
builder: (_) {
final selection = _composer.selection;
if (selection == null) {
return const SizedBox();
}
return KeyboardEditingToolbar(
document: _doc,
composer: _composer,
commonOps: _docOps,
);
},
);
}
}