import 'dart:convert';
import 'dart:html';
import 'dart:io' as io;
import 'dart:math' as math;
import 'package:flutter/cupertino.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter/services.dart';
import 'package:flutter_quill/models/documents/attribute.dart';
import 'package:flutter_quill/models/documents/document.dart';
import 'package:flutter_quill/models/documents/nodes/container.dart'
as containerNode;
import 'package:flutter_quill/models/documents/nodes/embed.dart';
import 'package:flutter_quill/models/documents/nodes/leaf.dart' as leaf;
import 'package:flutter_quill/models/documents/nodes/line.dart';
import 'package:flutter_quill/models/documents/nodes/node.dart';
import 'package:flutter_quill/utils/universal_ui/universal_ui.dart';
import 'package:flutter_quill/widgets/image.dart';
import 'package:flutter_quill/widgets/raw_editor.dart';
import 'package:flutter_quill/widgets/responsive_widget.dart';
import 'package:flutter_quill/widgets/text_selection.dart';
import 'package:string_validator/string_validator.dart';
// import 'package:universal_html/prefer_universal/html.dart' as html;
import 'package:url_launcher/url_launcher.dart';
import 'box.dart';
import 'controller.dart';
import 'cursor.dart';
import 'default_styles.dart';
import 'delegate.dart';
const linkPrefixes = [
'mailto:', // email
'tel:', // telephone
'sms:', // SMS
'callto:',
'wtai:',
'market:',
'geopoint:',
'ymsgr:',
'msnim:',
'gtalk:', // Google Talk
'skype:',
'sip:', // Lync
'whatsapp:',
'http'
];
abstract class EditorState extends State {
TextEditingValue getTextEditingValue();
void setTextEditingValue(TextEditingValue value);
RenderEditor? getRenderEditor();
EditorTextSelectionOverlay? getSelectionOverlay();
bool showToolbar();
void hideToolbar();
void requestKeyboard();
}
abstract class RenderAbstractEditor {
TextSelection selectWordAtPosition(TextPosition position);
TextSelection selectLineAtPosition(TextPosition position);
double preferredLineHeight(TextPosition position);
TextPosition getPositionForOffset(Offset offset);
List getEndpointsForSelection(
TextSelection textSelection);
void handleTapDown(TapDownDetails details);
void selectWordsInRange(
Offset from,
Offset to,
SelectionChangedCause cause,
);
void selectWordEdge(SelectionChangedCause cause);
void selectPositionAt(Offset from, Offset to, SelectionChangedCause cause);
void selectWord(SelectionChangedCause cause);
void selectPosition(SelectionChangedCause cause);
}
Widget _defaultEmbedBuilder(BuildContext context, leaf.Embed node) {
switch (node.value.type) {
case 'image':
String imageUrl = node.value.data;
return imageUrl.startsWith('http')
? Image.network(imageUrl)
: Image.file(io.File(imageUrl));
default:
throw UnimplementedError(
'Embeddable type "${node.value.type}" is not supported by default embed '
'builder of QuillEditor. You must pass your own builder function to '
'embedBuilder property of QuillEditor or QuillField widgets.');
}
}
Widget _defaultEmbedBuilderWeb(BuildContext context, leaf.Embed node) {
switch (node.value.type) {
case 'image':
String imageUrl = node.value.data;
Size size = MediaQuery.of(context).size;
UniversalUI().platformViewRegistry.registerViewFactory(
imageUrl, (int viewId) => ImageElement()..src = imageUrl);
return Padding(
padding: EdgeInsets.only(
right: ResponsiveWidget.isMediumScreen(context)
? size.width * 0.5
: (ResponsiveWidget.isLargeScreen(context))
? size.width * 0.75
: size.width * 0.2,
),
child: SizedBox(
height: MediaQuery.of(context).size.height * 0.45,
child: HtmlElementView(
viewType: imageUrl,
),
),
);
default:
throw UnimplementedError(
'Embeddable type "${node.value.type}" is not supported by default embed '
'builder of QuillEditor. You must pass your own builder function to '
'embedBuilder property of QuillEditor or QuillField widgets.');
}
}
class QuillEditor extends StatefulWidget {
final QuillController controller;
final FocusNode focusNode;
final ScrollController scrollController;
final bool scrollable;
final EdgeInsetsGeometry padding;
final bool autoFocus;
final bool? showCursor;
final bool readOnly;
final String? placeholder;
final bool? enableInteractiveSelection;
final double? minHeight;
final double? maxHeight;
final DefaultStyles? customStyles;
final bool expands;
final TextCapitalization textCapitalization;
final Brightness keyboardAppearance;
final ScrollPhysics? scrollPhysics;
final ValueChanged? onLaunchUrl;
final EmbedBuilder embedBuilder;
QuillEditor(
{required this.controller,
required this.focusNode,
required this.scrollController,
required this.scrollable,
required this.padding,
required this.autoFocus,
this.showCursor,
required this.readOnly,
this.placeholder,
this.enableInteractiveSelection,
this.minHeight,
this.maxHeight,
this.customStyles,
required this.expands,
this.textCapitalization = TextCapitalization.sentences,
this.keyboardAppearance = Brightness.light,
this.scrollPhysics,
this.onLaunchUrl,
this.embedBuilder =
kIsWeb ? _defaultEmbedBuilderWeb : _defaultEmbedBuilder})
: assert(controller != null),
assert(scrollController != null),
assert(scrollable != null),
assert(focusNode != null),
assert(autoFocus != null),
assert(readOnly != null),
assert(embedBuilder != null);
factory QuillEditor.basic(
{required QuillController controller, required bool readOnly}) {
return QuillEditor(
controller: controller,
scrollController: ScrollController(),
scrollable: true,
focusNode: FocusNode(),
autoFocus: true,
readOnly: readOnly,
enableInteractiveSelection: true,
expands: false,
padding: EdgeInsets.zero);
}
@override
_QuillEditorState createState() => _QuillEditorState();
}
class _QuillEditorState extends State
implements EditorTextSelectionGestureDetectorBuilderDelegate {
final GlobalKey _editorKey = GlobalKey();
late EditorTextSelectionGestureDetectorBuilder _selectionGestureDetectorBuilder;
@override
void initState() {
super.initState();
_selectionGestureDetectorBuilder =
_QuillEditorSelectionGestureDetectorBuilder(this);
}
@override
Widget build(BuildContext context) {
ThemeData theme = Theme.of(context);
TextSelectionThemeData selectionTheme = TextSelectionTheme.of(context);
TextSelectionControls textSelectionControls;
bool paintCursorAboveText;
bool cursorOpacityAnimates;
Offset? cursorOffset;
Color? cursorColor;
Color selectionColor;
Radius? cursorRadius;
switch (theme.platform) {
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
textSelectionControls = materialTextSelectionControls;
paintCursorAboveText = false;
cursorOpacityAnimates = false;
cursorColor ??= selectionTheme.cursorColor ?? theme.colorScheme.primary;
selectionColor = selectionTheme.selectionColor ??
theme.colorScheme.primary.withOpacity(0.40);
break;
case TargetPlatform.iOS:
case TargetPlatform.macOS:
CupertinoThemeData cupertinoTheme = CupertinoTheme.of(context);
textSelectionControls = cupertinoTextSelectionControls;
paintCursorAboveText = true;
cursorOpacityAnimates = true;
cursorColor ??=
selectionTheme.cursorColor ?? cupertinoTheme.primaryColor;
selectionColor = selectionTheme.selectionColor ??
cupertinoTheme.primaryColor.withOpacity(0.40);
cursorRadius ??= const Radius.circular(2.0);
cursorOffset = Offset(
iOSHorizontalOffset / MediaQuery.of(context).devicePixelRatio, 0);
break;
default:
throw UnimplementedError();
}
return _selectionGestureDetectorBuilder.build(
HitTestBehavior.translucent,
RawEditor(
_editorKey,
widget.controller,
widget.focusNode,
widget.scrollController,
widget.scrollable,
widget.padding,
widget.readOnly,
widget.placeholder,
widget.onLaunchUrl,
ToolbarOptions(
copy: widget.enableInteractiveSelection ?? true,
cut: widget.enableInteractiveSelection ?? true,
paste: widget.enableInteractiveSelection ?? true,
selectAll: widget.enableInteractiveSelection ?? true,
),
theme.platform == TargetPlatform.iOS ||
theme.platform == TargetPlatform.android,
widget.showCursor,
CursorStyle(
color: cursorColor,
backgroundColor: Colors.grey,
width: 2.0,
radius: cursorRadius,
offset: cursorOffset,
paintAboveText: paintCursorAboveText,
opacityAnimates: cursorOpacityAnimates,
),
widget.textCapitalization,
widget.maxHeight,
widget.minHeight,
widget.customStyles,
widget.expands,
widget.autoFocus,
selectionColor,
textSelectionControls,
widget.keyboardAppearance,
widget.enableInteractiveSelection!,
widget.scrollPhysics,
widget.embedBuilder),
);
}
@override
GlobalKey getEditableTextKey() {
return _editorKey;
}
@override
bool getForcePressEnabled() {
return false;
}
@override
bool? getSelectionEnabled() {
return widget.enableInteractiveSelection;
}
_requestKeyboard() {
_editorKey.currentState!.requestKeyboard();
}
}
class _QuillEditorSelectionGestureDetectorBuilder
extends EditorTextSelectionGestureDetectorBuilder {
final _QuillEditorState _state;
_QuillEditorSelectionGestureDetectorBuilder(this._state) : super(_state);
@override
onForcePressStart(ForcePressDetails details) {
super.onForcePressStart(details);
if (delegate.getSelectionEnabled()! && shouldShowSelectionToolbar) {
getEditor()!.showToolbar();
}
}
@override
onForcePressEnd(ForcePressDetails details) {}
@override
void onSingleLongTapMoveUpdate(LongPressMoveUpdateDetails details) {
if (!delegate.getSelectionEnabled()!) {
return;
}
switch (Theme.of(_state.context).platform) {
case TargetPlatform.iOS:
case TargetPlatform.macOS:
getRenderEditor()!.selectPositionAt(
details.globalPosition,
null,
SelectionChangedCause.longPress,
);
break;
case TargetPlatform.android:
case TargetPlatform.fuchsia:
case TargetPlatform.linux:
case TargetPlatform.windows:
getRenderEditor()!.selectWordsInRange(
details.globalPosition - details.offsetFromOrigin,
details.globalPosition,
SelectionChangedCause.longPress,
);
break;
default:
throw ('Invalid platform');
}
}
bool _onTapping(TapUpDetails details) {
if (_state.widget.controller.document.isEmpty()) {
return false;
}
TextPosition pos =
getRenderEditor()!.getPositionForOffset(details.globalPosition);
containerNode.ChildQuery result =
getEditor()!.widget.controller.document.queryChild(pos.offset);
if (result.node == null) {
return false;
}
Line line = result.node as Line;
containerNode.ChildQuery segmentResult =
line.queryChild(result.offset, false);
if (segmentResult.node == null) {
if (line.length == 1) {
// tapping when no text yet on this line
_flipListCheckbox(pos, line, segmentResult);
getEditor()!.widget.controller.updateSelection(
TextSelection.collapsed(offset: pos.offset), ChangeSource.LOCAL);
return true;
}
return false;
}
leaf.Leaf segment = segmentResult.node as leaf.Leaf;
if (segment.style.containsKey(Attribute.link.key)) {
var launchUrl = getEditor()!.widget.onLaunchUrl;
if (launchUrl == null) {
launchUrl = _launchUrl;
}
String? link = segment.style.attributes[Attribute.link.key]!.value;
if (getEditor()!.widget.readOnly && link != null) {
link = link.trim();
if (!linkPrefixes
.any((linkPrefix) => link!.toLowerCase().startsWith(linkPrefix))) {
link = 'https://$link';
}
launchUrl(link);
}
return false;
}
if (getEditor()!.widget.readOnly && segment.value is BlockEmbed) {
BlockEmbed blockEmbed = segment.value as BlockEmbed;
if (blockEmbed.type == 'image') {
final String imageUrl = blockEmbed.data;
Navigator.push(
getEditor()!.context,
MaterialPageRoute(
builder: (context) => ImageTapWrapper(
imageProvider: imageUrl.startsWith('http')
? NetworkImage(imageUrl)
: isBase64(imageUrl)
? Image.memory(base64.decode(imageUrl)) as ImageProvider