diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c917d7c..f12301a3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ +## [1.7.0] - 2021-03-17 +* [BREAKING]: + * Refactored `height`, `autoAdjustHeight`, `decoration`, `showBottomToolbar`, and `darkMode` into new `HtmlEditorOptions` class - see README for how to migrate + * Removed 'Summernote Classes' plugin + * Sorry for all the breaking changes lately - I think I've finally figured out how I want to do the API design so there should be far less in future releases +* Added `onImageUpload` callback that fires when an image is inserted via `` +* Added `onImageLinkInsert` callback that fires when an image is inserted via URL +* Added `shouldEnsureVisible` that scrolls the editor into view when it is focused or text is typed, kind of like `TextField`s +* Added `adjustHeightForKeyboard` (default true) that adjusts the editor's height when the keyboard is active to ensure no content is cut off by the keyboard +* Added `filePath` which allows you to provide a completely custom HTML file to load +* If you plan on using any of the above, it is highly recommend looking at the README for more details and examples. +* Removed disabled scroll feature since it prevented the editor from scrolling even when the editor content was larger than the height +* Code cleanup + ## [1.6.0] - 2021-03-13 * [BREAKING] removed `dispose()` method on controller * The editor no longer uses a `Stream` to get text and therefore nothing needs to be disposed diff --git a/README.md b/README.md index d51ec053..bf9afddd 100644 --- a/README.md +++ b/README.md @@ -93,7 +93,7 @@ More is on the way! File a feature request or contribute to the project if you'd ## Setup -Add `html_editor_enhanced: ^1.6.0` as dependency to your pubspec.yaml +Add `html_editor_enhanced: ^1.7.0` as dependency to your pubspec.yaml Additional setup is required to allow the user to pick images via ``: @@ -248,7 +248,7 @@ Parameter | Type | Default | Description ------------ | ------------- | ------------- | ------------- **height** | `double` | 380 | Height of text editor (does not set the height in HTML yet, only the height of the WebView widget) **autoAdjustHeight** | `bool` | `true` | Automatically adjust the height of the text editor by analyzing the HTML height once the editor is loaded. Recommended value: `true`. See [below](#autoadjustheight) for more details. -**adjustHeightForKeyboard** | `bool` | `true` | Adjust the height of the editor if the keyboard is active and it overlaps the editor to prevent the overlap. Recommended value: `true`. See [below](#adjustheightforkeyboard) for more details. +**adjustHeightForKeyboard** | `bool` | `true` | Adjust the height of the editor if the keyboard is active and it overlaps the editor to prevent the overlap. Recommended value: `true`, only works on mobile. See [below](#adjustheightforkeyboard) for more details. **decoration** | `BoxDecoration` | | `BoxDecoration` that surrounds the widget **showBottomToolbar** | `bool` | true | Show or hide bottom toolbar **hint** | `String` | empty | Placeholder hint text @@ -436,7 +436,7 @@ If this does not help your use case feel free to disable it, but the recommended ### `adjustHeightForKeyboard` -Default value: true +Default value: true, only considered on mobile This option parameter changes the height of the editor if the keyboard is active and it overlaps with the editor. diff --git a/example/lib/main.dart b/example/lib/main.dart index 2d0030bc..06823263 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -70,10 +70,8 @@ class _HtmlEditorExampleState extends State { controller: controller, hint: "Your text here...", //initialText: "

text content initial, if any

