├── example ├── test │ └── widget_test.dart ├── ios │ ├── Flutter │ │ ├── Debug.xcconfig │ │ ├── Release.xcconfig │ │ └── AppFrameworkInfo.plist │ ├── Runner │ │ ├── Runner-Bridging-Header.h │ │ ├── Assets.xcassets │ │ │ ├── LaunchImage.imageset │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ ├── README.md │ │ │ │ └── Contents.json │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-83.5x83.5@2x.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Base.lproj │ │ │ ├── Main.storyboard │ │ │ └── LaunchScreen.storyboard │ │ └── Info.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ │ └── IDEWorkspaceChecks.plist │ │ ├── xcshareddata │ │ │ └── xcschemes │ │ │ │ └── Runner.xcscheme │ │ └── project.pbxproj │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── WorkspaceSettings.xcsettings │ │ │ └── IDEWorkspaceChecks.plist │ └── .gitignore ├── macos │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Runner │ │ ├── Configs │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ ├── Warnings.xcconfig │ │ │ └── AppInfo.xcconfig │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ ├── app_icon_64.png │ │ │ │ └── Contents.json │ │ ├── AppDelegate.swift │ │ ├── Release.entitlements │ │ ├── DebugProfile.entitlements │ │ ├── MainFlutterWindow.swift │ │ ├── Info.plist │ │ └── Base.lproj │ │ │ └── MainMenu.xib │ ├── .gitignore │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner.xcodeproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ └── RunnerTests │ │ └── RunnerTests.swift ├── android │ ├── gradle.properties │ ├── app │ │ ├── src │ │ │ ├── main │ │ │ │ ├── res │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ ├── drawable │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ ├── values │ │ │ │ │ │ └── styles.xml │ │ │ │ │ └── values-night │ │ │ │ │ │ └── styles.xml │ │ │ │ ├── kotlin │ │ │ │ │ └── com │ │ │ │ │ │ └── example │ │ │ │ │ │ └── example │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ └── AndroidManifest.xml │ │ │ ├── debug │ │ │ │ └── AndroidManifest.xml │ │ │ └── profile │ │ │ │ └── AndroidManifest.xml │ │ └── build.gradle │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ ├── .gitignore │ ├── settings.gradle │ └── build.gradle ├── lib │ ├── main.dart │ └── src │ │ └── ui │ │ ├── example_app │ │ └── example_app.dart │ │ └── home_page │ │ ├── custom │ │ └── metadata_button.dart │ │ └── home_page.dart ├── pubspec.yaml ├── README.md ├── .gitignore ├── .metadata ├── analysis_options.yaml └── pubspec.lock ├── lib ├── src │ ├── utils │ │ ├── function │ │ │ ├── extensions │ │ │ │ ├── color_extension.dart │ │ │ │ ├── nullable_string_extension.dart │ │ │ │ ├── string_extension.dart │ │ │ │ ├── extensions.dart │ │ │ │ ├── iterable_extension.dart │ │ │ │ ├── text_editor_extensions.dart │ │ │ │ └── list_extension.dart │ │ │ ├── utility_functions_barrel.dart │ │ │ └── util_functions │ │ │ │ └── util_functions.dart │ │ └── utils_barrel.dart │ └── text_editor │ │ ├── models │ │ ├── text_metadata │ │ │ ├── text_metadata_enum.dart │ │ │ ├── text_decoration_enum.dart │ │ │ └── text_metadata.dart │ │ ├── text_editor_models_barrel.dart │ │ ├── text_deltas │ │ │ ├── text_deltas_utils.dart │ │ │ └── text_deltas.dart │ │ └── text_delta │ │ │ └── text_delta.dart │ │ ├── text_editor_barrel.dart │ │ ├── controller │ │ ├── text_editor_controller_public.dart │ │ └── text_editor_controller.dart │ │ └── ui │ │ └── widgets │ │ ├── rich_text_field.dart │ │ └── rich_text_form_field.dart └── rich_text_editor_controller.dart ├── .metadata ├── analysis_options.yaml ├── ROADMAP.md ├── test ├── src │ ├── utils │ │ └── function │ │ │ ├── extensions │ │ │ ├── color_extension_test.dart │ │ │ ├── string_extension_test.dart │ │ │ ├── iterable_extension_test.dart │ │ │ ├── nullable_string_extension_test.dart │ │ │ ├── list_extension_test.dart │ │ │ └── text_editor_extensions_test.dart │ │ │ └── util_functions │ │ │ └── util_functions_test.dart │ └── text_editor │ │ └── ui │ │ └── widgets │ │ └── rich_text_field_test.dart └── rich_text_editor_controller_test.dart ├── .gitignore ├── pubspec.yaml ├── CHANGELOG.md ├── LICENSE ├── CONTRIBUTION_GUIDE.md ├── .github └── workflows │ └── ci.yaml ├── README.md └── coverage └── lcov.info /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "ephemeral/Flutter-Generated.xcconfig" 2 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/color_extension.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension ColorExtension on Color { 4 | String get toSerializerString => value.toString(); 5 | } 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rich_text_editor_controller_example/src/ui/example_app/example_app.dart'; 3 | 4 | void main() => runApp(const ExampleApp()); 5 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/nullable_string_extension.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension NullableStringExtension on String? { 4 | bool get isNullOrEmpty => this?.isEmpty ?? true; 5 | } 6 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/folaoluwafemi/rich_text_editor_controller/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /lib/src/utils/utils_barrel.dart: -------------------------------------------------------------------------------- 1 | export 'package:rich_text_editor_controller/src/utils/function/util_functions/util_functions.dart'; 2 | export 'package:rich_text_editor_controller/src/utils/function/utility_functions_barrel.dart'; 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | 9 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 10 | } 11 | -------------------------------------------------------------------------------- /lib/src/utils/function/utility_functions_barrel.dart: -------------------------------------------------------------------------------- 1 | export 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 2 | export 'package:rich_text_editor_controller/src/utils/function/util_functions/util_functions.dart'; 3 | -------------------------------------------------------------------------------- /lib/src/utils/function/util_functions/util_functions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | abstract class UtilFunctions { 4 | static Color colorFromMap(dynamic map) { 5 | return Color(int.parse(map['color'].toString())); 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/string_extension.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension StringExtension on String { 4 | String removeAll(String pattern) { 5 | return replaceAll(pattern, ''); 6 | } 7 | 8 | List get chars => split(''); 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @NSApplicationMain 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_metadata/text_metadata_enum.dart: -------------------------------------------------------------------------------- 1 | /// Enum for text metadata change 2 | /// 3 | /// It is used to specify which metadata field has changed 4 | enum TextMetadataChange { 5 | all, 6 | color, 7 | fontWeight, 8 | fontStyle, 9 | fontSize, 10 | alignment, 11 | fontDecoration, 12 | fontFeatures; 13 | } 14 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/extensions.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | part 'color_extension.dart'; 4 | 5 | part 'iterable_extension.dart'; 6 | 7 | part 'list_extension.dart'; 8 | 9 | part 'nullable_string_extension.dart'; 10 | 11 | part 'string_extension.dart'; 12 | 13 | part 'text_editor_extensions.dart'; 14 | -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: b06b8b2710955028a6b562f5aa6fe62941d6febf 8 | channel: stable 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/iterable_extension.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension IterableExtension on Iterable { 4 | bool containsWhere(bool Function(E value) test) { 5 | for (E element in this) { 6 | bool satisfied = test(element); 7 | if (satisfied) { 8 | return true; 9 | } 10 | } 11 | return false; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /lib/rich_text_editor_controller.dart: -------------------------------------------------------------------------------- 1 | library rich_text_editor_controller; 2 | 3 | export 'package:rich_text_editor_controller/src/text_editor/text_editor_barrel.dart' 4 | show 5 | TextMetadata, 6 | TextDeltas, 7 | TextDelta, 8 | TextDeltasExtension, 9 | RichTextEditorController, 10 | TextDecorationEnum, 11 | RichTextFormField, 12 | RichTextField; 13 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /lib/src/text_editor/text_editor_barrel.dart: -------------------------------------------------------------------------------- 1 | export 'package:rich_text_editor_controller/src/text_editor/controller/text_editor_controller_public.dart'; 2 | export 'package:rich_text_editor_controller/src/text_editor/models/text_editor_models_barrel.dart'; 3 | export 'package:rich_text_editor_controller/src/text_editor/ui/widgets/rich_text_field.dart'; 4 | export 'package:rich_text_editor_controller/src/text_editor/ui/widgets/rich_text_form_field.dart'; 5 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_editor_models_barrel.dart: -------------------------------------------------------------------------------- 1 | export 'package:rich_text_editor_controller/src/text_editor/models/text_delta/text_delta.dart'; 2 | export 'package:rich_text_editor_controller/src/text_editor/models/text_deltas/text_deltas.dart'; 3 | export 'package:rich_text_editor_controller/src/text_editor/models/text_metadata/text_metadata.dart'; 4 | export 'package:rich_text_editor_controller/src/text_editor/models/text_metadata/text_metadata_enum.dart'; 5 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | # Additional information about this file can be found at 4 | # https://dart.dev/guides/language/analysis-options 5 | linter: 6 | rules: 7 | always_declare_return_types: true 8 | avoid_print: true 9 | always_use_package_imports: true 10 | prefer_relative_imports: false 11 | no_logic_in_create_state: true 12 | require_trailing_commas: true 13 | prefer_single_quotes: true -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /ROADMAP.md: -------------------------------------------------------------------------------- 1 | # RichTextEditorController Roadmap 2 | 3 | Below are some list of features/behaviour that will/should be supported with their status. 4 |
Note that there's no specific timeline to it's development and also there's no specific assignee, so feel free to 5 | contribute to any of it's implementation. [click here for the guide to roadmap contribution](CONTRIBUTION_GUIDE.md) 6 | 7 | - [ ] Add TextDirectionality support 8 | - Tag: ```directionality_support``` 9 | - Status: ```pending``` -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @UIApplicationMain 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rich_text_editor_controller_example 2 | description: A new Flutter project. 3 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 4 | 5 | version: 1.0.0+1 6 | 7 | environment: 8 | sdk: '>=2.19.0 <3.0.0' 9 | 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | cupertino_icons: ^1.0.2 15 | rich_text_editor_controller: 16 | path: ../ 17 | 18 | dev_dependencies: 19 | flutter_test: 20 | sdk: flutter 21 | 22 | flutter_lints: ^2.0.0 23 | 24 | flutter: 25 | 26 | uses-material-design: true -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/text_editor_extensions.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension TextAlignExtension on TextAlign { 4 | Alignment get toAlignment { 5 | switch (this) { 6 | case TextAlign.start: 7 | case TextAlign.left: 8 | return Alignment.centerLeft; 9 | case TextAlign.end: 10 | case TextAlign.right: 11 | return Alignment.centerRight; 12 | case TextAlign.center: 13 | return Alignment.center; 14 | case TextAlign.justify: 15 | return Alignment.center; 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/color_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | 4 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 5 | 6 | void main() { 7 | group('ColorExtension', () { 8 | group('toSerializerString', () { 9 | test( 10 | 'returns Serialized String value of color', 11 | () async { 12 | expect(const Color(0xFF000000).toSerializerString, '4278190080'); 13 | }, 14 | ); 15 | }); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | .vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2024 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_deltas/text_deltas_utils.dart: -------------------------------------------------------------------------------- 1 | part of 'text_deltas.dart'; 2 | 3 | abstract class TextDeltasUtils { 4 | static TextDeltas deltasFromString( 5 | String string, [ 6 | TextMetadata? metadata, 7 | ]) { 8 | final TextDeltas deltas = []; 9 | final List chars = string.chars; 10 | 11 | for (final String char in chars) { 12 | deltas.add(TextDelta(char: char, metadata: metadata)); 13 | } 14 | return deltas; 15 | } 16 | 17 | static TextDeltas deltasFromList(List list) { 18 | final TextDeltas deltas = []; 19 | for (dynamic map in list) { 20 | deltas.add(TextDelta.fromMap((map as Map).cast())); 21 | } 22 | return deltas; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.2.0' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /example/lib/src/ui/example_app/example_app.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:rich_text_editor_controller_example/src/ui/home_page/home_page.dart'; 3 | 4 | class ExampleApp extends StatelessWidget { 5 | const ExampleApp({super.key}); 6 | 7 | @override 8 | Widget build(BuildContext context) { 9 | return MaterialApp( 10 | title: 'Rich Text Editor Controller Demo', 11 | theme: ThemeData.dark().copyWith( 12 | primaryColor: Colors.deepOrange, 13 | colorScheme: const ColorScheme.dark( 14 | primary: Colors.deepOrange, 15 | onPrimary: Colors.white, 16 | onBackground: Colors.white, 17 | ), 18 | ), 19 | home: const HomePage(), 20 | ); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: rich_text_editor_controller 2 | description: 3 | A very lightweight package that allows rich text editing as well as providing a simple API for data serialization 4 | version: 0.0.5 5 | homepage: https://github.com/folaoluwafemi/rich_text_editor_controller 6 | repository: https://github.com/folaoluwafemi/rich_text_editor_controller 7 | 8 | environment: 9 | sdk: '>=2.19.0 <4.0.0' 10 | flutter: ">=1.17.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | 16 | dev_dependencies: 17 | flutter_test: 18 | sdk: flutter 19 | flutter_lints: ^2.0.0 20 | 21 | # For information on the generic Dart part of this file, see the 22 | # following page: https://dart.dev/tools/pub/pubspec 23 | 24 | flutter: 25 | uses-material-design: true 26 | 27 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_deltas/text_deltas.dart: -------------------------------------------------------------------------------- 1 | import 'package:rich_text_editor_controller/src/text_editor/models/text_editor_models_barrel.dart'; 2 | import 'package:rich_text_editor_controller/src/utils/utils_barrel.dart'; 3 | 4 | part 'text_deltas_utils.dart'; 5 | 6 | /// Sugar for working with list of [TextDelta]s. 7 | typedef TextDeltas = List; 8 | 9 | extension TextDeltasExtension on TextDeltas { 10 | String get text { 11 | if (isEmpty) return ''; 12 | final StringBuffer stringBuffer = StringBuffer(first.char); 13 | for (int i = 1; i < length; i++) { 14 | stringBuffer.write(this[i].char); 15 | } 16 | return stringBuffer.toString(); 17 | } 18 | 19 | ///creates a value copy of the list 20 | TextDeltas get copy => List.from(this); 21 | } 22 | -------------------------------------------------------------------------------- /test/src/utils/function/util_functions/util_functions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:rich_text_editor_controller/src/utils/function/util_functions/util_functions.dart'; 4 | 5 | void main() { 6 | test('colorFromMap returns a Color object from a valid map', () { 7 | final Map map = {'color': '0xFF0000'}; 8 | final Color color = UtilFunctions.colorFromMap(map); 9 | expect(color, isInstanceOf()); 10 | expect(color.value, equals(0xFF0000)); 11 | }); 12 | 13 | test('colorFromMap throws an exception when the map is invalid', () { 14 | final Map map = {'color': 'invalid_value'}; 15 | expect(() => UtilFunctions.colorFromMap(map), throwsFormatException); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.0.1+1 2 | 3 | * Initial release: lacks documentation and is not very stable yet, I do not recommend you use in production 4 | 5 | ## 0.0.1 6 | * Added better documentation 7 | * Add example code 8 | * Add TextDecoration to exports 9 | * Add RichTextFormField 10 | * Add more methods to manipulate metadata 11 | 12 | 13 | ## 0.0.2 14 | * reformat example file 15 | 16 | 17 | ## 0.0.3 18 | * update example project 19 | * update readme 20 | * add tests and coverage report for project 21 | * remove redundancies in project 22 | 23 | ## 0.0.4 24 | * add more tests 25 | * Added `metadata` to controller constructor 26 | * added `metadata` or default metadata (if `metadata` is null) to delta initialization in `RichTextEditorController` constructor 27 | 28 | ## 0.0.5 29 | * Implement list/bullet point 30 | * fix bugs -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_metadata/text_decoration_enum.dart: -------------------------------------------------------------------------------- 1 | part of 'text_metadata.dart'; 2 | 3 | ///This super enum is used to convert [TextDecoration] to [TextDecorationEnum] and vice versa. 4 | /// 5 | /// It is being used to allow for easy data serialization. since [TextDecoration] does not expose toJson() and fromJson() methods. 6 | enum TextDecorationEnum { 7 | none(TextDecoration.none, 'none'), 8 | underline(TextDecoration.underline, 'underline'), 9 | strikeThrough(TextDecoration.lineThrough, 'line-through'), 10 | ; 11 | 12 | final TextDecoration value; 13 | final String cssValue; 14 | 15 | const TextDecorationEnum(this.value, this.cssValue); 16 | 17 | factory TextDecorationEnum.fromDecoration(TextDecoration decoration) { 18 | return values.firstWhere((element) => element.value == decoration); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/string_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 3 | 4 | void main() { 5 | group('StringExtension', () { 6 | group('removeAll', () { 7 | test( 8 | 'returns new string with input character removed', 9 | () async { 10 | const String name = 'Th00is i0s an0 e00xamp0l0e'; 11 | 12 | expect(name.removeAll('0'), 'This is an example'); 13 | }, 14 | ); 15 | }); 16 | 17 | group('chars', () { 18 | test( 19 | 'returns a new list of characters in the string', 20 | () async { 21 | const String name = 'jamiu'; 22 | 23 | expect(name.chars, ['j', 'a', 'm', 'i', 'u']); 24 | }, 25 | ); 26 | }); 27 | }); 28 | } 29 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Symbolication related 36 | app.*.symbols 37 | 38 | # Obfuscation related 39 | app.*.map.json 40 | 41 | # Android Studio will place build artifacts here 42 | /android/app/debug 43 | /android/app/profile 44 | /android/app/release 45 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 11.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/utils/function/extensions/list_extension.dart: -------------------------------------------------------------------------------- 1 | part of 'extensions.dart'; 2 | 3 | extension ListExtension on List { 4 | E? get firstOrNull { 5 | try { 6 | return first; 7 | } catch (e) { 8 | return null; 9 | } 10 | } 11 | 12 | E? get lastOrNull { 13 | try { 14 | return last; 15 | } catch (e) { 16 | return null; 17 | } 18 | } 19 | 20 | E? elementAtOrNull(int index) { 21 | try { 22 | return this[index]; 23 | } catch (e) { 24 | return null; 25 | } 26 | } 27 | void addItemBetweenList( 28 | int index, { 29 | required E item, 30 | }) { 31 | if (index > length) throw Exception('Index out of bounds'); 32 | if (index == length) return add(item); 33 | 34 | final List completeList = [ 35 | ...sublist(0, index), 36 | item, 37 | ...sublist(index, length), 38 | ]; 39 | 40 | clear(); 41 | addAll(completeList); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/iterable_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 3 | 4 | void main() { 5 | group('IterableExtension', () { 6 | final Iterable items = [1, 2, 3, 4, 5, 6]; 7 | 8 | group('containsWhere', () { 9 | test( 10 | 'returns true if iterable contains test item', 11 | () async { 12 | const int testItem = 1; 13 | 14 | final bool result = items.containsWhere((item) => item == testItem); 15 | 16 | expect(result, true); 17 | }, 18 | ); 19 | 20 | test( 21 | 'returns false if iterable does not contain test item', 22 | () async { 23 | const int testItem = 8; 24 | 25 | final bool result = items.containsWhere((item) => item == testItem); 26 | 27 | expect(result, false); 28 | }, 29 | ); 30 | }); 31 | }); 32 | } 33 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/nullable_string_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 3 | 4 | void main() { 5 | group('NullableStringExtension', () { 6 | group('isNullOrEmpty', () { 7 | test( 8 | 'returns true if string is empty', 9 | () async { 10 | const String testItem = ''; 11 | 12 | expect(testItem.isNullOrEmpty, true); 13 | }, 14 | ); 15 | 16 | test( 17 | 'returns true if string is null', 18 | () async { 19 | String? testItem; 20 | 21 | expect(testItem.isNullOrEmpty, true); 22 | }, 23 | ); 24 | 25 | test( 26 | 'returns false if string is not null or empty', 27 | () async { 28 | const String testItem = '@developerjamiu'; 29 | 30 | expect(testItem.isNullOrEmpty, false); 31 | }, 32 | ); 33 | }); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2023 Fola Oluwafemi 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 8 | channel: stable 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 17 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 18 | - platform: macos 19 | create_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 20 | base_revision: 9cd3d0d9ff05768afa249e036acc66e8abe93bff 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/list_extension_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 3 | 4 | void main() { 5 | group('ListExtension', () { 6 | group('firstOrNull', () { 7 | test( 8 | 'returns first item if list is not empty', 9 | () async { 10 | final List list = [1, 2, 3, 4, 5, 6]; 11 | 12 | expect(list.firstOrNull, 1); 13 | }, 14 | ); 15 | 16 | test( 17 | 'returns null if list is empty', 18 | () async { 19 | final List list = []; 20 | 21 | expect(list.firstOrNull, null); 22 | }, 23 | ); 24 | }); 25 | 26 | group('lastOrNull', () { 27 | test( 28 | 'returns first item if list is not empty', 29 | () async { 30 | final List list = [1, 2, 3, 4, 5, 6]; 31 | 32 | expect(list.lastOrNull, 6); 33 | }, 34 | ); 35 | 36 | test( 37 | 'returns null if list is empty', 38 | () async { 39 | final List list = []; 40 | 41 | expect(list.lastOrNull, null); 42 | }, 43 | ); 44 | }); 45 | }); 46 | } 47 | -------------------------------------------------------------------------------- /CONTRIBUTION_GUIDE.md: -------------------------------------------------------------------------------- 1 | 2 | # Guide to contribution 3 | 4 | ## General 5 | ### Issues 6 | - Give a short title to your issue in the title section 7 | - Add a tag at the beginning of your comment with the format: "[ {Tag} ] ...{comment continuation}". below are the possible tag: 8 | - `feature request`, `error`, `bug` 9 | - Make your comment as descriptive as possible. 10 | 11 | ### Pull Request 12 | - PRs should follow the similar guide to issues 13 | - Add a tag at the beginning of your comment with the format: "[ {Tag} ] ...{comment continuation}". below are the possible tag: 14 | - `feature request`, `error`, `bug` 15 | - If your PR was made to fix an issue, reference the issue in your pr title like so: "{your pr title} | Issue {issue number}" 16 | 17 | ### Roadmap 18 | If you wish to contribute to the roadmap, fork this [repo](https://github.com/folaoluwafemi/rich_text_editor_controller) at branch ```{ROADMAP-TAG}``` if the branch does not exist, feel free to create one in your fork. 19 | 20 | ## Code conventions 21 | - All variables MUST be strongly typed except in instances where it is obvious what the variable type will be by it's assigned value. [see here](https://dart.dev/guides/language/effective-dart/design#dont-redundantly-type-annotate-initialized-local-variables) for clarity 22 | - All code MUST pass static analysis. 23 | - DO NOT `ignore` any lint rule. 24 | -------------------------------------------------------------------------------- /test/src/text_editor/ui/widgets/rich_text_field_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:rich_text_editor_controller/rich_text_editor_controller.dart'; 4 | 5 | void main() { 6 | testWidgets('RichTextField should use the text alignment from the controller', 7 | (WidgetTester tester) async { 8 | final RichTextEditorController controller = RichTextEditorController(); 9 | final RichTextField richTextField = RichTextField( 10 | controller: controller, 11 | textAlign: TextAlign.right, 12 | ); 13 | controller.changeAlignment(TextAlign.center); 14 | 15 | await tester.pumpWidget(MaterialApp(home: Scaffold(body: richTextField))); 16 | 17 | // Verify that the text alignment is initially set to the center. 18 | expect(find.byType(TextField), findsOneWidget); 19 | final TextField textFieldWidget = tester.widget( 20 | find.byType(TextField), 21 | ); 22 | expect(textFieldWidget.textAlign, TextAlign.center); 23 | 24 | // Update the controller's text alignment. 25 | controller.metadata = const TextMetadata(alignment: TextAlign.right); 26 | await tester.pump(); 27 | 28 | // Verify that the text alignment is now set to the right. 29 | expect(find.byType(TextField), findsOneWidget); 30 | final TextField updatedTextFieldWidget = tester.widget( 31 | find.byType(TextField), 32 | ); 33 | expect(updatedTextFieldWidget.textAlign, TextAlign.right); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_delta/text_delta.dart: -------------------------------------------------------------------------------- 1 | import 'package:rich_text_editor_controller/src/text_editor/models/text_editor_models_barrel.dart'; 2 | 3 | /// Class that holds the style change for each character. 4 | class TextDelta { 5 | final String char; 6 | final TextMetadata? metadata; 7 | 8 | const TextDelta({ 9 | required this.char, 10 | this.metadata, 11 | }); 12 | 13 | TextDelta copyWith({ 14 | String? char, 15 | TextMetadata? metadata, 16 | }) { 17 | return TextDelta( 18 | char: char ?? this.char, 19 | metadata: metadata ?? this.metadata, 20 | ); 21 | } 22 | 23 | @override 24 | String toString() { 25 | return ''' 26 | TextDelta( 27 | char: $char 28 | metadata: $metadata 29 | )'''; 30 | } 31 | 32 | Map toMap() { 33 | return { 34 | 'char': char, 35 | 'metadata': metadata?.toMap(), 36 | }; 37 | } 38 | 39 | factory TextDelta.fromMap(Map map) { 40 | return TextDelta( 41 | char: map['char'] as String, 42 | metadata: map['metadata'] == null 43 | ? null 44 | : TextMetadata.fromMap( 45 | (map['metadata'] as Map).cast(), 46 | ), 47 | ); 48 | } 49 | 50 | TextDelta copyWithChar(String char) { 51 | return TextDelta( 52 | char: char, 53 | metadata: metadata, 54 | ); 55 | } 56 | 57 | @override 58 | bool operator ==(Object other) => 59 | identical(this, other) || 60 | other is TextDelta && 61 | runtimeType == other.runtimeType && 62 | char == other.char && 63 | metadata == other.metadata; 64 | 65 | @override 66 | int get hashCode => char.hashCode ^ metadata.hashCode; 67 | } 68 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/rich_text_editor_controller_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter_test/flutter_test.dart'; 2 | import 'package:rich_text_editor_controller/rich_text_editor_controller.dart'; 3 | 4 | void main() { 5 | final List someDeltas = [ 6 | const TextDelta( 7 | char: 'h', 8 | metadata: RichTextEditorController.defaultMetadata, 9 | ), 10 | const TextDelta( 11 | char: 'e', 12 | metadata: RichTextEditorController.defaultMetadata, 13 | ), 14 | ]; 15 | group( 16 | 'rich_text_editor_controller_test', 17 | () { 18 | test('constructing an instance of rich text editor controller works fine', 19 | () async { 20 | expect(() => RichTextEditorController(), returnsNormally); 21 | }); 22 | 23 | test( 24 | '[text] is the same as passed into constructor', 25 | () { 26 | final controller = RichTextEditorController(text: 'hello'); 27 | expect(controller.text, equals('hello')); 28 | }, 29 | ); 30 | 31 | test( 32 | '[metadata] is the same as passed into constructor', 33 | () { 34 | final controller = RichTextEditorController( 35 | metadata: const TextMetadata(), 36 | ); 37 | expect(controller.metadata, equals(const TextMetadata())); 38 | }, 39 | ); 40 | 41 | test( 42 | '[delta] is the same as passed into constructor', 43 | () { 44 | final controller = RichTextEditorController(deltas: someDeltas); 45 | expect(controller.deltas, equals(someDeltas)); 46 | }, 47 | ); 48 | test( 49 | '[delta] is gotten from passed in [text] correctly', 50 | () { 51 | final controller = RichTextEditorController(text: 'he'); 52 | expect(controller.deltas, equals(someDeltas)); 53 | }, 54 | ); 55 | }, 56 | ); 57 | } 58 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | UIViewControllerBasedStatusBarAppearance 45 | 46 | CADisableMinimumFrameDurationOnPhone 47 | 48 | UIApplicationSupportsIndirectInputEvents 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /example/lib/src/ui/home_page/custom/metadata_button.dart: -------------------------------------------------------------------------------- 1 | part of '../home_page.dart'; 2 | 3 | enum MetadataValue { 4 | color, 5 | bold, 6 | bulletPoints, 7 | italic, 8 | alignRight, 9 | alignCenter, 10 | alignLeft, 11 | underline, 12 | superscript, 13 | subscript, 14 | } 15 | 16 | class MetadataButton extends StatelessWidget { 17 | final TextMetadata metadata; 18 | final MetadataValue value; 19 | final ValueChanged onPressed; 20 | 21 | const MetadataButton({ 22 | Key? key, 23 | required this.metadata, 24 | required this.value, 25 | required this.onPressed, 26 | }) : super(key: key); 27 | 28 | void _onPressed() => onPressed(value); 29 | 30 | @override 31 | Widget build(BuildContext context) { 32 | late final bool selected; 33 | switch (value) { 34 | case MetadataValue.bold: 35 | selected = metadata.fontWeight == FontWeight.w700; 36 | break; 37 | case MetadataValue.italic: 38 | selected = metadata.fontStyle == FontStyle.italic; 39 | break; 40 | case MetadataValue.alignRight: 41 | selected = metadata.alignment == TextAlign.right; 42 | break; 43 | case MetadataValue.alignCenter: 44 | selected = metadata.alignment == TextAlign.center; 45 | break; 46 | case MetadataValue.alignLeft: 47 | selected = metadata.alignment == TextAlign.left; 48 | break; 49 | case MetadataValue.underline: 50 | selected = metadata.decoration == TextDecorationEnum.underline; 51 | break; 52 | default: 53 | selected = false; 54 | } 55 | 56 | return MaterialButton( 57 | color: selected ? Colors.deepOrange : null, 58 | elevation: 0, 59 | highlightElevation: 0, 60 | onPressed: _onPressed, 61 | child: Text( 62 | value.name, 63 | style: TextStyle( 64 | color: selected ? Colors.white : null, 65 | ), 66 | ), 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/src/utils/function/extensions/text_editor_extensions_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:rich_text_editor_controller/src/utils/function/extensions/extensions.dart'; 4 | 5 | void main() { 6 | group('TextAlignExtension', () { 7 | group('toAlignment', () { 8 | test( 9 | 'returns Alignment.centerLeft for input TextAlign.start', 10 | () async { 11 | const TextAlign textAlign = TextAlign.start; 12 | 13 | expect(textAlign.toAlignment, Alignment.centerLeft); 14 | }, 15 | ); 16 | 17 | test( 18 | 'returns Alignment.centerLeft for input TextAlign.left', 19 | () async { 20 | const TextAlign textAlign = TextAlign.left; 21 | 22 | expect(textAlign.toAlignment, Alignment.centerLeft); 23 | }, 24 | ); 25 | 26 | test( 27 | 'returns Alignment.centerLeft for input TextAlign.end', 28 | () async { 29 | const TextAlign textAlign = TextAlign.end; 30 | 31 | expect(textAlign.toAlignment, Alignment.centerRight); 32 | }, 33 | ); 34 | 35 | test( 36 | 'returns Alignment.centerLeft for input TextAlign.right', 37 | () async { 38 | const TextAlign textAlign = TextAlign.right; 39 | 40 | expect(textAlign.toAlignment, Alignment.centerRight); 41 | }, 42 | ); 43 | 44 | test( 45 | 'returns Alignment.center for input TextAlign.center', 46 | () async { 47 | const TextAlign textAlign = TextAlign.center; 48 | 49 | expect(textAlign.toAlignment, Alignment.center); 50 | }, 51 | ); 52 | 53 | test( 54 | 'returns Alignment.center for input TextAlign.justify', 55 | () async { 56 | const TextAlign textAlign = TextAlign.justify; 57 | 58 | expect(textAlign.toAlignment, Alignment.center); 59 | }, 60 | ); 61 | }); 62 | }); 63 | } 64 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | env: 10 | flutter_version: "3.7.6" 11 | java_version: "11.x" 12 | 13 | jobs: 14 | format-and-lint: 15 | name: Linting and tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v1 19 | - uses: actions/setup-java@v1 20 | with: 21 | java-version: ${{ env.java_version }} 22 | - name: Cache Flutter dependencies 23 | uses: actions/cache@v1 24 | with: 25 | path: /opt/hostedtoolcache/flutter 26 | key: ${{ runner.OS }}-flutter-install-cache-${{ env.flutter_version }} 27 | - uses: subosito/flutter-action@v1 28 | with: 29 | flutter-version: ${{ env.flutter_version }} 30 | - name: Install dependencies 31 | run: flutter pub get 32 | - name: Format code 33 | run: flutter format . 34 | - name: Lint analysis 35 | run: flutter analyze --no-pub 36 | - name: Run tests 37 | run: flutter test --coverage 38 | # - name: Upload coverage to codecov 39 | # uses: codecov/codecov-action@v1.2.1 40 | # with: 41 | # file: ./coverage/lcov.info 42 | # token: ${{ secrets.CODECOV_TOKEN }} 43 | 44 | # build-android: 45 | # name: Build android client 46 | # needs: [format-and-lint] 47 | # runs-on: ubuntu-latest 48 | # steps: 49 | # - uses: actions/checkout@v1 50 | # - uses: actions/setup-java@v1 51 | # with: 52 | # java-version: ${{ env.java_version }} 53 | # - name: Cache Flutter dependencies 54 | # uses: actions/cache@v1 55 | # with: 56 | # path: /opt/hostedtoolcache/flutter 57 | # key: ${{ runner.OS }}-flutter-install-cache-${{ env.flutter_version }} 58 | # - uses: subosito/flutter-action@v1 59 | # with: 60 | # flutter-version: ${{ env.flutter_version }} 61 | # - name: Install dependencies 62 | # run: flutter pub get 63 | # working-directory: ./example 64 | # - name: Build 65 | # run: flutter build apk 66 | # working-directory: ./example 67 | # - run: git diff --exit-code -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "com.example.example" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | A very lightweight package that allows rich text editing as well as providing a simple and intuitive 2 | API for data serialization 3 | 4 | A Flutter package that lets you edit text in flutter text fields very easily, by 5 | simply just providing it a ```RichTextEditorController``` and ```RichTextField``` ( 6 | just ```TextField``` that supports changing alignment). 7 | note that you can use the controller on a normal ```TextField``` but you will not be able to change 8 | the alignment of the text. 9 | 10 | ## Features 11 | 12 | - Data serialization (you can store and fetch your styled text in json format) 13 | - add bullet points 14 | - change text alignment 15 | - change text color 16 | - change text size (TBD) 17 | - change font style 18 | - change font family (TBD) 19 | - change font weight 20 | - change font features (TBD currently supports changing 1) 21 | - change text decoration 22 | 23 | [video example](https://user-images.githubusercontent.com/89414401/230739943-845d77cd-60df-4d90-ba5a-1c9d14634695.mov) 24 | 25 | ## Getting started 26 | 27 | add this to your ```pubspec.yaml``` file 28 | 29 | ```yaml 30 | dependencies: 31 | flutter: 32 | sdk: flutter 33 | rich_text_editor_flutter: 0.0.5 34 | ``` 35 | 36 | or 37 | using pub 38 | 39 | ```bash 40 | pub add rich_text_editor_flutter 41 | ``` 42 | 43 | ## Usage 44 | 45 | ```dart 46 | import 'package:flutter/material.dart'; 47 | import 'package:flutter/rich_text_editor_controller/rich_text_editor_controller.dart'; 48 | 49 | ... 50 | 51 | class _HomePageState extends State { 52 | 53 | final RichTextEditorController controller = RichTextEditorController(); 54 | 55 | Widget build(BuildContext context) { 56 | return Scaffold( 57 | body: Center( 58 | child: Column( 59 | children: [ 60 | RichTextField( 61 | controller: controller, 62 | maxLines: 10, //use or apply style like in normal text fields 63 | minLines: 1, 64 | ), 65 | ], 66 | ), 67 | ), 68 | ); 69 | } 70 | 71 | } 72 | 73 | ``` 74 | 75 | Or use like normal controller 76 | 77 | ```dart 78 | ... 79 | //or use normal TextField but without alignment support 80 | TextField( 81 | controller: controller, 82 | maxLines: 10, 83 | minLines: 1, 84 | ), 85 | 86 | ... 87 | ``` 88 | 89 | Don't forget to dispose your controller 90 | 91 | ```dart 92 | @override 93 | void dispose() { 94 | controller.dispose(); 95 | super.dispose(); 96 | } 97 | ``` 98 | 99 | For more elaborate example, [see here](https://github.com/folaoluwafemi/rich_text_editor_controller_example) 100 | 101 | ## Additional information 102 | 103 | To create issues, prs or otherwise contribute in anyway see [contribution guide](https://github.com/folaoluwafemi/rich_text_editor_controller/blob/main/CONTRIBUTION_GUIDE.md). 104 | See our roadmap [here](https://github.com/folaoluwafemi/rich_text_editor_controller/blob/main/ROADMAP.md) -------------------------------------------------------------------------------- /lib/src/text_editor/controller/text_editor_controller_public.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math'; 2 | import 'dart:ui'; 3 | 4 | import 'package:flutter/material.dart'; 5 | import 'package:rich_text_editor_controller/src/text_editor/models/text_editor_models_barrel.dart'; 6 | import 'package:rich_text_editor_controller/src/utils/utils_barrel.dart'; 7 | 8 | part 'text_editor_controller.dart'; 9 | 10 | /// This is the main controller for the text editor 11 | class RichTextEditorController extends _RichTextEditorController { 12 | ///This holds all the text changes per character and it's corresponding style/metadata 13 | @override 14 | // ignore: overridden_fields 15 | final TextDeltas deltas; 16 | 17 | static const TextMetadata defaultMetadata = TextMetadata( 18 | alignment: TextAlign.start, 19 | decoration: TextDecorationEnum.none, 20 | fontSize: 14, 21 | fontStyle: FontStyle.normal, 22 | fontWeight: FontWeight.w400, 23 | fontFeatures: null, 24 | ); 25 | 26 | /// Constructs an instance of [RichTextEditorController] with the provided [text] and [deltas] 27 | /// 28 | /// if [text] is not provided, it will be generated from the [deltas]. 29 | /// if [delta] is not provided, it will be generated from the [text] and [metadata]. 30 | /// [metadata] is optional and if not provided, it will be set to [defaultMetadata] 31 | RichTextEditorController({ 32 | String? text, 33 | TextDeltas? deltas, 34 | TextMetadata? metadata, 35 | }) : deltas = deltas ?? 36 | (text == null 37 | ? [] 38 | : TextDeltasUtils.deltasFromString( 39 | text, 40 | metadata ?? defaultMetadata, 41 | )), 42 | super( 43 | text: text ?? deltas?.text, 44 | metaData: metadata, 45 | ) { 46 | addListener(_internalControllerListener); 47 | } 48 | 49 | @override 50 | RichTextEditorController copy() { 51 | return RichTextEditorController( 52 | text: text, 53 | deltas: deltas.copy, 54 | ) 55 | ..value = value 56 | ..metadata = metadata; 57 | } 58 | 59 | /// Data serializer method for this class 60 | Map toMap() { 61 | return { 62 | 'text': text, 63 | 'deltas': deltas.map((TextDelta delta) => delta.toMap()).toList(), 64 | 'metadata': metadata?.toMap(), 65 | 'value': value.toJSON(), 66 | }; 67 | } 68 | 69 | /// Data deserializer method for this class 70 | /// 71 | /// This is used to create a new instance of this class from a map 72 | factory RichTextEditorController.fromMap(Map map) { 73 | return RichTextEditorController( 74 | text: map['text'] as String, 75 | deltas: TextDeltasUtils.deltasFromList( 76 | (map['deltas'] as List).cast(), 77 | ), 78 | ) 79 | ..value = TextEditingValue.fromJSON( 80 | (map['value'] as Map).cast(), 81 | ) 82 | ..metadata = map['metadata'] == null 83 | ? null 84 | : TextMetadata.fromMap( 85 | (map['metadata'] as Map).cast(), 86 | ); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 41 | 42 | 52 | 54 | 60 | 61 | 62 | 63 | 69 | 71 | 77 | 78 | 79 | 80 | 82 | 83 | 86 | 87 | 88 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/lib/src/ui/home_page/home_page.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/foundation.dart'; 2 | import 'package:flutter/material.dart'; 3 | import 'package:rich_text_editor_controller/rich_text_editor_controller.dart'; 4 | 5 | part 'custom/metadata_button.dart'; 6 | 7 | class HomePage extends StatefulWidget { 8 | const HomePage({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _HomePageState(); 12 | } 13 | 14 | class _HomePageState extends State { 15 | final RichTextEditorController controller = RichTextEditorController( 16 | metadata: const TextMetadata( 17 | fontSize: 30, 18 | color: Colors.white, 19 | ), 20 | ); 21 | 22 | @override 23 | void dispose() { 24 | super.dispose(); 25 | controller.dispose(); 26 | } 27 | 28 | void onMetadataButtonPressed(MetadataValue value) { 29 | //switch through value and call the appropriate method on the controller 30 | switch (value) { 31 | case MetadataValue.bold: 32 | controller.toggleBold(); 33 | break; 34 | case MetadataValue.italic: 35 | controller.toggleItalic(); 36 | break; 37 | case MetadataValue.underline: 38 | controller.toggleUnderline(); 39 | break; 40 | case MetadataValue.color: 41 | controller.metadata?.color == Colors.deepOrange 42 | ? controller.changeColor(Colors.black) 43 | : controller.changeColor(Colors.deepOrange); 44 | break; 45 | case MetadataValue.alignRight: 46 | controller.changeAlignment(TextAlign.right); 47 | break; 48 | case MetadataValue.alignCenter: 49 | controller.changeAlignment(TextAlign.center); 50 | break; 51 | case MetadataValue.alignLeft: 52 | controller.changeAlignment(TextAlign.left); 53 | break; 54 | case MetadataValue.superscript: 55 | controller.toggleSuperscript(); 56 | break; 57 | case MetadataValue.subscript: 58 | controller.toggleSubscript(); 59 | break; 60 | case MetadataValue.bulletPoints: 61 | controller.toggleListMode(); 62 | break; 63 | } 64 | } 65 | 66 | @override 67 | Widget build(BuildContext context) { 68 | return Scaffold( 69 | appBar: AppBar( 70 | title: const Text('Rich Text Editor Controller Demo'), 71 | ), 72 | body: SafeArea( 73 | child: Column( 74 | children: [ 75 | ValueListenableBuilder( 76 | valueListenable: controller, 77 | builder: (_, __, ___) { 78 | return Wrap( 79 | children: [ 80 | ...MetadataValue.values.map( 81 | (value) => Padding( 82 | padding: const EdgeInsets.only(left: 8.0), 83 | child: MetadataButton( 84 | metadata: controller.metadata ?? 85 | RichTextEditorController.defaultMetadata, 86 | value: value, 87 | onPressed: onMetadataButtonPressed, 88 | ), 89 | ), 90 | ), 91 | ], 92 | ); 93 | }), 94 | Padding( 95 | padding: const EdgeInsets.symmetric(horizontal: 16.0), 96 | child: RichTextField( 97 | controller: controller, 98 | maxLines: 20, 99 | style: Theme.of(context).textTheme.bodyMedium?.copyWith( 100 | color: Colors.white, 101 | fontSize: 30, 102 | ), 103 | decoration: const InputDecoration( 104 | border: OutlineInputBorder(), 105 | hintText: 'Enter some text', 106 | ), 107 | ), 108 | ), 109 | MaterialButton( 110 | onPressed: () { 111 | final Map data = controller.toMap(); 112 | if (kDebugMode) { 113 | print(data); 114 | } 115 | //add call to your dto/repository here to save the data 116 | }, 117 | color: Colors.deepOrange, 118 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 8), 119 | child: const Text( 120 | 'Save', 121 | style: TextStyle( 122 | fontSize: 20, 123 | color: Colors.white, 124 | ), 125 | ), 126 | ), 127 | ], 128 | ), 129 | ), 130 | ); 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | async: 5 | dependency: transitive 6 | description: 7 | name: async 8 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 9 | url: "https://pub.dev" 10 | source: hosted 11 | version: "2.11.0" 12 | boolean_selector: 13 | dependency: transitive 14 | description: 15 | name: boolean_selector 16 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 17 | url: "https://pub.dev" 18 | source: hosted 19 | version: "2.1.1" 20 | characters: 21 | dependency: transitive 22 | description: 23 | name: characters 24 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 25 | url: "https://pub.dev" 26 | source: hosted 27 | version: "1.3.0" 28 | clock: 29 | dependency: transitive 30 | description: 31 | name: clock 32 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 33 | url: "https://pub.dev" 34 | source: hosted 35 | version: "1.1.1" 36 | collection: 37 | dependency: transitive 38 | description: 39 | name: collection 40 | sha256: "4a07be6cb69c84d677a6c3096fcf960cc3285a8330b4603e0d463d15d9bd934c" 41 | url: "https://pub.dev" 42 | source: hosted 43 | version: "1.17.1" 44 | cupertino_icons: 45 | dependency: "direct main" 46 | description: 47 | name: cupertino_icons 48 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 49 | url: "https://pub.dev" 50 | source: hosted 51 | version: "1.0.5" 52 | fake_async: 53 | dependency: transitive 54 | description: 55 | name: fake_async 56 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 57 | url: "https://pub.dev" 58 | source: hosted 59 | version: "1.3.1" 60 | flutter: 61 | dependency: "direct main" 62 | description: flutter 63 | source: sdk 64 | version: "0.0.0" 65 | flutter_lints: 66 | dependency: "direct dev" 67 | description: 68 | name: flutter_lints 69 | sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c 70 | url: "https://pub.dev" 71 | source: hosted 72 | version: "2.0.1" 73 | flutter_test: 74 | dependency: "direct dev" 75 | description: flutter 76 | source: sdk 77 | version: "0.0.0" 78 | js: 79 | dependency: transitive 80 | description: 81 | name: js 82 | sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3 83 | url: "https://pub.dev" 84 | source: hosted 85 | version: "0.6.7" 86 | lints: 87 | dependency: transitive 88 | description: 89 | name: lints 90 | sha256: "5e4a9cd06d447758280a8ac2405101e0e2094d2a1dbdd3756aec3fe7775ba593" 91 | url: "https://pub.dev" 92 | source: hosted 93 | version: "2.0.1" 94 | matcher: 95 | dependency: transitive 96 | description: 97 | name: matcher 98 | sha256: "6501fbd55da300384b768785b83e5ce66991266cec21af89ab9ae7f5ce1c4cbb" 99 | url: "https://pub.dev" 100 | source: hosted 101 | version: "0.12.15" 102 | material_color_utilities: 103 | dependency: transitive 104 | description: 105 | name: material_color_utilities 106 | sha256: d92141dc6fe1dad30722f9aa826c7fbc896d021d792f80678280601aff8cf724 107 | url: "https://pub.dev" 108 | source: hosted 109 | version: "0.2.0" 110 | meta: 111 | dependency: transitive 112 | description: 113 | name: meta 114 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 115 | url: "https://pub.dev" 116 | source: hosted 117 | version: "1.9.1" 118 | path: 119 | dependency: transitive 120 | description: 121 | name: path 122 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 123 | url: "https://pub.dev" 124 | source: hosted 125 | version: "1.8.3" 126 | rich_text_editor_controller: 127 | dependency: "direct main" 128 | description: 129 | path: ".." 130 | relative: true 131 | source: path 132 | version: "0.0.4" 133 | sky_engine: 134 | dependency: transitive 135 | description: flutter 136 | source: sdk 137 | version: "0.0.99" 138 | source_span: 139 | dependency: transitive 140 | description: 141 | name: source_span 142 | sha256: dd904f795d4b4f3b870833847c461801f6750a9fa8e61ea5ac53f9422b31f250 143 | url: "https://pub.dev" 144 | source: hosted 145 | version: "1.9.1" 146 | stack_trace: 147 | dependency: transitive 148 | description: 149 | name: stack_trace 150 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.11.0" 154 | stream_channel: 155 | dependency: transitive 156 | description: 157 | name: stream_channel 158 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "2.1.1" 162 | string_scanner: 163 | dependency: transitive 164 | description: 165 | name: string_scanner 166 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "1.2.0" 170 | term_glyph: 171 | dependency: transitive 172 | description: 173 | name: term_glyph 174 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "1.2.1" 178 | test_api: 179 | dependency: transitive 180 | description: 181 | name: test_api 182 | sha256: eb6ac1540b26de412b3403a163d919ba86f6a973fe6cc50ae3541b80092fdcfb 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "0.5.1" 186 | vector_math: 187 | dependency: transitive 188 | description: 189 | name: vector_math 190 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "2.1.4" 194 | sdks: 195 | dart: ">=3.0.0-0 <4.0.0" 196 | flutter: ">=1.17.0" 197 | -------------------------------------------------------------------------------- /lib/src/text_editor/ui/widgets/rich_text_field.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/gestures.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:rich_text_editor_controller/src/text_editor/text_editor_barrel.dart'; 6 | 7 | /// A [TextField] that uses a [RichTextEditorController] to control the text. 8 | /// 9 | /// The only relevance of this widget over [TextField] is that it listens to changes in the controller and rebuilds on text align change. 10 | class RichTextField extends TextField { 11 | @override 12 | // ignore: overridden_fields 13 | final RichTextEditorController controller; 14 | 15 | const RichTextField({ 16 | super.key, 17 | required this.controller, 18 | super.focusNode, 19 | super.decoration = const InputDecoration(), 20 | TextInputType? keyboardType, 21 | super.textInputAction, 22 | super.textCapitalization = TextCapitalization.none, 23 | super.style, 24 | super.strutStyle, 25 | super.textAlign = TextAlign.start, 26 | super.textAlignVertical, 27 | super.textDirection, 28 | super.readOnly = false, 29 | super.showCursor, 30 | super.autofocus = false, 31 | super.obscuringCharacter = '•', 32 | super.obscureText = false, 33 | super.autocorrect = true, 34 | SmartDashesType? smartDashesType, 35 | SmartQuotesType? smartQuotesType, 36 | super.enableSuggestions = true, 37 | super.maxLines = 1, 38 | super.minLines, 39 | super.expands = false, 40 | super.maxLength, 41 | super.maxLengthEnforcement, 42 | super.onChanged, 43 | super.onEditingComplete, 44 | super.onSubmitted, 45 | super.onAppPrivateCommand, 46 | super.inputFormatters, 47 | super.enabled, 48 | super.cursorWidth = 2.0, 49 | super.cursorHeight, 50 | super.cursorRadius, 51 | super.cursorColor, 52 | super.selectionHeightStyle = BoxHeightStyle.tight, 53 | super.selectionWidthStyle = BoxWidthStyle.tight, 54 | super.keyboardAppearance, 55 | super.scrollPadding = const EdgeInsets.all(20.0), 56 | super.dragStartBehavior = DragStartBehavior.start, 57 | bool? enableInteractiveSelection, 58 | super.selectionControls, 59 | super.onTap, 60 | super.onTapOutside, 61 | super.mouseCursor, 62 | super.buildCounter, 63 | super.scrollController, 64 | super.scrollPhysics, 65 | super.autofillHints = const [], 66 | super.clipBehavior = Clip.hardEdge, 67 | super.restorationId, 68 | super.scribbleEnabled = true, 69 | super.enableIMEPersonalizedLearning = true, 70 | super.contextMenuBuilder, 71 | super.spellCheckConfiguration, 72 | super.magnifierConfiguration, 73 | }); 74 | 75 | @override 76 | State createState() => _RichTextFieldState(); 77 | } 78 | 79 | class _RichTextFieldState extends State { 80 | late RichTextEditorController controller = widget.controller; 81 | 82 | @override 83 | Widget build(BuildContext context) { 84 | ///value listenable builder added to listen to changes in the controller and rebuild on text align change 85 | return ValueListenableBuilder( 86 | valueListenable: controller, 87 | builder: (_, controllerValue, __) { 88 | return TextField( 89 | keyboardType: widget.keyboardType, 90 | key: widget.key, 91 | controller: widget.controller, 92 | focusNode: widget.focusNode, 93 | decoration: widget.decoration, 94 | textInputAction: widget.textInputAction, 95 | textCapitalization: widget.textCapitalization, 96 | style: widget.style, 97 | strutStyle: widget.strutStyle, 98 | 99 | ///this is the only line that is different from the original text field 100 | textAlign: controller.metadata?.alignment ?? widget.textAlign, 101 | textAlignVertical: widget.textAlignVertical, 102 | textDirection: widget.textDirection, 103 | readOnly: widget.readOnly, 104 | showCursor: widget.showCursor, 105 | autofocus: widget.autofocus, 106 | obscuringCharacter: widget.obscuringCharacter, 107 | obscureText: widget.obscureText, 108 | autocorrect: widget.autocorrect, 109 | enableSuggestions: widget.enableSuggestions, 110 | maxLines: widget.maxLines, 111 | minLines: widget.minLines, 112 | expands: widget.expands, 113 | maxLength: widget.maxLength, 114 | maxLengthEnforcement: widget.maxLengthEnforcement, 115 | onChanged: widget.onChanged, 116 | onEditingComplete: widget.onEditingComplete, 117 | onSubmitted: widget.onSubmitted, 118 | onAppPrivateCommand: widget.onAppPrivateCommand, 119 | inputFormatters: widget.inputFormatters, 120 | enabled: widget.enabled, 121 | cursorWidth: widget.cursorWidth, 122 | cursorHeight: widget.cursorHeight, 123 | cursorRadius: widget.cursorRadius, 124 | cursorColor: widget.cursorColor, 125 | selectionHeightStyle: widget.selectionHeightStyle, 126 | selectionWidthStyle: widget.selectionWidthStyle, 127 | keyboardAppearance: widget.keyboardAppearance, 128 | scrollPadding: widget.scrollPadding, 129 | dragStartBehavior: widget.dragStartBehavior, 130 | enableInteractiveSelection: widget.enableInteractiveSelection, 131 | selectionControls: widget.selectionControls, 132 | onTap: widget.onTap, 133 | onTapOutside: widget.onTapOutside, 134 | mouseCursor: widget.mouseCursor, 135 | buildCounter: widget.buildCounter, 136 | scrollController: widget.scrollController, 137 | scrollPhysics: widget.scrollPhysics, 138 | autofillHints: widget.autofillHints, 139 | clipBehavior: widget.clipBehavior, 140 | restorationId: widget.restorationId, 141 | scribbleEnabled: widget.scribbleEnabled, 142 | enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, 143 | contextMenuBuilder: widget.contextMenuBuilder, 144 | spellCheckConfiguration: widget.spellCheckConfiguration, 145 | magnifierConfiguration: widget.magnifierConfiguration, 146 | ); 147 | }, 148 | ); 149 | } 150 | 151 | @override 152 | void didUpdateWidget(RichTextField oldWidget) { 153 | super.didUpdateWidget(oldWidget); 154 | if (oldWidget.controller != widget.controller) { 155 | controller = widget.controller; 156 | } 157 | } 158 | } 159 | -------------------------------------------------------------------------------- /lib/src/text_editor/models/text_metadata/text_metadata.dart: -------------------------------------------------------------------------------- 1 | import 'dart:ui'; 2 | 3 | import 'package:flutter/material.dart'; 4 | import 'package:rich_text_editor_controller/src/text_editor/models/text_metadata/text_metadata_enum.dart'; 5 | import 'package:rich_text_editor_controller/src/utils/utils_barrel.dart'; 6 | 7 | part 'text_decoration_enum.dart'; 8 | 9 | class TextMetadata { 10 | final Color color; 11 | final FontWeight fontWeight; 12 | final FontStyle fontStyle; 13 | final double fontSize; 14 | final TextDecorationEnum decoration; 15 | final List? fontFeatures; 16 | final TextAlign alignment; 17 | 18 | const TextMetadata({ 19 | this.color = Colors.black, 20 | this.fontWeight = FontWeight.w400, 21 | this.fontStyle = FontStyle.normal, 22 | this.fontSize = 14, 23 | this.alignment = TextAlign.start, 24 | this.decoration = TextDecorationEnum.none, 25 | this.fontFeatures, 26 | }); 27 | 28 | TextMetadata.fromTextStyle( 29 | TextStyle style, { 30 | this.alignment = TextAlign.start, 31 | }) : color = style.color ?? Colors.black, 32 | fontWeight = style.fontWeight ?? FontWeight.w400, 33 | fontStyle = style.fontStyle ?? FontStyle.normal, 34 | fontSize = style.fontSize ?? 14, 35 | decoration = style.decoration == null 36 | ? TextDecorationEnum.none 37 | : TextDecorationEnum.fromDecoration(style.decoration!), 38 | fontFeatures = style.fontFeatures; 39 | 40 | TextMetadata copyWith({ 41 | Color? color, 42 | FontWeight? fontWeight, 43 | FontStyle? fontStyle, 44 | double? fontSize, 45 | TextDecorationEnum? decoration, 46 | List? fontFeatures, 47 | TextAlign? alignment, 48 | }) { 49 | return TextMetadata( 50 | color: color ?? this.color, 51 | fontWeight: fontWeight ?? this.fontWeight, 52 | fontStyle: fontStyle ?? this.fontStyle, 53 | fontSize: fontSize ?? this.fontSize, 54 | decoration: decoration ?? this.decoration, 55 | fontFeatures: fontFeatures ?? this.fontFeatures, 56 | alignment: alignment ?? this.alignment, 57 | ); 58 | } 59 | 60 | /// This method is used to combine two [TextMetadata] objects relative to 61 | /// the specified [TextMetadataChange] 62 | TextMetadata combineWhatChanged( 63 | TextMetadataChange change, 64 | TextMetadata other, 65 | ) { 66 | switch (change) { 67 | case TextMetadataChange.all: 68 | return other; 69 | case TextMetadataChange.color: 70 | return copyWith(color: other.color); 71 | case TextMetadataChange.fontWeight: 72 | return copyWith(fontWeight: other.fontWeight); 73 | case TextMetadataChange.fontStyle: 74 | return copyWith(fontStyle: other.fontStyle); 75 | case TextMetadataChange.fontSize: 76 | return copyWith(fontSize: other.fontSize); 77 | case TextMetadataChange.alignment: 78 | return copyWith(alignment: other.alignment); 79 | case TextMetadataChange.fontDecoration: 80 | return copyWith(decoration: other.decoration); 81 | case TextMetadataChange.fontFeatures: 82 | return copyWith(fontFeatures: other.fontFeatures); 83 | } 84 | } 85 | 86 | TextStyle get style => TextStyle( 87 | fontSize: fontSize, 88 | color: color, 89 | decoration: decoration.value, 90 | fontWeight: fontWeight, 91 | fontStyle: fontStyle, 92 | fontFeatures: fontFeatures, 93 | ); 94 | 95 | TextStyle get styleWithoutFontFeatures => TextStyle( 96 | fontSize: fontSize, 97 | color: color, 98 | decoration: decoration.value, 99 | fontWeight: fontWeight, 100 | fontStyle: fontStyle, 101 | ); 102 | 103 | factory TextMetadata.combineWhereNotEqual( 104 | final TextMetadata metadata1, 105 | final TextMetadata metadata2, { 106 | bool favourFirst = true, 107 | }) { 108 | return TextMetadata( 109 | color: favourFirst ? metadata1.color : metadata2.color, 110 | fontWeight: favourFirst ? metadata1.fontWeight : metadata2.fontWeight, 111 | fontStyle: favourFirst ? metadata1.fontStyle : metadata2.fontStyle, 112 | fontSize: favourFirst ? metadata1.fontSize : metadata2.fontSize, 113 | decoration: favourFirst ? metadata1.decoration : metadata2.decoration, 114 | fontFeatures: 115 | favourFirst ? metadata1.fontFeatures : metadata2.fontFeatures, 116 | alignment: favourFirst ? metadata1.alignment : metadata2.alignment, 117 | ); 118 | } 119 | 120 | TextMetadata combineWith( 121 | TextMetadata other, { 122 | bool favourOther = true, 123 | }) { 124 | return TextMetadata( 125 | color: color == other.color 126 | ? color 127 | : favourOther 128 | ? other.color 129 | : color, 130 | fontWeight: fontWeight == other.fontWeight 131 | ? fontWeight 132 | : favourOther 133 | ? other.fontWeight 134 | : fontWeight, 135 | fontStyle: fontStyle == other.fontStyle 136 | ? fontStyle 137 | : favourOther 138 | ? other.fontStyle 139 | : fontStyle, 140 | fontSize: fontSize == other.fontSize 141 | ? fontSize 142 | : favourOther 143 | ? other.fontSize 144 | : fontSize, 145 | decoration: decoration == other.decoration 146 | ? decoration 147 | : favourOther 148 | ? other.decoration 149 | : decoration, 150 | fontFeatures: fontFeatures == other.fontFeatures 151 | ? fontFeatures 152 | : favourOther 153 | ? other.fontFeatures ?? fontFeatures 154 | : fontFeatures ?? other.fontFeatures, 155 | alignment: alignment == other.alignment 156 | ? alignment 157 | : favourOther 158 | ? other.alignment 159 | : alignment, 160 | ); 161 | } 162 | 163 | factory TextMetadata.fromMap(Map map) { 164 | return TextMetadata( 165 | color: UtilFunctions.colorFromMap(map), 166 | fontWeight: FontWeight.values[(map['fontWeight'])], 167 | fontStyle: FontStyle.values[(map['fontStyle'])], 168 | fontSize: map['fontSize'] as double, 169 | fontFeatures: (map['fontFeatures'] as List?) 170 | ?.cast>() 171 | .map((e) => _fontFeatureFromMap(e)) 172 | .toList(), 173 | alignment: TextAlign.values[(map['alignment'])], 174 | decoration: TextDecorationEnum.values[(map['decoration'])], 175 | ); 176 | } 177 | 178 | Map toMap() { 179 | return { 180 | 'color': color.toSerializerString, 181 | 'fontWeight': fontWeight.index, 182 | 'fontStyle': fontStyle.index, 183 | 'fontSize': fontSize, 184 | 'fontFeatures': fontFeatures?.map((e) => _fontFeatureToMap(e)).toList(), 185 | 'alignment': alignment.index, 186 | 'decoration': decoration.index, 187 | }; 188 | } 189 | 190 | @override 191 | bool operator ==(Object other) => 192 | identical(this, other) || 193 | other is TextMetadata && 194 | runtimeType == other.runtimeType && 195 | color == other.color && 196 | fontWeight == other.fontWeight && 197 | fontStyle == other.fontStyle && 198 | fontSize == other.fontSize && 199 | decoration == other.decoration && 200 | fontFeatures == other.fontFeatures && 201 | alignment == other.alignment; 202 | 203 | @override 204 | int get hashCode => 205 | color.hashCode ^ 206 | fontWeight.hashCode ^ 207 | fontStyle.hashCode ^ 208 | fontSize.hashCode ^ 209 | decoration.hashCode ^ 210 | fontFeatures.hashCode ^ 211 | alignment.hashCode; 212 | 213 | @override 214 | String toString() { 215 | return ''' 216 | TextMetadata{ 217 | color: $color, 218 | fontWeight: $fontWeight, 219 | fontStyle: $fontStyle, 220 | fontSize: $fontSize, 221 | decoration: $decoration, 222 | fontFeatures: $fontFeatures, 223 | alignment: $alignment 224 | }'''; 225 | } 226 | } 227 | 228 | FontFeature _fontFeatureFromMap(Map map) { 229 | return FontFeature( 230 | map['feature'], 231 | map['value'], 232 | ); 233 | } 234 | 235 | Map _fontFeatureToMap(FontFeature feature) { 236 | return { 237 | 'feature': feature.feature, 238 | 'value': feature.value, 239 | }; 240 | } 241 | -------------------------------------------------------------------------------- /lib/src/text_editor/ui/widgets/rich_text_form_field.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | import 'package:flutter/services.dart'; 3 | import 'package:rich_text_editor_controller/rich_text_editor_controller.dart'; 4 | 5 | /// A [TextFormField] that uses a [RichTextEditorController] to control the text. 6 | /// 7 | /// The only relevance of this widget over [TextFormField] is that it listens to changes in the controller and rebuilds on text align change. 8 | class RichTextFormField extends TextFormField { 9 | @override 10 | // ignore: overridden_fields 11 | final RichTextEditorController controller; 12 | 13 | final FocusNode? focusNode; 14 | final InputDecoration? decoration; 15 | final TextInputType? keyboardType; 16 | final TextCapitalization textCapitalization; 17 | final TextInputAction? textInputAction; 18 | final TextStyle? style; 19 | final StrutStyle? strutStyle; 20 | final TextDirection? textDirection; 21 | final TextAlign textAlign; 22 | final TextAlignVertical? textAlignVertical; 23 | final bool autofocus; 24 | final bool readOnly; 25 | final bool? showCursor; 26 | final String obscuringCharacter; 27 | final bool obscureText; 28 | final bool autocorrect; 29 | final SmartDashesType? smartDashesType; 30 | final SmartQuotesType? smartQuotesType; 31 | final bool enableSuggestions; 32 | final MaxLengthEnforcement? maxLengthEnforcement; 33 | final int? maxLines; 34 | final int? minLines; 35 | final bool expands; 36 | final int? maxLength; 37 | final ValueChanged? onChanged; 38 | final GestureTapCallback? onTap; 39 | final TapRegionCallback? onTapOutside; 40 | final VoidCallback? onEditingComplete; 41 | final ValueChanged? onFieldSubmitted; 42 | final List? inputFormatters; 43 | final double cursorWidth; 44 | final double? cursorHeight; 45 | final Radius? cursorRadius; 46 | final Color? cursorColor; 47 | final Brightness? keyboardAppearance; 48 | final EdgeInsets scrollPadding; 49 | final bool? enableInteractiveSelection; 50 | final TextSelectionControls? selectionControls; 51 | final InputCounterWidgetBuilder? buildCounter; 52 | final ScrollPhysics? scrollPhysics; 53 | final Iterable? autofillHints; 54 | final ScrollController? scrollController; 55 | final bool enableIMEPersonalizedLearning; 56 | final MouseCursor? mouseCursor; 57 | final EditableTextContextMenuBuilder? contextMenuBuilder; 58 | 59 | RichTextFormField({ 60 | required this.controller, 61 | super.initialValue, 62 | super.autovalidateMode, 63 | this.focusNode, 64 | this.decoration = const InputDecoration(), 65 | this.keyboardType, 66 | this.textCapitalization = TextCapitalization.none, 67 | this.textInputAction, 68 | this.style, 69 | this.strutStyle, 70 | this.textDirection, 71 | this.textAlign = TextAlign.start, 72 | this.textAlignVertical, 73 | this.autofocus = false, 74 | this.readOnly = false, 75 | this.showCursor, 76 | this.obscuringCharacter = '•', 77 | this.obscureText = false, 78 | this.autocorrect = true, 79 | this.smartDashesType, 80 | this.smartQuotesType, 81 | this.enableSuggestions = true, 82 | this.maxLengthEnforcement, 83 | this.maxLines = 1, 84 | this.minLines, 85 | this.expands = false, 86 | this.maxLength, 87 | this.onChanged, 88 | this.onTap, 89 | this.onTapOutside, 90 | this.onEditingComplete, 91 | this.onFieldSubmitted, 92 | this.inputFormatters, 93 | super.enabled, 94 | this.cursorWidth = 2.0, 95 | this.cursorHeight, 96 | this.cursorRadius, 97 | this.cursorColor, 98 | this.keyboardAppearance, 99 | this.scrollPadding = const EdgeInsets.all(20.0), 100 | this.enableInteractiveSelection, 101 | this.selectionControls, 102 | this.buildCounter, 103 | this.scrollPhysics, 104 | this.autofillHints, 105 | this.scrollController, 106 | this.enableIMEPersonalizedLearning = true, 107 | this.mouseCursor, 108 | this.contextMenuBuilder, 109 | super.key, 110 | super.onSaved, 111 | super.validator, 112 | }); 113 | 114 | @override 115 | FormFieldState createState() => _RichTextFormFieldState(); 116 | } 117 | 118 | class _RichTextFormFieldState extends FormFieldState { 119 | late RichTextEditorController controller = _richTextFormField.controller; 120 | 121 | RichTextFormField get _richTextFormField => super.widget as RichTextFormField; 122 | 123 | @override 124 | void didUpdateWidget(RichTextFormField oldWidget) { 125 | super.didUpdateWidget(oldWidget); 126 | if (oldWidget.controller != _richTextFormField.controller) { 127 | controller = _richTextFormField.controller; 128 | } 129 | } 130 | 131 | @override 132 | Widget build(BuildContext context) { 133 | ///value listenable builder added to listen to changes in the controller and rebuild on text align change 134 | return ValueListenableBuilder( 135 | valueListenable: controller, 136 | builder: (_, controllerValue, __) { 137 | return TextFormField( 138 | ///this is the only line that is different from the original text field 139 | textAlign: 140 | controller.metadata?.alignment ?? _richTextFormField.textAlign, 141 | key: _richTextFormField.key, 142 | controller: controller, 143 | focusNode: _richTextFormField.focusNode, 144 | decoration: _richTextFormField.decoration, 145 | keyboardType: _richTextFormField.keyboardType, 146 | textInputAction: _richTextFormField.textInputAction, 147 | textCapitalization: _richTextFormField.textCapitalization, 148 | style: _richTextFormField.style, 149 | strutStyle: _richTextFormField.strutStyle, 150 | textAlignVertical: _richTextFormField.textAlignVertical, 151 | textDirection: _richTextFormField.textDirection, 152 | readOnly: _richTextFormField.readOnly, 153 | showCursor: _richTextFormField.showCursor, 154 | autofocus: _richTextFormField.autofocus, 155 | obscuringCharacter: _richTextFormField.obscuringCharacter, 156 | obscureText: _richTextFormField.obscureText, 157 | autocorrect: _richTextFormField.autocorrect, 158 | smartDashesType: _richTextFormField.smartDashesType, 159 | smartQuotesType: _richTextFormField.smartQuotesType, 160 | enableSuggestions: _richTextFormField.enableSuggestions, 161 | maxLines: _richTextFormField.maxLines, 162 | minLines: _richTextFormField.minLines, 163 | expands: _richTextFormField.expands, 164 | maxLength: _richTextFormField.maxLength, 165 | maxLengthEnforcement: _richTextFormField.maxLengthEnforcement, 166 | onChanged: _richTextFormField.onChanged, 167 | onTap: _richTextFormField.onTap, 168 | onTapOutside: _richTextFormField.onTapOutside, 169 | onEditingComplete: _richTextFormField.onEditingComplete, 170 | onFieldSubmitted: _richTextFormField.onFieldSubmitted, 171 | onSaved: _richTextFormField.onSaved, 172 | validator: _richTextFormField.validator, 173 | inputFormatters: _richTextFormField.inputFormatters, 174 | enabled: _richTextFormField.enabled, 175 | cursorWidth: _richTextFormField.cursorWidth, 176 | cursorHeight: _richTextFormField.cursorHeight, 177 | cursorRadius: _richTextFormField.cursorRadius, 178 | cursorColor: _richTextFormField.cursorColor, 179 | keyboardAppearance: _richTextFormField.keyboardAppearance, 180 | scrollPadding: _richTextFormField.scrollPadding, 181 | enableInteractiveSelection: 182 | _richTextFormField.enableInteractiveSelection, 183 | selectionControls: _richTextFormField.selectionControls, 184 | buildCounter: _richTextFormField.buildCounter, 185 | scrollController: _richTextFormField.scrollController, 186 | scrollPhysics: _richTextFormField.scrollPhysics, 187 | autofillHints: _richTextFormField.autofillHints, 188 | contextMenuBuilder: _richTextFormField.contextMenuBuilder, 189 | autovalidateMode: _richTextFormField.autovalidateMode, 190 | enableIMEPersonalizedLearning: 191 | _richTextFormField.enableIMEPersonalizedLearning, 192 | initialValue: _richTextFormField.initialValue, 193 | mouseCursor: _richTextFormField.mouseCursor, 194 | restorationId: _richTextFormField.restorationId, 195 | ); 196 | }, 197 | ); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /coverage/lcov.info: -------------------------------------------------------------------------------- 1 | SF:lib/src/text_editor/controller/text_editor_controller_public.dart 2 | DA:26,0 3 | DA:30,0 4 | DA:31,0 5 | DA:34,0 6 | DA:36,0 7 | DA:37,0 8 | DA:38,0 9 | DA:40,0 10 | DA:41,0 11 | DA:45,0 12 | DA:46,0 13 | DA:47,0 14 | DA:48,0 15 | DA:49,0 16 | DA:50,0 17 | DA:57,0 18 | DA:58,0 19 | DA:59,0 20 | DA:60,0 21 | DA:61,0 22 | DA:64,0 23 | DA:65,0 24 | DA:67,0 25 | DA:69,0 26 | DA:70,0 27 | LF:25 28 | LH:0 29 | end_of_record 30 | SF:lib/src/text_editor/controller/text_editor_controller.dart 31 | DA:12,0 32 | DA:19,0 33 | DA:20,0 34 | DA:21,0 35 | DA:22,0 36 | DA:25,0 37 | DA:28,0 38 | DA:29,0 39 | DA:32,0 40 | DA:33,0 41 | DA:34,0 42 | DA:40,0 43 | DA:41,0 44 | DA:42,0 45 | DA:43,0 46 | DA:45,0 47 | DA:46,0 48 | DA:50,0 49 | DA:55,0 50 | DA:56,0 51 | DA:57,0 52 | DA:59,0 53 | DA:60,0 54 | DA:63,0 55 | DA:67,0 56 | DA:68,0 57 | DA:71,0 58 | DA:72,0 59 | DA:73,0 60 | DA:74,0 61 | DA:76,0 62 | DA:79,0 63 | DA:80,0 64 | DA:81,0 65 | DA:82,0 66 | DA:85,0 67 | DA:86,0 68 | DA:87,0 69 | DA:90,0 70 | DA:92,0 71 | DA:93,0 72 | DA:94,0 73 | DA:95,0 74 | DA:96,0 75 | DA:99,0 76 | DA:106,0 77 | DA:108,0 78 | DA:114,0 79 | DA:118,0 80 | DA:120,0 81 | DA:121,0 82 | DA:123,0 83 | DA:124,0 84 | DA:126,0 85 | DA:128,0 86 | DA:129,0 87 | DA:131,0 88 | DA:133,0 89 | DA:134,0 90 | DA:135,0 91 | DA:136,0 92 | DA:138,0 93 | DA:139,0 94 | DA:140,0 95 | DA:141,0 96 | DA:142,0 97 | DA:143,0 98 | DA:149,0 99 | DA:150,0 100 | DA:151,0 101 | DA:152,0 102 | DA:154,0 103 | DA:156,0 104 | DA:157,0 105 | DA:158,0 106 | DA:159,0 107 | DA:160,0 108 | DA:161,0 109 | DA:162,0 110 | DA:171,0 111 | DA:172,0 112 | DA:175,0 113 | DA:181,0 114 | DA:183,0 115 | DA:184,0 116 | DA:187,0 117 | DA:193,0 118 | DA:195,0 119 | DA:197,0 120 | DA:198,0 121 | DA:205,0 122 | DA:211,0 123 | DA:217,0 124 | DA:219,0 125 | DA:220,0 126 | DA:222,0 127 | DA:223,0 128 | DA:224,0 129 | DA:235,0 130 | DA:237,0 131 | DA:238,0 132 | DA:239,0 133 | DA:244,0 134 | DA:247,0 135 | DA:248,0 136 | DA:253,0 137 | DA:255,0 138 | DA:257,0 139 | DA:258,0 140 | DA:262,0 141 | DA:265,0 142 | DA:266,0 143 | DA:271,0 144 | DA:273,0 145 | DA:275,0 146 | DA:276,0 147 | DA:281,0 148 | DA:284,0 149 | DA:285,0 150 | DA:290,0 151 | DA:292,0 152 | DA:294,0 153 | DA:295,0 154 | DA:301,0 155 | DA:304,0 156 | DA:305,0 157 | DA:310,0 158 | DA:312,0 159 | DA:314,0 160 | DA:315,0 161 | DA:321,0 162 | DA:324,0 163 | DA:325,0 164 | DA:329,0 165 | DA:331,0 166 | DA:334,0 167 | DA:337,0 168 | DA:338,0 169 | DA:342,0 170 | DA:344,0 171 | DA:347,0 172 | DA:350,0 173 | DA:351,0 174 | DA:370,0 175 | DA:371,0 176 | DA:372,0 177 | DA:373,0 178 | DA:377,0 179 | DA:383,0 180 | DA:385,0 181 | DA:386,0 182 | DA:387,0 183 | DA:388,0 184 | DA:389,0 185 | DA:390,0 186 | DA:395,0 187 | DA:396,0 188 | DA:402,0 189 | DA:404,0 190 | DA:405,0 191 | LF:160 192 | LH:0 193 | end_of_record 194 | SF:lib/src/text_editor/models/text_delta/text_delta.dart 195 | DA:8,0 196 | DA:13,0 197 | DA:17,0 198 | DA:18,0 199 | DA:19,0 200 | DA:23,0 201 | DA:24,0 202 | DA:25,0 203 | DA:26,0 204 | DA:30,0 205 | DA:31,0 206 | DA:32,0 207 | DA:33,0 208 | DA:35,0 209 | DA:36,0 210 | DA:41,0 211 | DA:42,0 212 | DA:44,0 213 | DA:48,0 214 | DA:51,0 215 | DA:52,0 216 | DA:53,0 217 | DA:54,0 218 | DA:56,0 219 | DA:57,0 220 | LF:25 221 | LH:0 222 | end_of_record 223 | SF:lib/src/text_editor/models/text_deltas/text_deltas_utils.dart 224 | DA:4,0 225 | DA:8,0 226 | DA:9,0 227 | DA:11,0 228 | DA:12,0 229 | DA:17,0 230 | DA:18,0 231 | DA:19,0 232 | DA:20,0 233 | LF:9 234 | LH:0 235 | end_of_record 236 | SF:lib/src/text_editor/models/text_deltas/text_deltas.dart 237 | DA:10,0 238 | DA:11,0 239 | DA:12,0 240 | DA:13,0 241 | DA:14,0 242 | DA:16,0 243 | DA:20,0 244 | LF:7 245 | LH:0 246 | end_of_record 247 | SF:lib/src/text_editor/models/text_metadata/text_metadata.dart 248 | DA:18,7 249 | DA:28,0 250 | DA:37,0 251 | DA:38,0 252 | DA:39,0 253 | DA:40,0 254 | DA:41,0 255 | DA:42,0 256 | DA:43,0 257 | DA:44,0 258 | DA:48,0 259 | DA:53,0 260 | DA:55,0 261 | DA:56,0 262 | DA:57,0 263 | DA:58,0 264 | DA:59,0 265 | DA:60,0 266 | DA:61,0 267 | DA:62,0 268 | DA:63,0 269 | DA:64,0 270 | DA:65,0 271 | DA:66,0 272 | DA:67,0 273 | DA:68,0 274 | DA:72,0 275 | DA:73,0 276 | DA:74,0 277 | DA:75,0 278 | DA:76,0 279 | DA:77,0 280 | DA:78,0 281 | DA:81,0 282 | DA:82,0 283 | DA:83,0 284 | DA:84,0 285 | DA:85,0 286 | DA:86,0 287 | DA:89,0 288 | DA:94,0 289 | DA:95,0 290 | DA:96,0 291 | DA:97,0 292 | DA:98,0 293 | DA:99,0 294 | DA:101,0 295 | DA:102,0 296 | DA:106,0 297 | DA:110,0 298 | DA:111,0 299 | DA:112,0 300 | DA:114,0 301 | DA:115,0 302 | DA:116,0 303 | DA:117,0 304 | DA:119,0 305 | DA:120,0 306 | DA:121,0 307 | DA:122,0 308 | DA:124,0 309 | DA:125,0 310 | DA:126,0 311 | DA:127,0 312 | DA:129,0 313 | DA:130,0 314 | DA:131,0 315 | DA:132,0 316 | DA:134,0 317 | DA:135,0 318 | DA:136,0 319 | DA:137,0 320 | DA:139,0 321 | DA:140,0 322 | DA:141,0 323 | DA:142,0 324 | DA:144,0 325 | DA:145,0 326 | DA:149,0 327 | DA:150,0 328 | DA:151,0 329 | DA:152,0 330 | DA:153,0 331 | DA:154,0 332 | DA:155,0 333 | DA:156,0 334 | DA:157,0 335 | DA:161,0 336 | DA:164,0 337 | DA:165,0 338 | DA:166,0 339 | DA:167,0 340 | DA:168,0 341 | DA:169,0 342 | DA:170,0 343 | DA:171,0 344 | DA:172,0 345 | DA:174,0 346 | DA:176,0 347 | DA:177,0 348 | DA:178,0 349 | DA:179,0 350 | DA:180,0 351 | DA:181,0 352 | DA:182,0 353 | DA:184,0 354 | DA:188,0 355 | DA:189,0 356 | DA:190,0 357 | DA:191,0 358 | DA:192,0 359 | DA:193,0 360 | DA:194,0 361 | DA:195,0 362 | DA:198,0 363 | DA:199,0 364 | DA:200,0 365 | DA:201,0 366 | DA:202,0 367 | DA:203,0 368 | DA:204,0 369 | DA:205,0 370 | DA:206,0 371 | DA:207,0 372 | DA:208,0 373 | DA:209,0 374 | DA:214,0 375 | DA:215,0 376 | DA:216,0 377 | DA:217,0 378 | DA:221,0 379 | DA:222,0 380 | DA:223,0 381 | DA:224,0 382 | LF:134 383 | LH:1 384 | end_of_record 385 | SF:lib/src/text_editor/ui/widgets/rich_text_field.dart 386 | DA:15,0 387 | DA:75,0 388 | DA:76,0 389 | DA:82,0 390 | DA:85,0 391 | DA:86,0 392 | DA:87,0 393 | DA:88,0 394 | DA:89,0 395 | DA:90,0 396 | DA:91,0 397 | DA:92,0 398 | DA:93,0 399 | DA:94,0 400 | DA:95,0 401 | DA:96,0 402 | DA:97,0 403 | DA:100,0 404 | DA:101,0 405 | DA:102,0 406 | DA:103,0 407 | DA:104,0 408 | DA:105,0 409 | DA:106,0 410 | DA:107,0 411 | DA:108,0 412 | DA:109,0 413 | DA:110,0 414 | DA:111,0 415 | DA:112,0 416 | DA:113,0 417 | DA:114,0 418 | DA:115,0 419 | DA:116,0 420 | DA:117,0 421 | DA:118,0 422 | DA:119,0 423 | DA:120,0 424 | DA:121,0 425 | DA:122,0 426 | DA:123,0 427 | DA:124,0 428 | DA:125,0 429 | DA:126,0 430 | DA:127,0 431 | DA:128,0 432 | DA:129,0 433 | DA:130,0 434 | DA:131,0 435 | DA:132,0 436 | DA:133,0 437 | DA:134,0 438 | DA:135,0 439 | DA:136,0 440 | DA:137,0 441 | DA:138,0 442 | DA:139,0 443 | DA:140,0 444 | DA:141,0 445 | DA:142,0 446 | DA:143,0 447 | DA:144,0 448 | DA:145,0 449 | DA:150,0 450 | DA:152,0 451 | DA:153,0 452 | DA:154,0 453 | LF:67 454 | LH:0 455 | end_of_record 456 | SF:lib/src/text_editor/ui/widgets/rich_text_form_field.dart 457 | DA:59,0 458 | DA:114,0 459 | DA:115,0 460 | DA:121,0 461 | DA:123,0 462 | DA:125,0 463 | DA:126,0 464 | DA:127,0 465 | DA:131,0 466 | DA:134,0 467 | DA:135,0 468 | DA:136,0 469 | DA:137,0 470 | DA:140,0 471 | DA:141,0 472 | DA:142,0 473 | DA:143,0 474 | DA:144,0 475 | DA:145,0 476 | DA:146,0 477 | DA:147,0 478 | DA:148,0 479 | DA:149,0 480 | DA:150,0 481 | DA:151,0 482 | DA:152,0 483 | DA:153,0 484 | DA:154,0 485 | DA:155,0 486 | DA:156,0 487 | DA:157,0 488 | DA:158,0 489 | DA:159,0 490 | DA:160,0 491 | DA:161,0 492 | DA:162,0 493 | DA:163,0 494 | DA:164,0 495 | DA:165,0 496 | DA:166,0 497 | DA:167,0 498 | DA:168,0 499 | DA:169,0 500 | DA:170,0 501 | DA:171,0 502 | DA:172,0 503 | DA:173,0 504 | DA:174,0 505 | DA:175,0 506 | DA:176,0 507 | DA:177,0 508 | DA:178,0 509 | DA:179,0 510 | DA:180,0 511 | DA:182,0 512 | DA:183,0 513 | DA:184,0 514 | DA:185,0 515 | DA:186,0 516 | DA:187,0 517 | DA:188,0 518 | DA:189,0 519 | DA:191,0 520 | DA:192,0 521 | DA:193,0 522 | DA:194,0 523 | LF:66 524 | LH:0 525 | end_of_record 526 | SF:lib/src/utils/function/extensions/color_extension.dart 527 | DA:4,3 528 | LF:1 529 | LH:1 530 | end_of_record 531 | SF:lib/src/utils/function/extensions/iterable_extension.dart 532 | DA:4,1 533 | DA:5,2 534 | DA:6,1 535 | LF:3 536 | LH:3 537 | end_of_record 538 | SF:lib/src/utils/function/extensions/list_extension.dart 539 | DA:4,1 540 | DA:6,1 541 | DA:12,1 542 | DA:14,1 543 | LF:4 544 | LH:4 545 | end_of_record 546 | SF:lib/src/utils/function/extensions/nullable_string_extension.dart 547 | DA:4,2 548 | LF:1 549 | LH:1 550 | end_of_record 551 | SF:lib/src/utils/function/extensions/string_extension.dart 552 | DA:4,1 553 | DA:5,1 554 | DA:8,2 555 | LF:3 556 | LH:3 557 | end_of_record 558 | SF:lib/src/utils/function/extensions/text_editor_extensions.dart 559 | DA:4,1 560 | DA:6,1 561 | DA:7,1 562 | DA:9,1 563 | DA:10,1 564 | DA:12,1 565 | DA:14,1 566 | LF:7 567 | LH:7 568 | end_of_record 569 | SF:lib/src/utils/function/util_functions/util_functions.dart 570 | DA:4,1 571 | DA:5,4 572 | LF:2 573 | LH:2 574 | end_of_record 575 | -------------------------------------------------------------------------------- /lib/src/text_editor/controller/text_editor_controller.dart: -------------------------------------------------------------------------------- 1 | part of 'text_editor_controller_public.dart'; 2 | 3 | class _RichTextEditorController extends TextEditingController { 4 | ///This holds all the text changes per character and it's corresponding style/metadata 5 | final TextDeltas deltas; 6 | 7 | /// This holds the state of the text styles. 8 | /// on every selection change (collapsed or not) it's value defaults to that 9 | /// of the [TextDelta] before it in the [deltas] list or the [defaultMetadata] if it's the first 10 | TextMetadata? _metadata; 11 | 12 | TextMetadata? get metadata => _metadata; 13 | 14 | /// This is holds the state of the metadata change temporarily. 15 | /// 16 | /// it is reset when it's getter is called 17 | bool _metadataToggled = false; 18 | 19 | bool get metadataToggled { 20 | if (_metadataToggled) { 21 | final bool value = _metadataToggled; 22 | _metadataToggled = false; 23 | return value; 24 | } 25 | return _metadataToggled; 26 | } 27 | 28 | set metadataToggled(bool value) { 29 | _metadataToggled = value; 30 | } 31 | 32 | set metadata(TextMetadata? value) { 33 | _metadata = value; 34 | notifyListeners(); 35 | } 36 | 37 | /// returns a copy of this controller 38 | /// 39 | /// why? because all flutter [Listenable] objects are stored in memory and passed by reference 40 | _RichTextEditorController copy() { 41 | return _RichTextEditorController( 42 | text: text, 43 | deltas: deltas.copy, 44 | ) 45 | ..value = value 46 | ..metadata = metadata; 47 | } 48 | 49 | /// returns a copy of this controller with the given parameters 50 | _RichTextEditorController copyWith({ 51 | TextDeltas? deltas, 52 | TextEditingValue? value, 53 | TextMetadata? metadata, 54 | }) { 55 | return _RichTextEditorController( 56 | text: text, 57 | deltas: deltas?.copy ?? this.deltas.copy, 58 | ) 59 | ..value = value ?? this.value 60 | ..metadata = metadata ?? this.metadata; 61 | } 62 | 63 | _RichTextEditorController({ 64 | super.text, 65 | TextDeltas? deltas, 66 | TextMetadata? metaData, 67 | }) : _metadata = metaData ?? RichTextEditorController.defaultMetadata, 68 | deltas = deltas ?? 69 | (text == null ? [] : TextDeltasUtils.deltasFromString(text)) { 70 | addListener(_internalControllerListener); 71 | } 72 | 73 | void _internalControllerListener() { 74 | TextDeltas newDeltas = _compareNewStringAndOldTextDeltasForChanges( 75 | text, 76 | deltas.copy, 77 | ); 78 | 79 | if (isListMode && newDeltas.length != deltas.length) { 80 | newDeltas = modifyDeltasForBulletListChange( 81 | newDeltas, 82 | deltas.copy, 83 | ); 84 | } 85 | 86 | setDeltas(newDeltas); 87 | } 88 | 89 | /// set the new deltas and reset [metadata] relative to the new selection/caret position 90 | void setDeltas(TextDeltas newDeltas) { 91 | deltas.clear(); 92 | deltas.addAll(newDeltas); 93 | if (selection.isCollapsed) resetMetadataOnSelectionCollapsed(); 94 | } 95 | 96 | /// If a selection changed and is inside the text and empty (collapsed), 97 | /// this function sets the current [metadata] to the metadata of the 98 | /// text before it's new position 99 | void resetMetadataOnSelectionCollapsed() { 100 | if (!selection.isCollapsed) return; 101 | if (selection.end == text.length || textBeforeSelection().isNullOrEmpty) { 102 | return; 103 | } 104 | if (_metadataToggled) return; 105 | 106 | final TextMetadata newMetadata = (deltas.isNotEmpty 107 | ? deltas[text.indexOf(selection.textBefore(text).chars.last)] 108 | .metadata 109 | : metadata) ?? 110 | metadata ?? 111 | RichTextEditorController.defaultMetadata; 112 | 113 | _metadata = _metadata?.combineWith( 114 | newMetadata, 115 | favourOther: true, 116 | ) ?? 117 | newMetadata; 118 | } 119 | 120 | String? textBeforeSelection() { 121 | try { 122 | return selection.textBefore(text); 123 | } catch (e) { 124 | return null; 125 | } 126 | } 127 | 128 | static const String bulletPoint = '•'; 129 | 130 | List modifyDeltasForBulletListChange( 131 | List modifiedDeltas, 132 | List oldDeltas, 133 | ) { 134 | final List oldChars = oldDeltas.text.characters.toList(); 135 | final List newChars = modifiedDeltas.text.characters.toList(); 136 | 137 | if (oldChars.length > newChars.length) return modifiedDeltas; 138 | 139 | if (newChars.last == '\n') { 140 | const String bulletValue = '\n $bulletPoint '; 141 | 142 | final TextDeltas bulletDeltas = List.generate( 143 | bulletValue.length, 144 | (index) => TextDelta( 145 | char: bulletValue[index], 146 | 147 | /// adding this check so that the character typed after this does not inherit the bullet point's metadata 148 | /// hence the restoration back to the [this] controller's [metadata] 149 | metadata: (index != bulletValue.length - 2) 150 | ? metadata 151 | : (metadata ?? RichTextEditorController.defaultMetadata).copyWith( 152 | fontWeight: FontWeight.bold, 153 | ), 154 | ), 155 | ); 156 | 157 | final TextDeltas deltas = modifiedDeltas.copy 158 | ..replaceRange( 159 | modifiedDeltas.length - 1, 160 | modifiedDeltas.length, 161 | bulletDeltas, 162 | ); 163 | 164 | text = deltas.text; 165 | selection = TextSelection.collapsed(offset: text.length); 166 | 167 | return deltas; 168 | } 169 | 170 | return modifiedDeltas; 171 | } 172 | 173 | /// this compares the old strings and the new strings by the following criteria 174 | /// * 175 | TextDeltas _compareNewStringAndOldTextDeltasForChanges( 176 | String text, 177 | TextDeltas oldDeltas, 178 | ) { 179 | if (text.isEmpty) return []; 180 | final TextDeltas newDeltas = oldDeltas.copy; 181 | final int minLength = min(text.length, oldDeltas.length); 182 | final int maxLength = max(text.length, oldDeltas.length); 183 | 184 | final int indexOfChange = _compareNewAndOldForChangeIndex( 185 | text: text, 186 | oldDeltas: oldDeltas, 187 | minLength: minLength, 188 | ); 189 | 190 | if (indexOfChange == -1) return newDeltas; 191 | 192 | final int lengthOfChange = maxLength - minLength; 193 | 194 | if (minLength == oldDeltas.length) { 195 | final TextMetadata newMetadata = metadataToggled 196 | ? (metadata ?? 197 | oldDeltas.elementAtOrNull(indexOfChange - 1)?.metadata ?? 198 | RichTextEditorController.defaultMetadata) 199 | : oldDeltas.elementAtOrNull(indexOfChange - 1)?.metadata ?? 200 | metadata ?? 201 | RichTextEditorController.defaultMetadata; 202 | final int iterationLimit = lengthOfChange + indexOfChange; 203 | for (int i = indexOfChange; i < iterationLimit; i++) { 204 | newDeltas.insert(i, TextDelta(char: text[i], metadata: newMetadata)); 205 | } 206 | } else { 207 | newDeltas.removeRange(indexOfChange, indexOfChange + lengthOfChange); 208 | } 209 | return newDeltas; 210 | } 211 | 212 | int _compareNewAndOldForChangeIndex({ 213 | required String text, 214 | required TextDeltas oldDeltas, 215 | required int minLength, 216 | }) { 217 | if (text.isEmpty || oldDeltas.isEmpty) return 0; 218 | for (int i = 0; i < minLength; i++) { 219 | final String newChar = text[i]; 220 | final String oldChar = oldDeltas[i].char; 221 | if (newChar != oldChar) return i; 222 | } 223 | 224 | if (text.length == oldDeltas.length) return -1; 225 | 226 | return minLength; 227 | } 228 | 229 | void applyDefaultMetadataChange(TextMetadata changedMetadata) { 230 | metadata = changedMetadata; 231 | } 232 | 233 | bool get isListMode => indexOflListChar != null; 234 | 235 | int? indexOflListChar; 236 | 237 | void toggleListMode() { 238 | indexOflListChar = indexOflListChar == null ? (deltas.length - 1) : null; 239 | _metadataToggled = true; 240 | notifyListeners(); 241 | } 242 | 243 | void changeStyleOnSelectionChange({ 244 | TextMetadata? changedMetadata, 245 | required TextMetadataChange change, 246 | required TextDeltas modifiedDeltas, 247 | required TextSelection selection, 248 | }) { 249 | if (!selection.isValid) return; 250 | changedMetadata ??= 251 | deltas[text.indexOf(selection.textBefore(text).chars.last)].metadata ?? 252 | metadata ?? 253 | RichTextEditorController.defaultMetadata; 254 | 255 | _metadata = _metadata?.combineWhatChanged( 256 | change, 257 | changedMetadata, 258 | ) ?? 259 | changedMetadata; 260 | 261 | metadataToggled = true; 262 | 263 | if (selection.isCollapsed) return notifyListeners(); 264 | 265 | setDeltas( 266 | applyMetadataToTextInSelection( 267 | newMetadata: changedMetadata, 268 | change: change, 269 | deltas: modifiedDeltas, 270 | selection: selection, 271 | ), 272 | ); 273 | notifyListeners(); 274 | } 275 | 276 | /// Applies the [newMetadata] to the [deltas] in the [selection] by the [change]. 277 | /// 278 | /// use [TextMetadataChange.all] to apply change to more than one metadata field change. 279 | TextDeltas applyMetadataToTextInSelection({ 280 | required TextMetadata newMetadata, 281 | required TextDeltas deltas, 282 | required TextMetadataChange change, 283 | required TextSelection selection, 284 | }) { 285 | final TextDeltas modifiedDeltas = deltas.copy; 286 | 287 | final int start = selection.start; 288 | final int end = selection.end; 289 | 290 | for (int i = start; i < end; i++) { 291 | modifiedDeltas[i] = modifiedDeltas[i].copyWith( 292 | metadata: modifiedDeltas[i].metadata?.combineWhatChanged( 293 | change, 294 | newMetadata, 295 | ) ?? 296 | newMetadata, 297 | ); 298 | } 299 | return modifiedDeltas; 300 | } 301 | 302 | /// Toggles the [TextMetadata.fontWeight] between [FontWeight.normal] and [FontWeight.w700]. 303 | void toggleBold() { 304 | final TextMetadata tempMetadata = 305 | metadata ?? RichTextEditorController.defaultMetadata; 306 | final TextMetadata changedMetadata = tempMetadata.copyWith( 307 | fontWeight: tempMetadata.fontWeight == FontWeight.normal 308 | ? FontWeight.w700 309 | : FontWeight.normal, 310 | ); 311 | 312 | changeStyleOnSelectionChange( 313 | changedMetadata: changedMetadata, 314 | change: TextMetadataChange.fontWeight, 315 | modifiedDeltas: deltas.copy, 316 | selection: selection.copyWith(), 317 | ); 318 | } 319 | 320 | /// Toggles the [TextMetadata.fontStyle] between [FontStyle.normal] and [FontStyle.italic]. 321 | void toggleItalic() { 322 | final TextMetadata tempMetadata = 323 | metadata ?? RichTextEditorController.defaultMetadata; 324 | 325 | final TextMetadata changedMetadata = tempMetadata.copyWith( 326 | fontStyle: tempMetadata.fontStyle == FontStyle.italic 327 | ? FontStyle.normal 328 | : FontStyle.italic, 329 | ); 330 | changeStyleOnSelectionChange( 331 | changedMetadata: changedMetadata, 332 | change: TextMetadataChange.fontStyle, 333 | modifiedDeltas: deltas.copy, 334 | selection: selection, 335 | ); 336 | } 337 | 338 | /// Toggles the [TextMetadata.decoration] between [TextDecorationEnum.none] and [TextDecorationEnum.underline]. 339 | void toggleUnderline() { 340 | final TextMetadata tempMetadata = 341 | metadata ?? RichTextEditorController.defaultMetadata; 342 | 343 | final TextMetadata changedMetadata = tempMetadata.copyWith( 344 | decoration: tempMetadata.decoration == TextDecorationEnum.underline 345 | ? TextDecorationEnum.none 346 | : TextDecorationEnum.underline, 347 | ); 348 | 349 | changeStyleOnSelectionChange( 350 | changedMetadata: changedMetadata, 351 | change: TextMetadataChange.fontDecoration, 352 | modifiedDeltas: deltas.copy, 353 | selection: selection, 354 | ); 355 | } 356 | 357 | /// Toggles the [TextMetadata.decoration] between [TextDecorationEnum.none] and [TextDecorationEnum.lineThrough]. 358 | void toggleSuperscript() { 359 | final TextMetadata tempMetadata = 360 | metadata ?? RichTextEditorController.defaultMetadata; 361 | 362 | final TextMetadata changedMetadata = tempMetadata.copyWith( 363 | fontFeatures: tempMetadata.fontFeatures?.firstOrNull == 364 | const FontFeature.superscripts() 365 | ? const [] 366 | : const [FontFeature.superscripts()], 367 | ); 368 | 369 | changeStyleOnSelectionChange( 370 | changedMetadata: changedMetadata, 371 | change: TextMetadataChange.fontFeatures, 372 | modifiedDeltas: deltas.copy, 373 | selection: selection, 374 | ); 375 | } 376 | 377 | /// Toggles the [TextMetadata.fontFeatures] between empty list and [FontFeature.subscripts()]. 378 | void toggleSubscript() { 379 | final TextMetadata tempMetadata = 380 | metadata ?? RichTextEditorController.defaultMetadata; 381 | 382 | final TextMetadata changedMetadata = tempMetadata.copyWith( 383 | fontFeatures: tempMetadata.fontFeatures?.firstOrNull == 384 | const FontFeature.subscripts() 385 | ? const [] 386 | : const [FontFeature.subscripts()], 387 | ); 388 | 389 | changeStyleOnSelectionChange( 390 | changedMetadata: changedMetadata, 391 | change: TextMetadataChange.fontFeatures, 392 | modifiedDeltas: deltas.copy, 393 | selection: selection, 394 | ); 395 | } 396 | 397 | void changeColor(Color color) { 398 | final TextMetadata changedMetadata = 399 | (metadata ?? RichTextEditorController.defaultMetadata).copyWith( 400 | color: color, 401 | ); 402 | changeStyleOnSelectionChange( 403 | changedMetadata: changedMetadata, 404 | change: TextMetadataChange.fontStyle, 405 | modifiedDeltas: deltas.copy, 406 | selection: selection, 407 | ); 408 | } 409 | 410 | void changeFontSize(double fontSize) { 411 | final TextMetadata changedMetadata = 412 | (metadata ?? RichTextEditorController.defaultMetadata).copyWith( 413 | fontSize: fontSize, 414 | ); 415 | changeStyleOnSelectionChange( 416 | changedMetadata: changedMetadata, 417 | change: TextMetadataChange.fontSize, 418 | modifiedDeltas: deltas.copy, 419 | selection: selection, 420 | ); 421 | } 422 | 423 | /// Changes the [TextMetadata.alignment] to the given [alignment]. 424 | /// 425 | /// note that you have to use [RichTextField] for changes made by this method to reflect. 426 | /// or otherwise set the [TextField.alignment] parameter of your textField to [TextMetadata.alignment] 427 | /// while listening to changes in the controller. 428 | /// example: 429 | ///... 430 | /// ValueListenableBuilder( 431 | /// valueListenable: controller, 432 | /// builder: (_, controllerValue, __) => TextField( 433 | /// controller: controller, 434 | /// textAlign: controller.metadata?.alignment ?? TextAlign.start, 435 | /// ), 436 | /// ), 437 | /// ... 438 | void changeAlignment(TextAlign alignment) { 439 | applyDefaultMetadataChange( 440 | (metadata ?? RichTextEditorController.defaultMetadata) 441 | .copyWith(alignment: alignment), 442 | ); 443 | } 444 | 445 | @override 446 | TextSpan buildTextSpan({ 447 | required BuildContext context, 448 | TextStyle? style, 449 | required bool withComposing, 450 | }) { 451 | final List spanChildren = []; 452 | 453 | for (final TextDelta delta in deltas) { 454 | spanChildren.add( 455 | TextSpan( 456 | text: delta.char, 457 | style: delta.metadata?.style ?? 458 | RichTextEditorController.defaultMetadata.style, 459 | ), 460 | ); 461 | } 462 | 463 | final TextSpan textSpan = TextSpan( 464 | style: metadata?.styleWithoutFontFeatures ?? style, 465 | children: spanChildren, 466 | ); 467 | return textSpan; 468 | } 469 | 470 | @override 471 | void dispose() { 472 | removeListener(_internalControllerListener); 473 | super.dispose(); 474 | } 475 | } 476 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 54; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; 11 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; 12 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; 13 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 14 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 15 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; 16 | /* End PBXBuildFile section */ 17 | 18 | /* Begin PBXCopyFilesBuildPhase section */ 19 | 9705A1C41CF9048500538489 /* Embed Frameworks */ = { 20 | isa = PBXCopyFilesBuildPhase; 21 | buildActionMask = 2147483647; 22 | dstPath = ""; 23 | dstSubfolderSpec = 10; 24 | files = ( 25 | ); 26 | name = "Embed Frameworks"; 27 | runOnlyForDeploymentPostprocessing = 0; 28 | }; 29 | /* End PBXCopyFilesBuildPhase section */ 30 | 31 | /* Begin PBXFileReference section */ 32 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 33 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; 34 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; 35 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 36 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; 37 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 38 | 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 39 | 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; 40 | 97C146EE1CF9000F007C117D /* Runner.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Runner.app; sourceTree = BUILT_PRODUCTS_DIR; }; 41 | 97C146FB1CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/Main.storyboard; sourceTree = ""; }; 42 | 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 43 | 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 44 | 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 45 | /* End PBXFileReference section */ 46 | 47 | /* Begin PBXFrameworksBuildPhase section */ 48 | 97C146EB1CF9000F007C117D /* Frameworks */ = { 49 | isa = PBXFrameworksBuildPhase; 50 | buildActionMask = 2147483647; 51 | files = ( 52 | ); 53 | runOnlyForDeploymentPostprocessing = 0; 54 | }; 55 | /* End PBXFrameworksBuildPhase section */ 56 | 57 | /* Begin PBXGroup section */ 58 | 9740EEB11CF90186004384FC /* Flutter */ = { 59 | isa = PBXGroup; 60 | children = ( 61 | 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */, 62 | 9740EEB21CF90195004384FC /* Debug.xcconfig */, 63 | 7AFA3C8E1D35360C0083082E /* Release.xcconfig */, 64 | 9740EEB31CF90195004384FC /* Generated.xcconfig */, 65 | ); 66 | name = Flutter; 67 | sourceTree = ""; 68 | }; 69 | 97C146E51CF9000F007C117D = { 70 | isa = PBXGroup; 71 | children = ( 72 | 9740EEB11CF90186004384FC /* Flutter */, 73 | 97C146F01CF9000F007C117D /* Runner */, 74 | 97C146EF1CF9000F007C117D /* Products */, 75 | ); 76 | sourceTree = ""; 77 | }; 78 | 97C146EF1CF9000F007C117D /* Products */ = { 79 | isa = PBXGroup; 80 | children = ( 81 | 97C146EE1CF9000F007C117D /* Runner.app */, 82 | ); 83 | name = Products; 84 | sourceTree = ""; 85 | }; 86 | 97C146F01CF9000F007C117D /* Runner */ = { 87 | isa = PBXGroup; 88 | children = ( 89 | 97C146FA1CF9000F007C117D /* Main.storyboard */, 90 | 97C146FD1CF9000F007C117D /* Assets.xcassets */, 91 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 92 | 97C147021CF9000F007C117D /* Info.plist */, 93 | 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 94 | 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 95 | 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, 96 | 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, 97 | ); 98 | path = Runner; 99 | sourceTree = ""; 100 | }; 101 | /* End PBXGroup section */ 102 | 103 | /* Begin PBXNativeTarget section */ 104 | 97C146ED1CF9000F007C117D /* Runner */ = { 105 | isa = PBXNativeTarget; 106 | buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; 107 | buildPhases = ( 108 | 9740EEB61CF901F6004384FC /* Run Script */, 109 | 97C146EA1CF9000F007C117D /* Sources */, 110 | 97C146EB1CF9000F007C117D /* Frameworks */, 111 | 97C146EC1CF9000F007C117D /* Resources */, 112 | 9705A1C41CF9048500538489 /* Embed Frameworks */, 113 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */, 114 | ); 115 | buildRules = ( 116 | ); 117 | dependencies = ( 118 | ); 119 | name = Runner; 120 | productName = Runner; 121 | productReference = 97C146EE1CF9000F007C117D /* Runner.app */; 122 | productType = "com.apple.product-type.application"; 123 | }; 124 | /* End PBXNativeTarget section */ 125 | 126 | /* Begin PBXProject section */ 127 | 97C146E61CF9000F007C117D /* Project object */ = { 128 | isa = PBXProject; 129 | attributes = { 130 | LastUpgradeCheck = 1300; 131 | ORGANIZATIONNAME = ""; 132 | TargetAttributes = { 133 | 97C146ED1CF9000F007C117D = { 134 | CreatedOnToolsVersion = 7.3.1; 135 | LastSwiftMigration = 1100; 136 | }; 137 | }; 138 | }; 139 | buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; 140 | compatibilityVersion = "Xcode 9.3"; 141 | developmentRegion = en; 142 | hasScannedForEncodings = 0; 143 | knownRegions = ( 144 | en, 145 | Base, 146 | ); 147 | mainGroup = 97C146E51CF9000F007C117D; 148 | productRefGroup = 97C146EF1CF9000F007C117D /* Products */; 149 | projectDirPath = ""; 150 | projectRoot = ""; 151 | targets = ( 152 | 97C146ED1CF9000F007C117D /* Runner */, 153 | ); 154 | }; 155 | /* End PBXProject section */ 156 | 157 | /* Begin PBXResourcesBuildPhase section */ 158 | 97C146EC1CF9000F007C117D /* Resources */ = { 159 | isa = PBXResourcesBuildPhase; 160 | buildActionMask = 2147483647; 161 | files = ( 162 | 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */, 163 | 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */, 164 | 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */, 165 | 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */, 166 | ); 167 | runOnlyForDeploymentPostprocessing = 0; 168 | }; 169 | /* End PBXResourcesBuildPhase section */ 170 | 171 | /* Begin PBXShellScriptBuildPhase section */ 172 | 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { 173 | isa = PBXShellScriptBuildPhase; 174 | alwaysOutOfDate = 1; 175 | buildActionMask = 2147483647; 176 | files = ( 177 | ); 178 | inputPaths = ( 179 | ); 180 | name = "Thin Binary"; 181 | outputPaths = ( 182 | ); 183 | runOnlyForDeploymentPostprocessing = 0; 184 | shellPath = /bin/sh; 185 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; 186 | }; 187 | 9740EEB61CF901F6004384FC /* Run Script */ = { 188 | isa = PBXShellScriptBuildPhase; 189 | alwaysOutOfDate = 1; 190 | buildActionMask = 2147483647; 191 | files = ( 192 | ); 193 | inputPaths = ( 194 | ); 195 | name = "Run Script"; 196 | outputPaths = ( 197 | ); 198 | runOnlyForDeploymentPostprocessing = 0; 199 | shellPath = /bin/sh; 200 | shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; 201 | }; 202 | /* End PBXShellScriptBuildPhase section */ 203 | 204 | /* Begin PBXSourcesBuildPhase section */ 205 | 97C146EA1CF9000F007C117D /* Sources */ = { 206 | isa = PBXSourcesBuildPhase; 207 | buildActionMask = 2147483647; 208 | files = ( 209 | 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 210 | 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, 211 | ); 212 | runOnlyForDeploymentPostprocessing = 0; 213 | }; 214 | /* End PBXSourcesBuildPhase section */ 215 | 216 | /* Begin PBXVariantGroup section */ 217 | 97C146FA1CF9000F007C117D /* Main.storyboard */ = { 218 | isa = PBXVariantGroup; 219 | children = ( 220 | 97C146FB1CF9000F007C117D /* Base */, 221 | ); 222 | name = Main.storyboard; 223 | sourceTree = ""; 224 | }; 225 | 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */ = { 226 | isa = PBXVariantGroup; 227 | children = ( 228 | 97C147001CF9000F007C117D /* Base */, 229 | ); 230 | name = LaunchScreen.storyboard; 231 | sourceTree = ""; 232 | }; 233 | /* End PBXVariantGroup section */ 234 | 235 | /* Begin XCBuildConfiguration section */ 236 | 249021D3217E4FDB00AE95B9 /* Profile */ = { 237 | isa = XCBuildConfiguration; 238 | buildSettings = { 239 | ALWAYS_SEARCH_USER_PATHS = NO; 240 | CLANG_ANALYZER_NONNULL = YES; 241 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 242 | CLANG_CXX_LIBRARY = "libc++"; 243 | CLANG_ENABLE_MODULES = YES; 244 | CLANG_ENABLE_OBJC_ARC = YES; 245 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 246 | CLANG_WARN_BOOL_CONVERSION = YES; 247 | CLANG_WARN_COMMA = YES; 248 | CLANG_WARN_CONSTANT_CONVERSION = YES; 249 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 250 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 251 | CLANG_WARN_EMPTY_BODY = YES; 252 | CLANG_WARN_ENUM_CONVERSION = YES; 253 | CLANG_WARN_INFINITE_RECURSION = YES; 254 | CLANG_WARN_INT_CONVERSION = YES; 255 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 256 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 257 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 258 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 259 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 260 | CLANG_WARN_STRICT_PROTOTYPES = YES; 261 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 262 | CLANG_WARN_UNREACHABLE_CODE = YES; 263 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 264 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 265 | COPY_PHASE_STRIP = NO; 266 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 267 | ENABLE_NS_ASSERTIONS = NO; 268 | ENABLE_STRICT_OBJC_MSGSEND = YES; 269 | GCC_C_LANGUAGE_STANDARD = gnu99; 270 | GCC_NO_COMMON_BLOCKS = YES; 271 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 272 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 273 | GCC_WARN_UNDECLARED_SELECTOR = YES; 274 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 275 | GCC_WARN_UNUSED_FUNCTION = YES; 276 | GCC_WARN_UNUSED_VARIABLE = YES; 277 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 278 | MTL_ENABLE_DEBUG_INFO = NO; 279 | SDKROOT = iphoneos; 280 | SUPPORTED_PLATFORMS = iphoneos; 281 | TARGETED_DEVICE_FAMILY = "1,2"; 282 | VALIDATE_PRODUCT = YES; 283 | }; 284 | name = Profile; 285 | }; 286 | 249021D4217E4FDB00AE95B9 /* Profile */ = { 287 | isa = XCBuildConfiguration; 288 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 289 | buildSettings = { 290 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 291 | CLANG_ENABLE_MODULES = YES; 292 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 293 | DEVELOPMENT_TEAM = WY2BRN5K55; 294 | ENABLE_BITCODE = NO; 295 | INFOPLIST_FILE = Runner/Info.plist; 296 | LD_RUNPATH_SEARCH_PATHS = ( 297 | "$(inherited)", 298 | "@executable_path/Frameworks", 299 | ); 300 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 301 | PRODUCT_NAME = "$(TARGET_NAME)"; 302 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 303 | SWIFT_VERSION = 5.0; 304 | VERSIONING_SYSTEM = "apple-generic"; 305 | }; 306 | name = Profile; 307 | }; 308 | 97C147031CF9000F007C117D /* Debug */ = { 309 | isa = XCBuildConfiguration; 310 | buildSettings = { 311 | ALWAYS_SEARCH_USER_PATHS = NO; 312 | CLANG_ANALYZER_NONNULL = YES; 313 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 314 | CLANG_CXX_LIBRARY = "libc++"; 315 | CLANG_ENABLE_MODULES = YES; 316 | CLANG_ENABLE_OBJC_ARC = YES; 317 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 318 | CLANG_WARN_BOOL_CONVERSION = YES; 319 | CLANG_WARN_COMMA = YES; 320 | CLANG_WARN_CONSTANT_CONVERSION = YES; 321 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 322 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 323 | CLANG_WARN_EMPTY_BODY = YES; 324 | CLANG_WARN_ENUM_CONVERSION = YES; 325 | CLANG_WARN_INFINITE_RECURSION = YES; 326 | CLANG_WARN_INT_CONVERSION = YES; 327 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 328 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 329 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 330 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 331 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 332 | CLANG_WARN_STRICT_PROTOTYPES = YES; 333 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 334 | CLANG_WARN_UNREACHABLE_CODE = YES; 335 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 336 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 337 | COPY_PHASE_STRIP = NO; 338 | DEBUG_INFORMATION_FORMAT = dwarf; 339 | ENABLE_STRICT_OBJC_MSGSEND = YES; 340 | ENABLE_TESTABILITY = YES; 341 | GCC_C_LANGUAGE_STANDARD = gnu99; 342 | GCC_DYNAMIC_NO_PIC = NO; 343 | GCC_NO_COMMON_BLOCKS = YES; 344 | GCC_OPTIMIZATION_LEVEL = 0; 345 | GCC_PREPROCESSOR_DEFINITIONS = ( 346 | "DEBUG=1", 347 | "$(inherited)", 348 | ); 349 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 350 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 351 | GCC_WARN_UNDECLARED_SELECTOR = YES; 352 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 353 | GCC_WARN_UNUSED_FUNCTION = YES; 354 | GCC_WARN_UNUSED_VARIABLE = YES; 355 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 356 | MTL_ENABLE_DEBUG_INFO = YES; 357 | ONLY_ACTIVE_ARCH = YES; 358 | SDKROOT = iphoneos; 359 | TARGETED_DEVICE_FAMILY = "1,2"; 360 | }; 361 | name = Debug; 362 | }; 363 | 97C147041CF9000F007C117D /* Release */ = { 364 | isa = XCBuildConfiguration; 365 | buildSettings = { 366 | ALWAYS_SEARCH_USER_PATHS = NO; 367 | CLANG_ANALYZER_NONNULL = YES; 368 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 369 | CLANG_CXX_LIBRARY = "libc++"; 370 | CLANG_ENABLE_MODULES = YES; 371 | CLANG_ENABLE_OBJC_ARC = YES; 372 | CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; 373 | CLANG_WARN_BOOL_CONVERSION = YES; 374 | CLANG_WARN_COMMA = YES; 375 | CLANG_WARN_CONSTANT_CONVERSION = YES; 376 | CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; 377 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 378 | CLANG_WARN_EMPTY_BODY = YES; 379 | CLANG_WARN_ENUM_CONVERSION = YES; 380 | CLANG_WARN_INFINITE_RECURSION = YES; 381 | CLANG_WARN_INT_CONVERSION = YES; 382 | CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; 383 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; 384 | CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; 385 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 386 | CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; 387 | CLANG_WARN_STRICT_PROTOTYPES = YES; 388 | CLANG_WARN_SUSPICIOUS_MOVE = YES; 389 | CLANG_WARN_UNREACHABLE_CODE = YES; 390 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 391 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 392 | COPY_PHASE_STRIP = NO; 393 | DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; 394 | ENABLE_NS_ASSERTIONS = NO; 395 | ENABLE_STRICT_OBJC_MSGSEND = YES; 396 | GCC_C_LANGUAGE_STANDARD = gnu99; 397 | GCC_NO_COMMON_BLOCKS = YES; 398 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 399 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 400 | GCC_WARN_UNDECLARED_SELECTOR = YES; 401 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 402 | GCC_WARN_UNUSED_FUNCTION = YES; 403 | GCC_WARN_UNUSED_VARIABLE = YES; 404 | IPHONEOS_DEPLOYMENT_TARGET = 11.0; 405 | MTL_ENABLE_DEBUG_INFO = NO; 406 | SDKROOT = iphoneos; 407 | SUPPORTED_PLATFORMS = iphoneos; 408 | SWIFT_COMPILATION_MODE = wholemodule; 409 | SWIFT_OPTIMIZATION_LEVEL = "-O"; 410 | TARGETED_DEVICE_FAMILY = "1,2"; 411 | VALIDATE_PRODUCT = YES; 412 | }; 413 | name = Release; 414 | }; 415 | 97C147061CF9000F007C117D /* Debug */ = { 416 | isa = XCBuildConfiguration; 417 | baseConfigurationReference = 9740EEB21CF90195004384FC /* Debug.xcconfig */; 418 | buildSettings = { 419 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 420 | CLANG_ENABLE_MODULES = YES; 421 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 422 | DEVELOPMENT_TEAM = WY2BRN5K55; 423 | ENABLE_BITCODE = NO; 424 | INFOPLIST_FILE = Runner/Info.plist; 425 | LD_RUNPATH_SEARCH_PATHS = ( 426 | "$(inherited)", 427 | "@executable_path/Frameworks", 428 | ); 429 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 430 | PRODUCT_NAME = "$(TARGET_NAME)"; 431 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 432 | SWIFT_OPTIMIZATION_LEVEL = "-Onone"; 433 | SWIFT_VERSION = 5.0; 434 | VERSIONING_SYSTEM = "apple-generic"; 435 | }; 436 | name = Debug; 437 | }; 438 | 97C147071CF9000F007C117D /* Release */ = { 439 | isa = XCBuildConfiguration; 440 | baseConfigurationReference = 7AFA3C8E1D35360C0083082E /* Release.xcconfig */; 441 | buildSettings = { 442 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 443 | CLANG_ENABLE_MODULES = YES; 444 | CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; 445 | DEVELOPMENT_TEAM = WY2BRN5K55; 446 | ENABLE_BITCODE = NO; 447 | INFOPLIST_FILE = Runner/Info.plist; 448 | LD_RUNPATH_SEARCH_PATHS = ( 449 | "$(inherited)", 450 | "@executable_path/Frameworks", 451 | ); 452 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example; 453 | PRODUCT_NAME = "$(TARGET_NAME)"; 454 | SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; 455 | SWIFT_VERSION = 5.0; 456 | VERSIONING_SYSTEM = "apple-generic"; 457 | }; 458 | name = Release; 459 | }; 460 | /* End XCBuildConfiguration section */ 461 | 462 | /* Begin XCConfigurationList section */ 463 | 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { 464 | isa = XCConfigurationList; 465 | buildConfigurations = ( 466 | 97C147031CF9000F007C117D /* Debug */, 467 | 97C147041CF9000F007C117D /* Release */, 468 | 249021D3217E4FDB00AE95B9 /* Profile */, 469 | ); 470 | defaultConfigurationIsVisible = 0; 471 | defaultConfigurationName = Release; 472 | }; 473 | 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */ = { 474 | isa = XCConfigurationList; 475 | buildConfigurations = ( 476 | 97C147061CF9000F007C117D /* Debug */, 477 | 97C147071CF9000F007C117D /* Release */, 478 | 249021D4217E4FDB00AE95B9 /* Profile */, 479 | ); 480 | defaultConfigurationIsVisible = 0; 481 | defaultConfigurationName = Release; 482 | }; 483 | /* End XCConfigurationList section */ 484 | }; 485 | rootObject = 97C146E61CF9000F007C117D /* Project object */; 486 | } 487 | -------------------------------------------------------------------------------- /example/macos/Runner/Base.lproj/MainMenu.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | 331 | 332 | 333 | 334 | 335 | 336 | 337 | 338 | 339 | 340 | 341 | 342 | 343 | 344 | --------------------------------------------------------------------------------