diff --git a/example/.gitignore b/example/.gitignore index 9d532b1..0fa6b67 100644 --- a/example/.gitignore +++ b/example/.gitignore @@ -39,3 +39,8 @@ app.*.symbols # Obfuscation related app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/.metadata b/example/.metadata index cd984dd..80206eb 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,7 @@ # This file should be version controlled and should not be manually edited. version: - revision: 9b2d32b605630f28625709ebd9d78ab3016b2bf6 + revision: c5a4b4029c0798f37c4a39b479d7cb75daa7b05c channel: stable project_type: app diff --git a/example/README.md b/example/README.md index f7c129a..44b9c0f 100644 --- a/example/README.md +++ b/example/README.md @@ -1,5 +1,16 @@ -# enough_html_editor_example +# enough_html_example -Example for enough_html_editor usage +A new Flutter project. -Please refer to the documentation for details. \ No newline at end of file +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) + +For help getting started with Flutter, view our +[online documentation](https://flutter.dev/docs), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle index 5145502..f6cef72 100644 --- a/example/android/app/build.gradle +++ b/example/android/app/build.gradle @@ -26,21 +26,17 @@ apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" android { - compileSdkVersion 29 + compileSdkVersion 30 sourceSets { main.java.srcDirs += 'src/main/kotlin' } - lintOptions { - disable 'InvalidPackage' - } - defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId "de.enough.enough_html_editor_example" - minSdkVersion 16 - targetSdkVersion 29 + applicationId "de.enough.enough_html_example" + minSdkVersion 17 + targetSdkVersion 30 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index 651c97c..f6d6d4c 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="de.enough.enough_html_example"> diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index 5bb1a5b..1f6f061 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,13 +1,7 @@ - - + + + + + + + + diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..449a9f9 --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml index 1f83a33..d74aa35 100644 --- a/example/android/app/src/main/res/values/styles.xml +++ b/example/android/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index 651c97c..f6d6d4c 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,5 +1,5 @@ + package="de.enough.enough_html_example"> diff --git a/example/android/build.gradle b/example/android/build.gradle index 3100ad2..c505a86 100644 --- a/example/android/build.gradle +++ b/example/android/build.gradle @@ -6,7 +6,7 @@ buildscript { } dependencies { - classpath 'com.android.tools.build:gradle:3.5.0' + classpath 'com.android.tools.build:gradle:4.1.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" } } diff --git a/example/android/gradle.properties b/example/android/gradle.properties index a673820..94adc3a 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,3 @@ org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true android.enableJetifier=true -android.enableR8=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..bc6a58a 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index 6b4c0f7..9367d48 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index c4c58c9..1e80803 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -289,17 +289,9 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlEditorExample; + PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -421,17 +413,9 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlEditorExample; + PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -448,17 +432,9 @@ CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); - PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlEditorExample; + PRODUCT_BUNDLE_IDENTIFIER = de.enough.enoughHtmlExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata index 1d526a1..919434a 100644 --- a/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -2,6 +2,6 @@ + location = "self:"> diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 99ac040..ddc043b 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -11,7 +11,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - enough_html_editor_example + enough_html_example CFBundlePackageType APPL CFBundleShortVersionString diff --git a/example/pubspec.lock b/example/pubspec.lock index 2f8a8ce..84c181c 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -49,7 +49,7 @@ packages: name: cupertino_icons url: "https://pub.dartlang.org" source: hosted - version: "1.0.0" + version: "1.0.2" enough_html_editor: dependency: "direct main" description: @@ -69,13 +69,13 @@ packages: description: flutter source: sdk version: "0.0.0" - flutter_icons: - dependency: "direct dev" + flutter_inappwebview: + dependency: transitive description: - name: flutter_icons + name: flutter_inappwebview url: "https://pub.dartlang.org" source: hosted - version: "1.1.0" + version: "5.1.0+4" flutter_test: dependency: "direct dev" description: flutter @@ -163,13 +163,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" - webview_flutter: - dependency: "direct dev" - description: - name: webview_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.7" sdks: dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=1.22.2" diff --git a/example/pubspec.yaml b/example/pubspec.yaml index bac2200..19a00cb 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -1,5 +1,5 @@ -name: enough_html_editor_example -description: Example for enough_html_editor usage +name: enough_html_example +description: Shows how to use the enough_html_editor project # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. @@ -23,19 +23,17 @@ environment: dependencies: flutter: sdk: flutter - enough_html_editor: - path: ../../enough_html_editor + enough_html_editor: + path: ../ # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. - cupertino_icons: ^1.0.0 + cupertino_icons: ^1.0.2 dev_dependencies: flutter_test: sdk: flutter - webview_flutter: ^1.0.7 - flutter_icons: ^1.1.0 # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart index 219beaf..19b0038 100644 --- a/example/test/widget_test.dart +++ b/example/test/widget_test.dart @@ -5,53 +5,26 @@ // gestures. You can also use WidgetTester to find child widgets in the widget // tree, read text, and verify that the values of widget properties are correct. -import 'dart:async'; - -import 'package:enough_html_editor/enough_html_editor.dart'; -import 'package:enough_html_editor_example/main.dart'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; +import 'package:enough_html_example/main.dart'; + void main() { - testWidgets('text retrieval smoke test', (WidgetTester tester) async { + testWidgets('Counter increments smoke test', (WidgetTester tester) async { // Build our app and trigger a frame. await tester.pumpWidget(MyApp()); - await tester.pumpAndSettle(); - - expect(find.byType(MaterialApp), findsOneWidget); - expect(find.byType(EditorPage), findsOneWidget); - final expectedHtml = '''

Here is some text

-

Here is bold text

-

Here is some italic sic text

-

Here is bold and italic text

-

Here is bold and italic and underline text

-
  • one list element
  • another point
-
Here is a quote
- that spans several lines
-
- Another second level blockqote -
-
'''; + // Verify that our counter starts at 0. + expect(find.text('0'), findsOneWidget); + expect(find.text('1'), findsNothing); - // Verify that our editor is there. - expect(find.byType(HtmlEditor), findsOneWidget); + // Tap the '+' icon and trigger a frame. + await tester.tap(find.byIcon(Icons.add)); + await tester.pump(); - final editor = tester.firstWidget(find.byType(HtmlEditor)) as HtmlEditor; - expect(editor, isNotNull); - while ((editor.key as GlobalKey).currentState == null) { - print('waiting for state'); - await Future.delayed(const Duration(milliseconds: 300)); - } - final state = (editor.key as GlobalKey).currentState; - final completer = Completer(); - print('waiting for editor to be ready...'); - state.api.onReady = () async { - final html = await state.api.getText(); - expect(html, expectedHtml); - completer.complete(true); - }; - final completed = await completer.future; - expect(completed, isTrue); + // Verify that our counter has incremented. + expect(find.text('0'), findsNothing); + expect(find.text('1'), findsOneWidget); }); } diff --git a/lib/enough_html_editor.dart b/lib/enough_html_editor.dart index cac1edc..39ea8fe 100644 --- a/lib/enough_html_editor.dart +++ b/lib/enough_html_editor.dart @@ -4,3 +4,5 @@ library enough_html_editor; export 'src/editor.dart'; export 'src/editor_controls.dart'; export 'src/editor_api.dart'; +// ensure that dependent package do not need to declare flutter_inappwebview: +export 'package:flutter_inappwebview/flutter_inappwebview.dart'; diff --git a/lib/src/editor.dart b/lib/src/editor.dart index d268259..431739a 100644 --- a/lib/src/editor.dart +++ b/lib/src/editor.dart @@ -1,8 +1,6 @@ -import 'dart:io'; -import 'dart:convert'; import 'editor_api.dart'; import 'package:flutter/widgets.dart'; -import 'package:webview_flutter/webview_flutter.dart'; +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; /// Standard format settings class FormatSettings { @@ -61,6 +59,7 @@ class HtmlEditorState extends State { var isSelectionItalic = false; var isSelectionUnderline = false; var selectionTextAlign = undefined; + var isLineBreakInput = false; var documentHeight; function onSelectionChange() { @@ -72,6 +71,8 @@ class HtmlEditorState extends State { var isUnderline = false; var node = anchorNode; var textAlign = undefined; + var nestedBlockqotes = 0; + var rootBlockquote; while (node.parentNode != null && node.id != 'editor') { if (node.nodeName == 'B') { isBold = true; @@ -79,6 +80,9 @@ class HtmlEditorState extends State { isItalic = true; } else if (node.nodeName == 'U') { isUnderline = true; + } else if (node.nodeName == 'BLOCKQUOTE') { + nestedBlockqotes++; + rootBlockquote = node; } if (textAlign == undefined && node.style?.textAlign != undefined && node.style.textAlign != '') { textAlign = node.style.textAlign; @@ -89,7 +93,6 @@ class HtmlEditorState extends State { isSelectionBold = isBold; isSelectionItalic = isItalic; isSelectionUnderline = isUnderline; - //console.log('bold=', isBold, ', italic=', isItalic, ', underline=', isUnderline); var message = 0; if (isBold) { message += 1; @@ -100,42 +103,93 @@ class HtmlEditorState extends State { if (isUnderline) { message += 4; } - FormatSettings.postMessage(message); + window.flutter_inappwebview.callHandler('FormatSettings', message); } if (textAlign != selectionTextAlign) { selectionTextAlign = textAlign; - AlignSettings.postMessage(textAlign); + window.flutter_inappwebview.callHandler('AlignSettings', textAlign); } + if (isLineBreakInput && nestedBlockqotes > 0 && anchorOffset == focusOffset) { + let rootNode = rootBlockquote.parentNode; + var cloneNode = null; + var requiresCloning = false; + var node = anchorNode; + while (node != rootBlockquote) { + let sibling = node.previousSibling; + if (sibling != null) { + var parentNode = node.parentNode; + var currentSibling = sibling; + while (currentSibling.previousSibling != null) { + currentSibling = currentSibling.previousSibling; + } + var cloneParentNode = document.createElement(parentNode.nodeName); + do { + var nextSibling = currentSibling.nextSibling; + parentNode.removeChild(currentSibling); + cloneParentNode.appendChild(currentSibling); + if (currentSibling == sibling) { + break; + } + currentSibling = nextSibling; + } while (true); + if (cloneNode != null) { + cloneParentNode.appendChild(cloneNode); + } + requiresCloning = true; + cloneNode = cloneParentNode; + } else if (requiresCloning) { + var cloneParentNode = document.createElement(node.nodeName); + cloneParentNode.appendChild(cloneNode); + cloneNode = cloneParentNode; + } + node = node.parentNode; + } + if (cloneNode != null) { + rootNode.insertBefore(cloneNode, rootBlockquote); + } + let textNode = document.createElement("P"); + let textNodeContent = document.createTextNode('_'); + textNode.appendChild(textNodeContent); + rootNode.insertBefore(textNode, rootBlockquote); + let range = new Range(); + range.setStart(textNodeContent, 0); + range.setEnd(textNodeContent, 1); + let selection = getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } + isLineBreakInput = false; } - function onInput() { + function onInput(inputEvent) { + isLineBreakInput = ((inputEvent.inputType == 'insertParagraph') || ((inputEvent.inputType == 'insertText') && (inputEvent.data == null))); var height = document.body.scrollHeight; if (height != documentHeight) { documentHeight = height; - InternalUpdate.postMessage('h' + height); + window.flutter_inappwebview.callHandler('InternalUpdate', 'h' + height); } } function onFocus() { - InternalUpdate.postMessage('onfocus'); + window.flutter_inappwebview.callHandler('InternalUpdate', 'onfocus'); } function onLoaded() { - documentHeight = document.body.scrollHeight; + documentHeight = document.body.scrollHeight; + document.onselectionchange = onSelectionChange; + document.getElementById('editor').oninput = onInput; } - - document.onselectionchange = onSelectionChange; - -
+ +
==content==
'''; String _initialPageContent; - WebViewController _webViewController; + InAppWebViewController _webViewController; double _documentHeight; EditorApi _api; @@ -163,35 +217,22 @@ blockquote { void initState() { super.initState(); _api = EditorApi(this); - - // Enable hybrid composition for better editing support. - if (Platform.isAndroid) { - WebView.platform = SurfaceAndroidWebView(); - } final stylesWithMinHeight = styles.replaceFirst('==minHeight==', '${widget.minHeight}'); final html = _template .replaceFirst('==styles==', stylesWithMinHeight) .replaceFirst('==content==', widget.initialContent ?? ''); - _initialPageContent = 'data:text/html;base64,' + - base64Encode(const Utf8Encoder().convert(html)); + _initialPageContent = html; } @override Widget build(BuildContext context) { if (widget.adjustHeight) { - final screenHeight = MediaQuery.of(context).size.height; - return LayoutBuilder( - builder: (context, constraints) { - if (!constraints.hasBoundedHeight) { - constraints = constraints.copyWith( - maxHeight: _documentHeight ?? screenHeight); - } - return ConstrainedBox( - constraints: constraints, - child: _buildEditor(), - ); - }, + final size = MediaQuery.of(context).size; + return SizedBox( + height: _documentHeight ?? size.height, + width: size.width, + child: _buildEditor(), ); } else { return _buildEditor(); @@ -206,31 +247,58 @@ blockquote { } Widget _buildWebView() { - return WebView( - initialUrl: _initialPageContent, - javascriptMode: JavascriptMode.unrestricted, + return InAppWebView( + key: ValueKey(_initialPageContent), + initialData: InAppWebViewInitialData(data: _initialPageContent), onWebViewCreated: _onWebViewCreated, - javascriptChannels: _buildJsChannels(), - onPageFinished: (url) async { + onLoadStop: (controller, url) async { if (widget.adjustHeight) { - final scrollHeightText = await _webViewController - .evaluateJavascript('document.body.scrollHeight'); - double height = double.tryParse(scrollHeightText); - if ((height != null) && + final scrollHeight = await _webViewController.evaluateJavascript( + source: 'document.body.scrollHeight') as int; + if ((scrollHeight != null) && mounted && - (widget.minHeight == null || (height + 20 > widget.minHeight))) { + (scrollHeight + 20 > widget.minHeight)) { setState(() { - _documentHeight = height + 20; + _documentHeight = scrollHeight + 20.0; }); } } + final scrollWidth = await _webViewController.evaluateJavascript( + source: 'document.body.scrollWidth') as int; + final size = MediaQuery.of(context).size; + print( + 'scrollWidth=$scrollWidth available=${size.width} adjustHeight=${widget.adjustHeight}'); + }, + initialOptions: InAppWebViewGroupOptions( + crossPlatform: InAppWebViewOptions( + useShouldOverrideUrlLoading: true, + verticalScrollBarEnabled: false, + ), + android: AndroidInAppWebViewOptions( + useWideViewPort: false, + loadWithOverviewMode: true, + useHybridComposition: true, + ), + ), + // deny browsing while editing: + shouldOverrideUrlLoading: (controller, action) => + Future.value(NavigationActionPolicy.CANCEL), + onConsoleMessage: (controller, consoleMessage) { + print(consoleMessage); }, ); } - void _onWebViewCreated(WebViewController controller) { + void _onWebViewCreated(InAppWebViewController controller) { _webViewController = controller; _api.webViewController = controller; + controller.addJavaScriptHandler( + handlerName: 'FormatSettings', callback: _onFormatSettingsReceived); + controller.addJavaScriptHandler( + handlerName: 'AlignSettings', callback: _onAlignSettingsReceived); + controller.addJavaScriptHandler( + handlerName: 'InternalUpdate', callback: _onInternalUpdateReceived); + if (widget.onCreated != null) { widget.onCreated(_api); } @@ -239,78 +307,59 @@ blockquote { } } - Set _buildJsChannels() { - return [ - _formatSettingsJavascriptChannel(), - _alignSettingsJavascriptChannel(), - _internalUpdatesJavascriptChannel(), - ].toSet(); - } - - JavascriptChannel _formatSettingsJavascriptChannel() { - return JavascriptChannel( - name: 'FormatSettings', - onMessageReceived: (JavascriptMessage message) { - // print('FormatSettings got update: ${message.message}'); - if (_api.onFormatSettingsChanged != null) { - final numericMessage = int.tryParse(message.message); - if (numericMessage != null) { - final isBold = (numericMessage & 1) == 1; - final isItalic = (numericMessage & 2) == 2; - final isUnderline = (numericMessage & 4) == 4; - _api.onFormatSettingsChanged( - FormatSettings(isBold, isItalic, isUnderline)); - } - } - }, - ); + void _onFormatSettingsReceived(List parameters) { + print('got format $parameters'); + if (_api.onFormatSettingsChanged != null && parameters.isNotEmpty) { + int numericMessage = parameters.first; + if (numericMessage != null) { + final isBold = (numericMessage & 1) == 1; + final isItalic = (numericMessage & 2) == 2; + final isUnderline = (numericMessage & 4) == 4; + _api.onFormatSettingsChanged( + FormatSettings(isBold, isItalic, isUnderline)); + } + } } - JavascriptChannel _alignSettingsJavascriptChannel() { - return JavascriptChannel( - name: 'AlignSettings', - onMessageReceived: (JavascriptMessage message) { - // print('AlignSettings got update: ${message.message}'); - if (_api.onAlignSettingsChanged != null) { - ElementAlign align; - switch (message.message) { - case 'left': - align = ElementAlign.left; - break; - case 'center': - align = ElementAlign.center; - break; - case 'right': - align = ElementAlign.right; - break; - case 'justify': - align = ElementAlign.justify; - break; - } - _api.onAlignSettingsChanged(align); - } - }, - ); + void _onAlignSettingsReceived(List parameters) { + print('got align $parameters'); + if (_api.onAlignSettingsChanged != null && parameters.isNotEmpty) { + ElementAlign align; + switch (parameters.first) { + case 'left': + align = ElementAlign.left; + break; + case 'center': + align = ElementAlign.center; + break; + case 'right': + align = ElementAlign.right; + break; + case 'justify': + align = ElementAlign.justify; + break; + default: + align = ElementAlign.left; + break; + } + _api.onAlignSettingsChanged(align); + } } - JavascriptChannel _internalUpdatesJavascriptChannel() { - return JavascriptChannel( - name: 'InternalUpdate', - onMessageReceived: (JavascriptMessage message) { - print('InternalUpdate got update: ${message.message}'); - if (message.message.startsWith('h')) { - final height = double.tryParse(message.message.substring(1)); - if (height != null) { - setState(() { - _documentHeight = height + 5; - }); - } - } else if (message.message == 'onfocus') { - FocusScope.of(context).unfocus(); - //_webViewController. - //FocusScope.of(context).requestFocus(); + void _onInternalUpdateReceived(List parameters) { + print('InternalUpdate got update: $parameters'); + if (parameters.isNotEmpty) { + final message = parameters.first; + if (message.startsWith('h')) { + final height = double.tryParse(message.substring(1)); + if (height != null) { + setState(() { + _documentHeight = height + 5; + }); } - }, - ); + } else if (message == 'onfocus') { + FocusScope.of(context).unfocus(); + } + } } } diff --git a/lib/src/editor_api.dart b/lib/src/editor_api.dart index 8c22d79..118f3e9 100644 --- a/lib/src/editor_api.dart +++ b/lib/src/editor_api.dart @@ -1,15 +1,16 @@ +import 'package:flutter_inappwebview/flutter_inappwebview.dart'; import 'editor.dart'; -import 'package:webview_flutter/webview_flutter.dart'; /// API to control the `HtmlEditor`. /// /// Get access to this API either by waiting for the `HtmlEditor.onCreated()` callback or by accessing /// the `HtmlEditorState` with a `GlobalKey`. class EditorApi { - WebViewController _webViewController; + InAppWebViewController _webViewController; final HtmlEditorState _htmlEditorState; - set webViewController(WebViewController value) => _webViewController = value; + set webViewController(InAppWebViewController value) => + _webViewController = value; /// Define any custom CSS styles, replacing the existing styles. /// @@ -84,8 +85,8 @@ class EditorApi { } Future _execCommand(String command) async { - await _webViewController - .evaluateJavascript('document.execCommand($command);'); + await _webViewController.evaluateJavascript( + source: 'document.execCommand($command);'); // document.getElementById("editor").focus(); // FocusScope.of(context).unfocus(); // Timer(const Duration(milliseconds: 1), () { @@ -97,16 +98,9 @@ class EditorApi { /// /// Compare [getFullHtml()] to the complete HTML document's text. Future getText() async { - var rawHtml = await _webViewController - .evaluateJavascript('document.getElementById("editor").innerHTML;'); - if (rawHtml.startsWith('"')) { - rawHtml = rawHtml.substring(1, rawHtml.length - 1).trim(); - } - rawHtml = rawHtml.replaceAll(r'\n', '\n'); - rawHtml = rawHtml.replaceAll(r'\"', '"'); - rawHtml = rawHtml.replaceAll(r'\\', r'\'); - rawHtml = rawHtml.replaceAll(r'\u003C', '<'); - return rawHtml; + final innerHtml = await _webViewController.evaluateJavascript( + source: 'document.getElementById("editor").innerHTML;') as String; + return innerHtml; } /// Retrieves the edited text within a complete HTML document. diff --git a/pubspec.lock b/pubspec.lock index 0943594..b60667b 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -55,6 +55,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_inappwebview: + dependency: "direct main" + description: + name: flutter_inappwebview + url: "https://pub.dartlang.org" + source: hosted + version: "5.1.0+4" flutter_test: dependency: "direct dev" description: flutter @@ -142,13 +149,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" - webview_flutter: - dependency: "direct main" - description: - name: webview_flutter - url: "https://pub.dartlang.org" - source: hosted - version: "1.0.7" sdks: dart: ">=2.12.0-0.0 <3.0.0" - flutter: ">=1.22.0" + flutter: ">=1.22.2" diff --git a/pubspec.yaml b/pubspec.yaml index 7b402b1..ff2f5f4 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,7 +10,7 @@ environment: dependencies: flutter: sdk: flutter - webview_flutter: ^1.0.7 + flutter_inappwebview: ^5.1.0+4 dev_dependencies: flutter_test: diff --git a/test/enough_html_editor_test.dart b/test/enough_html_editor_test.dart index 55d4648..6446497 100644 --- a/test/enough_html_editor_test.dart +++ b/test/enough_html_editor_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:enough_html_editor/enough_html_editor.dart'; +// import 'package:enough_html_editor/enough_html_editor.dart'; void main() { test('adds one to input values', () { diff --git a/test/playground.html b/test/playground.html index 2884873..8a01cc1 100644 --- a/test/playground.html +++ b/test/playground.html @@ -36,7 +36,7 @@ var isSelectionBold = false; var isSelectionItalic = false; var isSelectionUnderline = false; - var isUserInput = false; + var isLineBreakInput = false; var selectionTextAlign = undefined; var documentHeight; @@ -49,9 +49,10 @@ var isBold = false; var isItalic = false; var isUnderline = false; - var nestedBlockqotes = 0; var node = anchorNode; var textAlign = undefined; + var nestedBlockqotes = 0; + var rootBlockquote; while (node.parentNode != null && node.id != 'editor') { if (node.nodeName == 'B') { isBold = true; @@ -61,6 +62,7 @@ isUnderline = true; } else if (node.nodeName == 'BLOCKQUOTE') { nestedBlockqotes++; + rootBlockquote = node; } textAlign ??= node.style?.textAlign; if (textAlign == '') { @@ -77,7 +79,7 @@ isSelectionBold = isBold; isSelectionItalic = isItalic; isSelectionUnderline = isUnderline; - console.log('bold=', isBold, ', italic=', isItalic, ', underline=', isUnderline); + // console.log('bold=', isBold, ', italic=', isItalic, ', underline=', isUnderline); var message = 0; if (isBold) { message += 1; @@ -89,21 +91,78 @@ message += 4; } - } else if (isUserInput && nestedBlockqotes > 0 && anchorOffset == focusOffset) { - // check if a linebreak has been added in a blockquote - console.log('EDIT IN BLOCKQUOTE!!'); - document.execCommand('insertHTML', false, 'YOUR TEXT HERE'); + } else if (isLineBreakInput && nestedBlockqotes > 0 && anchorOffset == focusOffset) { + //console.log('rootBlockquote', rootBlockquote); + // console.log('anchor.nodeName', anchorNode.nodeName, 'offset', anchorOffset); + let rootNode = rootBlockquote.parentNode; + // var level = 1; + var cloneNode = null; + var requiresCloning = false; + var node = anchorNode; + while (node != rootBlockquote) { + // console.log(level, node); + // console.log(level, 'nodeName', node.nodeName); + // console.log(level, 'prevSibling', node.previousSibling); + // console.log(level, 'nextSibling', node.nextSibling); + let sibling = node.previousSibling; + if (sibling != null) { + // move all siblings 'above' the current node to a clone: + var parentNode = node.parentNode; + var currentSibling = sibling; + while (currentSibling.previousSibling != null) { + currentSibling = currentSibling.previousSibling; + } + var cloneParentNode = document.createElement(parentNode.nodeName); + do { + var nextSibling = currentSibling.nextSibling; + // console.log(level, 'move sibling', currentSibling); + parentNode.removeChild(currentSibling); + cloneParentNode.appendChild(currentSibling); + if (currentSibling == sibling) { + break; + } + currentSibling = nextSibling; + } while (true); + if (cloneNode != null) { + cloneParentNode.appendChild(cloneNode); + } + requiresCloning = true; + cloneNode = cloneParentNode; + } else if (requiresCloning) { + // consolone(level, 'create articicial clone ' + node.nodeName + ' for', cloneNode); + var cloneParentNode = document.createElement(node.nodeName); + cloneParentNode.appendChild(cloneNode); + cloneNode = cloneParentNode; + } + node = node.parentNode; + // level++; + } + if (cloneNode != null) { + rootNode.insertBefore(cloneNode, rootBlockquote); + + } + let textNode = document.createElement("P"); + let textNodeContent = document.createTextNode('_'); + textNode.appendChild(textNodeContent); + rootNode.insertBefore(textNode, rootBlockquote); + let range = new Range(); + range.setStart(textNodeContent, 0); + range.setEnd(textNodeContent, 1); + let selection = getSelection(); + selection.removeAllRanges(); + selection.addRange(range); + } - isUserInput = false; + isLineBreakInput = false; // if (anchorOffset == focusOffset) { // console.log(anchorNode, anchorOffset); //, focusNode, focusOffset); // } } - function onInput() { - isUserInput = true; - console.log('onInput'); + function onInput(inputEvent) { + //console.log('inputEvent', inputEvent); + isLineBreakInput = ((inputEvent.inputType == 'insertParagraph') || ((inputEvent.inputType == 'insertText') && (inputEvent.data == null))); var height = document.body.scrollHeight; if (height != documentHeight) { documentHeight = height; @@ -121,19 +180,24 @@ function onLoaded() { documentHeight = document.body.scrollHeight; + document.getElementById('editor').oninput = onInput; + document.onselectionchange = onSelectionChange; } - document.onselectionchange = onSelectionChange; - + - + Here is some text outside of the editor. -
Hello ContentEditable +
Hello ContentEditable

This is a level 2 heading

Here is some text

Here is bold text

@@ -141,12 +205,31 @@

This is a level 2 heading

Here is bold and italic text

Here is bold and italic and underline text

  • one list element
  • another point
-
Here is a quote
+

Here is unquoted text...

+

Nested Quote

+
Nested quote: + San Francisco, officially the City and County of San Francisco, is a cultural, commercial, and financial center of Northern California. +
San Francisco is the 16th most populous city in the United States, and the fourth most populous in California, 881,549 residents as of 2019. +
It covers an area of about 46.89 square miles (121.4 km2), mostly at the north end of the San Francisco Peninsula in the San Francisco Bay Area, making it the second most densely populated large U.S. city, and the fifth most densely populated U.S. county, behind only four of the five New York City boroughs. +
+ San Francisco is part of the 12th-largest metropolitan statistical area in the United States by population, with 4.7 million people, and the fourth-largest by economic output, with GDP of $592 billion in 2019. With San Jose, it forms the fifth most populous combined statistical area in the United States, with 9.67 million residents as of 2019. Colloquial nicknames for San Francisco include The City, SF, Frisco and San Fran. +
+
+

Nested Complex Quote

+
Nested complex
that spans several lines
A second level blockqote
+

Simple Quote

+
Simple quote: + San Francisco, officially the City and County of San Francisco, is a cultural, commercial, and financial center of Northern California. San Francisco is the 16th most populous city in the United States, and the fourth most populous in California, with 881,549 residents as of 2019. It covers an area of about 46.89 square miles (121.4 km2), mostly at the north end of the San Francisco Peninsula in the San Francisco Bay Area, making it the second most densely populated large U.S. city, and the fifth most densely populated U.S. county, behind only four of the five New York City boroughs. San Francisco is part of the 12th-largest metropolitan statistical area in the United States by population, with 4.7 million people, and the fourth-largest by economic output, with GDP of $592 billion in 2019. With San Jose, it forms the fifth most populous combined statistical area in the United States, with 9.67 million residents as of 2019. Colloquial nicknames for San Francisco include The City, SF, Frisco and San Fran. +
+

Complex Quote

+
Complex quote: + San Francisco, officially the City and County of San Francisco, is a cultural,
commercial,
and financial
center of Northern California. San Francisco is the 16th most populous city in the United States, and the fourth most populous in California, with 881,549 residents as of 2019. It covers an area of about 46.89 square miles (121.4 km2), mostly at the north end of the San Francisco Peninsula in the San Francisco Bay Area, making it the second most densely populated large U.S. city, and the fifth most densely populated U.S. county, behind only four of the five New York City boroughs. San Francisco is part of the 12th-largest metropolitan statistical area in the United States by population, with 4.7 million people, and the fourth-largest by economic output, with GDP of $592 billion in 2019. With San Jose, it forms the fifth most populous combined statistical area in the United States, with 9.67 million residents as of 2019. Colloquial nicknames for San Francisco include The City, SF, Frisco and San Fran. +

And finally, some code

                 Hello World