", - options: HtmlEditorOptions( - height: 450, - shouldEnsureVisible: true - ), + options: + HtmlEditorOptions(height: 450, shouldEnsureVisible: true), callbacks: Callbacks( onChange: (String? changed) { print("content changed to $changed"); @@ -137,23 +135,25 @@ class _HtmlEditorExampleState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( - style: - TextButton.styleFrom(backgroundColor: Colors.blueGrey), + style: TextButton.styleFrom( + backgroundColor: Colors.blueGrey), onPressed: () { controller.undo(); }, - child: Text("Undo", style: TextStyle(color: Colors.white)), + child: + Text("Undo", style: TextStyle(color: Colors.white)), ), SizedBox( width: 16, ), TextButton( - style: - TextButton.styleFrom(backgroundColor: Colors.blueGrey), + style: TextButton.styleFrom( + backgroundColor: Colors.blueGrey), onPressed: () { controller.clear(); }, - child: Text("Reset", style: TextStyle(color: Colors.white)), + child: + Text("Reset", style: TextStyle(color: Colors.white)), ), SizedBox( width: 16, @@ -205,13 +205,13 @@ class _HtmlEditorExampleState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ TextButton( - style: - TextButton.styleFrom(backgroundColor: Colors.blueGrey), + style: TextButton.styleFrom( + backgroundColor: Colors.blueGrey), onPressed: () { controller.disable(); }, - child: - Text("Disable", style: TextStyle(color: Colors.white)), + child: Text("Disable", + style: TextStyle(color: Colors.white)), ), SizedBox( width: 16, diff --git a/example/pubspec.lock b/example/pubspec.lock index 6c0c8cdc..e18d12e6 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -80,7 +80,7 @@ packages: path: ".." relative: true source: path - version: "1.6.0" + version: "1.7.0" js: dependency: transitive description: diff --git a/lib/src/html_editor_controller_mobile.dart b/lib/src/html_editor_controller_mobile.dart index 6888a290..4bd57c3a 100644 --- a/lib/src/html_editor_controller_mobile.dart +++ b/lib/src/html_editor_controller_mobile.dart @@ -153,7 +153,9 @@ class HtmlEditorController extends unsupported.HtmlEditorController { /// accommodate the keyboard. This should only be used on mobile, and only /// when [adjustHeightForKeyboard] is enabled. void resetHeight() { - _evaluateJavascript(source: "console.log('_HtmlEditorWidgetMobileState().resetHeight();');"); + _evaluateJavascript( + source: + "console.log('_HtmlEditorWidgetMobileState().resetHeight();');"); } /// Reloads the IFrameElement, throws an exception on mobile diff --git a/lib/src/widgets/html_editor_widget_mobile.dart b/lib/src/widgets/html_editor_widget_mobile.dart index 5f78ebd9..15d2d637 100644 --- a/lib/src/widgets/html_editor_widget_mobile.dart +++ b/lib/src/widgets/html_editor_widget_mobile.dart @@ -65,7 +65,8 @@ class _HtmlEditorWidgetMobileState extends State { if (widget.options.filePath != null) { filePath = widget.options.filePath!; } else if (widget.plugins.isEmpty) { - filePath = 'packages/html_editor_enhanced/assets/summernote-no-plugins.html'; + filePath = + 'packages/html_editor_enhanced/assets/summernote-no-plugins.html'; } else { filePath = 'packages/html_editor_enhanced/assets/summernote.html'; } @@ -78,6 +79,7 @@ class _HtmlEditorWidgetMobileState extends State { super.dispose(); } + /// resets the height of the editor to the original height void resetHeight() async { setState(() { if (docHeight != null) { @@ -86,8 +88,8 @@ class _HtmlEditorWidgetMobileState extends State { actualHeight = widget.options.height + 125; } }); - await controllerMap[widget.controller].evaluateJavascript(source: - "\$('div.note-editable').height(${widget.options.height});"); + await controllerMap[widget.controller].evaluateJavascript( + source: "\$('div.note-editable').height(${widget.options.height});"); } @override @@ -116,8 +118,8 @@ class _HtmlEditorWidgetMobileState extends State { }, initialOptions: InAppWebViewGroupOptions( crossPlatform: InAppWebViewOptions( - javaScriptEnabled: true, - transparentBackground: true, + javaScriptEnabled: true, + transparentBackground: true, ), android: AndroidInAppWebViewOptions( useHybridComposition: true, @@ -129,14 +131,16 @@ class _HtmlEditorWidgetMobileState extends State { }, onConsoleMessage: (controller, message) { print(message.message); - if (message.message == "_HtmlEditorWidgetMobileState().resetHeight();") + if (message.message == + "_HtmlEditorWidgetMobileState().resetHeight();") resetHeight(); }, onWindowFocus: (controller) async { - if (widget.options.shouldEnsureVisible && Scrollable.of(context) != null) { + if (widget.options.shouldEnsureVisible && + Scrollable.of(context) != null) { Scrollable.of(context)!.position.ensureVisible( - context.findRenderObject()!, - ); + context.findRenderObject()!, + ); } if (widget.options.adjustHeightForKeyboard) { visibleStream.stream.drain(); @@ -145,10 +149,12 @@ class _HtmlEditorWidgetMobileState extends State { if (docHeight != null) { newHeight = (docHeight! + 40) * visibleDecimal; } else { - newHeight = (widget.options.height + 125) * visibleDecimal; + newHeight = + (widget.options.height + 125) * visibleDecimal; } - await controller.evaluateJavascript(source: - "\$('div.note-editable').height(${max(widget.options.height - (actualHeight - newHeight) - 10, 30)});"); + await controller.evaluateJavascript( + source: + "\$('div.note-editable').height(${max(widget.options.height - (actualHeight - newHeight) - 10, 30)});"); setState(() { if (docHeight != null) { actualHeight = newHeight; @@ -158,7 +164,8 @@ class _HtmlEditorWidgetMobileState extends State { }); } }, - onLoadStop: (InAppWebViewController controller, Uri? uri) async { + onLoadStop: + (InAppWebViewController controller, Uri? uri) async { String url = uri.toString(); if (url.contains(filePath)) { String summernoteToolbar = "[\n"; @@ -190,11 +197,13 @@ class _HtmlEditorWidgetMobileState extends State { }, """; if (p.onSelect != null) { - controllerMap[widget.controller].addJavaScriptHandler( - handlerName: 'onSelectMention', - callback: (value) { - p.onSelect!.call(value.first.toString()); - }); + controllerMap[widget.controller] + .addJavaScriptHandler( + handlerName: 'onSelectMention', + callback: (value) { + p.onSelect! + .call(value.first.toString()); + }); } } if (p is SummernoteFile) { @@ -212,27 +221,30 @@ class _HtmlEditorWidgetMobileState extends State { window.flutter_inappwebview.callHandler('onFileUpload', JSON.stringify(newObject)); }, """; - controllerMap[widget.controller].addJavaScriptHandler( - handlerName: 'onFileUpload', - callback: (files) { - FileUpload file = - fileUploadFromJson(files.first); - p.onFileUpload!.call(file); - }); + controllerMap[widget.controller] + .addJavaScriptHandler( + handlerName: 'onFileUpload', + callback: (files) { + FileUpload file = + fileUploadFromJson(files.first); + p.onFileUpload!.call(file); + }); } } } } if (widget.callbacks != null) { if (widget.callbacks!.onImageLinkInsert != null) { - summernoteCallbacks = summernoteCallbacks + """ + summernoteCallbacks = summernoteCallbacks + + """ onImageLinkInsert: function(url) { window.flutter_inappwebview.callHandler('onImageLinkInsert', url); }, """; } if (widget.callbacks!.onImageUpload != null) { - summernoteCallbacks = summernoteCallbacks + """ + summernoteCallbacks = summernoteCallbacks + + """ onImageUpload: function(files) { var newObject = { 'lastModified': files[0].lastModified, @@ -304,13 +316,16 @@ class _HtmlEditorWidgetMobileState extends State { controller.addJavaScriptHandler( handlerName: 'onChange', callback: (contents) { - if (widget.options.shouldEnsureVisible && Scrollable.of(context) != null) { + if (widget.options.shouldEnsureVisible && + Scrollable.of(context) != null) { Scrollable.of(context)!.position.ensureVisible( - context.findRenderObject()!, - ); + context.findRenderObject()!, + ); } - if (widget.callbacks != null && widget.callbacks!.onChange != null) { - widget.callbacks!.onChange!.call(contents.first.toString()); + if (widget.callbacks != null && + widget.callbacks!.onChange != null) { + widget.callbacks!.onChange! + .call(contents.first.toString()); } }); } @@ -425,8 +440,7 @@ class _HtmlEditorWidgetMobileState extends State { controllerMap[widget.controller].addJavaScriptHandler( handlerName: 'onImageUpload', callback: (files) { - FileUpload file = - fileUploadFromJson(files.first); + FileUpload file = fileUploadFromJson(files.first); c.onImageUpload!.call(file); }); } diff --git a/lib/src/widgets/html_editor_widget_web.dart b/lib/src/widgets/html_editor_widget_web.dart index 43926eb6..c88e56ba 100644 --- a/lib/src/widgets/html_editor_widget_web.dart +++ b/lib/src/widgets/html_editor_widget_web.dart @@ -80,8 +80,8 @@ class _HtmlEditorWidgetWebState extends State { (p == widget.plugins.last ? "]]\n" : p.getToolbarString().isNotEmpty - ? ", " - : ""); + ? ", " + : ""); headString = headString + p.getHeadString() + "\n"; if (p is SummernoteAtMention) { summernoteCallbacks = summernoteCallbacks + @@ -136,7 +136,8 @@ class _HtmlEditorWidgetWebState extends State { } if (widget.callbacks != null) { if (widget.callbacks!.onImageLinkInsert != null) { - summernoteCallbacks = summernoteCallbacks + """ + summernoteCallbacks = summernoteCallbacks + + """ onImageLinkInsert: function(url) { console.log('fired'); window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageLinkInsert", "url": url}), "*"); @@ -144,7 +145,8 @@ class _HtmlEditorWidgetWebState extends State { """; } if (widget.callbacks!.onImageUpload != null) { - summernoteCallbacks = summernoteCallbacks + """ + summernoteCallbacks = summernoteCallbacks + + """ onImageUpload: function(files) { window.parent.postMessage(JSON.stringify({"view": "$createdViewId", "type": "toDart: onImageUpload", "lastModified": files[0].lastModified, "lastModifiedDate": files[0].lastModifiedDate, "name": files[0].name, "size": files[0].size, "mimeType": files[0].type}), "*"); }, @@ -156,10 +158,10 @@ class _HtmlEditorWidgetWebState extends State { print(summernoteCallbacks); String darkCSS = ""; if ((Theme.of(widget.initBC).brightness == Brightness.dark || - widget.options.darkMode == true) && + widget.options.darkMode == true) && widget.options.darkMode != false) { darkCSS = - ""; + ""; } String jsCallbacks = ""; if (widget.callbacks != null) @@ -252,17 +254,20 @@ class _HtmlEditorWidgetWebState extends State { $jsCallbacks """; - String filePath = 'packages/html_editor_enhanced/assets/summernote-no-plugins.html'; - if (widget.options.filePath != null) - filePath = widget.options.filePath!; + String filePath = + 'packages/html_editor_enhanced/assets/summernote-no-plugins.html'; + if (widget.options.filePath != null) filePath = widget.options.filePath!; String htmlString = await rootBundle.loadString(filePath); htmlString = htmlString .replaceFirst("", darkCSS) .replaceFirst("", headString) .replaceFirst("", summernoteScripts) - .replaceFirst("jquery.min.js", "assets/packages/html_editor_enhanced/assets/jquery.min.js") - .replaceFirst("summernote-lite.min.css", "assets/packages/html_editor_enhanced/assets/summernote-lite.min.css") - .replaceFirst("summernote-lite.min.js", "assets/packages/html_editor_enhanced/assets/summernote-lite.min.js"); + .replaceFirst("jquery.min.js", + "assets/packages/html_editor_enhanced/assets/jquery.min.js") + .replaceFirst("summernote-lite.min.css", + "assets/packages/html_editor_enhanced/assets/summernote-lite.min.css") + .replaceFirst("summernote-lite.min.js", + "assets/packages/html_editor_enhanced/assets/summernote-lite.min.js"); if (widget.callbacks != null) addJSListener(widget.callbacks!); final html.IFrameElement iframe = html.IFrameElement() ..width = MediaQuery.of(widget.initBC).size.width.toString() //'800' @@ -272,8 +277,7 @@ class _HtmlEditorWidgetWebState extends State { ..srcdoc = htmlString ..style.border = 'none' ..onLoad.listen((event) async { - if (widget.callbacks != null && - widget.callbacks!.onInit != null) + if (widget.callbacks != null && widget.callbacks!.onInit != null) widget.callbacks!.onInit!.call(); if (widget.value != null) widget.controller.setText(widget.value!); Map data = {"type": "toIframe: getHeight"}; @@ -286,15 +290,14 @@ class _HtmlEditorWidgetWebState extends State { if (data["type"] != null && data["type"].contains("toDart: onChange") && data["view"] == createdViewId) { - if (widget.callbacks != null && - widget.callbacks!.onChange != null) + if (widget.callbacks != null && widget.callbacks!.onChange != null) widget.callbacks!.onChange!.call(data["contents"]); - if (widget.options.shouldEnsureVisible && Scrollable.of(context) != null) { + if (widget.options.shouldEnsureVisible && + Scrollable.of(context) != null) { Scrollable.of(context)!.position.ensureVisible( context.findRenderObject()!, duration: const Duration(milliseconds: 100), - curve: Curves.easeIn - ); + curve: Curves.easeIn); } } }); @@ -309,24 +312,28 @@ class _HtmlEditorWidgetWebState extends State { @override Widget build(BuildContext context) { return Container( - height: widget.options.autoAdjustHeight ? actualHeight : widget.options.height, + height: widget.options.autoAdjustHeight + ? actualHeight + : widget.options.height, child: Column( children: [ Expanded( child: Directionality( textDirection: TextDirection.ltr, child: FutureBuilder( - future: summernoteInit, - builder: (context, snapshot) { - if (snapshot.hasData) { - return HtmlElementView( - viewType: createdViewId, - ); - } else { - return Container(height: widget.options.autoAdjustHeight ? actualHeight : widget.options.height); - } - } - ))), + future: summernoteInit, + builder: (context, snapshot) { + if (snapshot.hasData) { + return HtmlElementView( + viewType: createdViewId, + ); + } else { + return Container( + height: widget.options.autoAdjustHeight + ? actualHeight + : widget.options.height); + } + }))), widget.options.showBottomToolbar ? Divider(height: 0) : Container(height: 0, width: 0), @@ -407,7 +414,8 @@ class _HtmlEditorWidgetWebState extends State { if (data["type"] != null && data["type"].contains("toDart:") && data["view"] == createdViewId) { - if (data["type"].contains("htmlHeight") && widget.options.autoAdjustHeight) { + if (data["type"].contains("htmlHeight") && + widget.options.autoAdjustHeight) { final docHeight = data["height"] ?? actualHeight; if ((docHeight != null && docHeight != actualHeight) && mounted) { setState(() { diff --git a/lib/utils/toolbar_widget.dart b/lib/utils/toolbar_widget.dart index 0078cf0d..cc709539 100644 --- a/lib/utils/toolbar_widget.dart +++ b/lib/utils/toolbar_widget.dart @@ -10,41 +10,38 @@ class ToolbarWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Padding( - padding: const EdgeInsets.only( - left: 4, right: 4, bottom: 8, top: 8), + padding: const EdgeInsets.only(left: 4, right: 4, bottom: 8, top: 8), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ - toolbarIcon(context, Icons.content_copy, "Copy", - onTap: () async { - String? data = await controller.getText(); - Clipboard.setData(new ClipboardData(text: data)); - }), - toolbarIcon(context, Icons.content_paste, "Paste", - onTap: () async { - ClipboardData? data = - await Clipboard.getData(Clipboard.kTextPlain); - if (data != null) { - String text = data.text!; - if (controller.processInputHtml) { - text = text - .replaceAll("'", '\\"') - .replaceAll('"', '\\"') - .replaceAll("[", "\\[") - .replaceAll("]", "\\]") - .replaceAll("\n", "
") - .replaceAll("\n\n", "
") - .replaceAll("\r", " ") - .replaceAll('\r\n', " "); - } - controller.insertHtml(text); - } - }), + toolbarIcon(context, Icons.content_copy, "Copy", onTap: () async { + String? data = await controller.getText(); + Clipboard.setData(new ClipboardData(text: data)); + }), + toolbarIcon(context, Icons.content_paste, "Paste", onTap: () async { + ClipboardData? data = await Clipboard.getData(Clipboard.kTextPlain); + if (data != null) { + String text = data.text!; + if (controller.processInputHtml) { + text = text + .replaceAll("'", '\\"') + .replaceAll('"', '\\"') + .replaceAll("[", "\\[") + .replaceAll("]", "\\]") + .replaceAll("\n", "
") + .replaceAll("\n\n", "
") + .replaceAll("\r", " ") + .replaceAll('\r\n', " "); + } + controller.insertHtml(text); + } + }), ], ), ); } } + /// Widget for the toolbar icon Widget toolbarIcon(BuildContext context, IconData icon, String title, {required Function() onTap}) { diff --git a/pubspec.yaml b/pubspec.yaml index a8fecbae..fd7fe6a5 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: html_editor_enhanced description: HTML rich text editor for Android, iOS, and Web, using the Summernote library. Enhanced with critical bug fixes and support for callbacks, plugins, dark mode, and much more. -version: 1.6.0 +version: 1.7.0 homepage: https://github.com/tneotia/html-editor-enhanced environment: