diff --git a/packages/json_field_editor/.gitignore b/packages/json_field_editor/.gitignore new file mode 100644 index 00000000..905cffba --- /dev/null +++ b/packages/json_field_editor/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +build/ +coverage/ +.vscode/ diff --git a/packages/json_field_editor/.pubignore b/packages/json_field_editor/.pubignore new file mode 100644 index 00000000..eeb9cdb0 --- /dev/null +++ b/packages/json_field_editor/.pubignore @@ -0,0 +1,8 @@ +pubspec.lock +melos_json_field_editor.iml +.dart_tool/ +build/ +coverage/ +test/ +asset/ +pubspec_overrides.yaml diff --git a/packages/json_field_editor/CHANGELOG.md b/packages/json_field_editor/CHANGELOG.md new file mode 100644 index 00000000..57e13def --- /dev/null +++ b/packages/json_field_editor/CHANGELOG.md @@ -0,0 +1,3 @@ +## 1.2.0 + +- Initial release. diff --git a/packages/json_field_editor/LICENSE b/packages/json_field_editor/LICENSE new file mode 100644 index 00000000..4f5cedc9 --- /dev/null +++ b/packages/json_field_editor/LICENSE @@ -0,0 +1,225 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2025 Ashita Prasad, Ankit Mahato + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. + +---------------------------------------------------------------------------- + +MIT License + +Copyright (c) 2023 Antonio Jesús Caballero Encinas + +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: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +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. diff --git a/packages/json_field_editor/README.md b/packages/json_field_editor/README.md new file mode 100644 index 00000000..2ca7bf1a --- /dev/null +++ b/packages/json_field_editor/README.md @@ -0,0 +1,92 @@ +# json_field_editor + +A comprehensive package designed for editing JSON text directly within a TextField. This tool integrates seamlessly with Flutter, providing a rich and customizable experience for JSON manipulation. + +It is a fork of `json_text_field` which includes various improvements. + +## Key Features + +- **Dynamic JSON Value Highlighting**: Effortlessly distinguish between different JSON elements like keys, strings, numbers, booleans, nulls, and special characters through customizable text styles. +- **Validation and Error Display**: Instantly validates JSON strings, highlighting errors for quick fixes. +- **Intuitive JSON Formatting**: Automatically formats JSON strings for better readability, with options for sorting and custom controllers. +- **Seamless Integration**: Easy to incorporate into existing Flutter projects, requiring no additional platform-specific configurations. + +## Getting Started + +### Adding the Package + +Include `json_field_editor` in your project by adding it as a dependency in your `pubspec.yaml` file. For detailed instructions, visit [Flutter's plugin guide](https://flutter.io/platform-plugins/). + +### Importing the Package + +```dart +import 'package:json_field_editor/json_field_editor.dart'; +``` + +## Basic Usage + +Replace the standard `TextField` widget with `JsonField` to enable JSON editing capabilities. + +```dart +const JsonField(), + +``` + +### Customizable Features + +`JsonField` extends the familiar `TextField` widget, adding unique properties for a tailored JSON editing experience: + +- **Custom Highlight Styles**: Personalize the appearance for different JSON elements: + - `keyHighlightStyle`: Style for JSON keys. + - `stringHighlightStyle`: Style for string values. + - `numberHighlightStyle`: Style for number values. + - `booleanHighlightStyle`: Style for boolean values. + - `nullHighlightStyle`: Style for null values. + - `specialCharacterHighlightStyle`: Style for special characters. + +### Example Configuration + +```dart +JsonField( + keyHighlightStyle: TextStyle(color: Colors.red), + stringHighlightStyle: TextStyle(color: Colors.green), + numberHighlightStyle: TextStyle(color: Colors.blue), + booleanHighlightStyle: TextStyle(color: Colors.yellow), + nullHighlightStyle: TextStyle(color: Colors.purple), + specialCharacterHighlightStyle: TextStyle(color: Colors.orange), +), +``` + +- **Custom Error Styles**: Personalize the appearance for different JSON errors: + + - `errorTextStyle`: Style for JSON errors text. + - `errorContainerDecoration`: Decoration for JSON errors container. + +- **Formatting and Sorting**: Enable or disable automatic formatting and sorting of the JSON string. + + - `isFormatting`: Toggle JSON string formatting. + - `showErrorMessage`: Show or hide error messages. + + ### Controller Usage + +`JsonField` utilizes `JsonTextFieldController`, an enhanced version of `TextEditingController`, with an additional method for JSON formatting and sorting. + +```dart +final JsonTextFieldController controller = JsonTextFieldController(); + +Column( + children: [ + JsonField( + controller: controller, + isFormatting: true, + showErrorMessage: false, + ), + ElevatedButton( + onPressed: () => controller.formatJson(sortJson: true), + child: const Text('Format Json (sort)'), + ), + ], +) +``` + +Explore the complete example in the `example` folder. diff --git a/packages/json_field_editor/analysis_options.yaml b/packages/json_field_editor/analysis_options.yaml new file mode 100644 index 00000000..a5744c1c --- /dev/null +++ b/packages/json_field_editor/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/json_field_editor/example/.gitignore b/packages/json_field_editor/example/.gitignore new file mode 100644 index 00000000..79c113f9 --- /dev/null +++ b/packages/json_field_editor/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/packages/json_field_editor/example/README.md b/packages/json_field_editor/example/README.md new file mode 100644 index 00000000..2b3fce4c --- /dev/null +++ b/packages/json_field_editor/example/README.md @@ -0,0 +1,16 @@ +# example + +A new Flutter project. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/packages/json_field_editor/example/analysis_options.yaml b/packages/json_field_editor/example/analysis_options.yaml new file mode 100644 index 00000000..0d290213 --- /dev/null +++ b/packages/json_field_editor/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/packages/json_field_editor/example/lib/main.dart b/packages/json_field_editor/example/lib/main.dart new file mode 100644 index 00000000..7edde6f7 --- /dev/null +++ b/packages/json_field_editor/example/lib/main.dart @@ -0,0 +1,99 @@ +import 'package:flutter/material.dart'; +import 'package:json_field_editor/json_field_editor.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + final controller = JsonTextFieldController(); + bool isFormating = true; + + // This widget is the root of your application. + @override + Widget build(BuildContext context) { + return MaterialApp( + debugShowCheckedModeBanner: false, + title: 'Flutter Demo', + theme: ThemeData( + colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), + useMaterial3: true, + ), + home: Scaffold( + appBar: AppBar( + title: const Center(child: Text('JSON Text Field Example')), + ), + body: Center( + child: Column( + children: [ + const SizedBox(height: 50), + SizedBox( + width: 300, + height: 300, + child: JsonField( + onError: (error) => debugPrint(error), + showErrorMessage: true, + controller: controller, + isFormatting: isFormating, + keyboardType: TextInputType.multiline, + expands: true, + maxLines: null, + textAlignVertical: TextAlignVertical.top, + onChanged: (value) {}, + decoration: InputDecoration( + hintText: "Enter JSON", + hintStyle: TextStyle( + color: Theme.of( + context, + ).colorScheme.outline.withOpacity(0.6), + ), + focusedBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide( + color: Theme.of( + context, + ).colorScheme.primary.withOpacity(0.6), + ), + ), + enabledBorder: OutlineInputBorder( + borderRadius: const BorderRadius.all(Radius.circular(8)), + borderSide: BorderSide( + color: Theme.of(context).colorScheme.surfaceVariant, + ), + ), + filled: true, + ), + ), + ), + ElevatedButton( + onPressed: () => controller.formatJson(sortJson: false), + child: const Text('Format JSON'), + ), + ElevatedButton( + onPressed: () => controller.formatJson(sortJson: true), + child: const Text('Format JSON (sort)'), + ), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Format as JSON'), + Switch( + value: isFormating, + onChanged: (value) => setState(() => isFormating = value), + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/packages/json_field_editor/example/pubspec.lock b/packages/json_field_editor/example/pubspec.lock new file mode 100644 index 00000000..3f009597 --- /dev/null +++ b/packages/json_field_editor/example/pubspec.lock @@ -0,0 +1,236 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + url: "https://pub.dev" + source: hosted + version: "2.12.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: d57953e10f9f8327ce64a508a355f0b1ec902193f66288e8cb5070e7c47eeb2d + url: "https://pub.dev" + source: hosted + version: "1.0.6" + extended_text_field: + dependency: transitive + description: + name: extended_text_field + sha256: "3996195c117c6beb734026a7bc0ba80d7e4e84e4edd4728caa544d8209ab4d7d" + url: "https://pub.dev" + source: hosted + version: "16.0.2" + extended_text_library: + dependency: transitive + description: + name: extended_text_library + sha256: "55d09098ec56fab0d9a8a68950ca0bbf2efa1327937f7cec6af6dfa066234829" + url: "https://pub.dev" + source: hosted + version: "12.0.0" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + url: "https://pub.dev" + source: hosted + version: "1.3.2" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04 + url: "https://pub.dev" + source: hosted + version: "2.0.3" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + json_field_editor: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "1.2.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + url: "https://pub.dev" + source: hosted + version: "10.0.8" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + url: "https://pub.dev" + source: hosted + version: "3.0.9" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "6ba465d5d76e67ddf503e1161d1f4a6bc42306f9d66ca1e8f079a47290fb06d3" + url: "https://pub.dev" + source: hosted + version: "3.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452" + url: "https://pub.dev" + source: hosted + version: "2.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + url: "https://pub.dev" + source: hosted + version: "0.7.4" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + url: "https://pub.dev" + source: hosted + version: "14.3.1" +sdks: + dart: ">=3.7.0 <4.0.0" + flutter: ">=3.24.0" diff --git a/packages/json_field_editor/example/pubspec.yaml b/packages/json_field_editor/example/pubspec.yaml new file mode 100644 index 00000000..6ef48df9 --- /dev/null +++ b/packages/json_field_editor/example/pubspec.yaml @@ -0,0 +1,90 @@ +name: example +description: "A new Flutter project." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: "none" # Remove this line if you wish to publish to pub.dev + +# The following defines the version and build number for your application. +# A version number is three numbers separated by dots, like 1.2.43 +# followed by an optional build number separated by a +. +# Both the version and the builder number may be overridden in flutter +# build by specifying --build-name and --build-number, respectively. +# In Android, build-name is used as versionName while build-number used as versionCode. +# Read more about Android versioning at https://developer.android.com/studio/publish/versioning +# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion. +# Read more about iOS versioning at +# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html +# In Windows, build-name is used as the major, minor, and patch parts +# of the product and file versions while build-number is used as the build suffix. +version: 1.0.0+1 + +environment: + sdk: ">=3.2.0 <4.0.0" + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.2 + json_field_editor: + path: ../ + +dev_dependencies: + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^2.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/assets-and-images/#resolution-aware + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/assets-and-images/#from-packages + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/custom-fonts/#from-packages diff --git a/packages/json_field_editor/lib/json_field_editor.dart b/packages/json_field_editor/lib/json_field_editor.dart new file mode 100644 index 00000000..9489d457 --- /dev/null +++ b/packages/json_field_editor/lib/json_field_editor.dart @@ -0,0 +1,4 @@ +library json_field_editor; + +export 'src/json_field.dart'; +export 'src/json_text_field_controller.dart'; diff --git a/packages/json_field_editor/lib/src/error_message_container.dart b/packages/json_field_editor/lib/src/error_message_container.dart new file mode 100644 index 00000000..7160bdae --- /dev/null +++ b/packages/json_field_editor/lib/src/error_message_container.dart @@ -0,0 +1,36 @@ +import 'package:flutter/material.dart'; + +class ErrorMessageContainer extends StatelessWidget { + const ErrorMessageContainer({ + super.key, + required this.jsonError, + required this.errorTextStyle, + this.decoration, + }); + + final String? jsonError; + final TextStyle errorTextStyle; + final BoxDecoration? decoration; + + @override + Widget build(BuildContext context) { + return AnimatedSwitcher( + duration: const Duration(milliseconds: 400), + child: + jsonError == null + ? const SizedBox.shrink() + : Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + constraints: const BoxConstraints(minHeight: 40, maxHeight: 60), + decoration: decoration, + child: Center( + child: Text( + jsonError ?? '', + style: const TextStyle(color: Colors.red), + ), + ), + ), + ); + } +} diff --git a/packages/json_field_editor/lib/src/extensions/text_editing_controller_extension.dart b/packages/json_field_editor/lib/src/extensions/text_editing_controller_extension.dart new file mode 100644 index 00000000..a98fae66 --- /dev/null +++ b/packages/json_field_editor/lib/src/extensions/text_editing_controller_extension.dart @@ -0,0 +1,18 @@ +import 'dart:math' as math; + +import 'package:flutter/material.dart'; + +extension TextEditingControllerExtension on TextEditingController { + void insert(String string) { + int offset = math.min(selection.baseOffset, selection.extentOffset); + String text = + this.text.substring(0, offset) + string + this.text.substring(offset); + value = TextEditingValue( + text: text, + selection: selection.copyWith( + baseOffset: selection.baseOffset + string.length, + extentOffset: selection.extentOffset + string.length, + ), + ); + } +} diff --git a/packages/json_field_editor/lib/src/json_field.dart b/packages/json_field_editor/lib/src/json_field.dart new file mode 100644 index 00000000..8cda1005 --- /dev/null +++ b/packages/json_field_editor/lib/src/json_field.dart @@ -0,0 +1,271 @@ +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:json_field_editor/json_field_editor.dart'; +import 'package:json_field_editor/src/error_message_container.dart'; +import 'package:json_field_editor/src/json_highlight/json_highlight.dart'; +import 'package:json_field_editor/src/json_utils.dart'; + +class JsonField extends ExtendedTextField { + @override + Type get runtimeType => EditableText; + + const JsonField({ + super.key, + super.autocorrect, + super.autofillHints, + super.autofocus, + super.buildCounter, + super.canRequestFocus, + super.clipBehavior, + this.controller, + super.cursorColor, + super.cursorHeight, + super.cursorRadius, + super.cursorWidth, + super.decoration, + super.enableInteractiveSelection, + super.enableSuggestions, + super.expands, + super.focusNode, + super.inputFormatters, + super.keyboardAppearance, + super.keyboardType, + super.maxLength, + super.maxLines, + super.minLines, + super.obscureText, + super.onAppPrivateCommand, + super.onChanged, + super.onEditingComplete, + super.onSubmitted, + super.onTap, + super.readOnly, + super.scrollController, + super.scrollPadding, + super.scrollPhysics, + super.showCursor, + super.smartDashesType, + super.smartQuotesType, + super.style, + super.textAlign, + super.textAlignVertical, + super.textCapitalization, + super.textDirection, + super.textInputAction, + super.toolbarOptions, + super.contentInsertionConfiguration, + super.selectionControls, + super.mouseCursor, + super.dragStartBehavior, + super.cursorOpacityAnimates, + super.enableIMEPersonalizedLearning, + super.enabled, + super.extendedContextMenuBuilder, + super.extendedSpellCheckConfiguration, + super.maxLengthEnforcement, + super.obscuringCharacter, + super.onTapOutside, + super.restorationId, + super.scribbleEnabled, + super.selectionHeightStyle, + super.selectionWidthStyle, + super.strutStyle, + super.undoController, + this.keyHighlightStyle, + this.stringHighlightStyle, + this.numberHighlightStyle, + this.boolHighlightStyle, + this.nullHighlightStyle, + this.specialCharHighlightStyle, + this.errorTextStyle, + this.commonTextStyle, + this.errorContainerDecoration, + this.showErrorMessage = false, + this.isFormatting = true, + this.onError, + }); + + /// If true, the text will be formatted as json. If false, the text field will behave as a normal text field. Default is true. + final bool isFormatting; + + /// TextStyle for the json key. + final TextStyle? keyHighlightStyle; + + /// TextStyle for the json string. + final TextStyle? stringHighlightStyle; + + /// TextStyle for the json number. + final TextStyle? numberHighlightStyle; + + /// TextStyle for the json bool. + final TextStyle? boolHighlightStyle; + + /// TextStyle for the json null. + final TextStyle? nullHighlightStyle; + + /// TextStyle for the json special character. + final TextStyle? specialCharHighlightStyle; + + /// TextStyle for the error message. + final TextStyle? errorTextStyle; + + /// TextStyle for the common text. + final TextStyle? commonTextStyle; + + /// If true, the error message will be shown, at bottom of the text field. Default is false. + final bool showErrorMessage; + + /// Decoration for the error message container. + final BoxDecoration? errorContainerDecoration; + + /// Callback for the error message. + final Function(String?)? onError; + @override + final JsonTextFieldController? controller; + + @override + JsonFieldState createState() { + return JsonFieldState(); + } +} + +class JsonFieldState extends State { + late final JsonTextFieldController controller = + widget.controller ?? JsonTextFieldController(); + late String? jsonError = + controller.text.isEmpty + ? null + : JsonUtils.getJsonParsingError(controller.text); + late TextStyle style = widget.style ?? const TextStyle(); + late final TextStyle keyHighlightStyle = + widget.keyHighlightStyle ?? + style.copyWith( + fontWeight: FontWeight.bold, + color: const Color.fromARGB(255, 68, 143, 255), + ); + late final TextStyle stringHighlightStyle = + widget.stringHighlightStyle ?? style.copyWith(color: Colors.green[900]); + late final TextStyle numberHighlightStyle = + widget.numberHighlightStyle ?? style.copyWith(color: Colors.purple[900]); + late final TextStyle boolHighlightStyle = + widget.boolHighlightStyle ?? + style.copyWith(color: Colors.purple[900], fontWeight: FontWeight.bold); + late final TextStyle nullHighlightStyle = + widget.nullHighlightStyle ?? + style.copyWith(color: Colors.grey[600], fontWeight: FontWeight.bold); + late final TextStyle specialCharHighlightStyle = + widget.specialCharHighlightStyle ?? + style.copyWith(color: Colors.grey[700]); + late final TextStyle errorTextStyle = + widget.errorTextStyle ?? style.copyWith(color: Colors.red); + late final TextStyle commonTextStyle = + widget.commonTextStyle ?? style.copyWith(color: Colors.black); + + @override + void initState() { + controller.text = + (widget.isFormatting && JsonUtils.isValidJson(controller.text)) + ? JsonUtils.getPrettyPrintJson(controller.text) + : controller.text; + + super.initState(); + } + + void _setJsonError(String? error) => setState(() => jsonError = error); + @override + Widget build(BuildContext context) { + return Stack( + alignment: AlignmentDirectional.bottomCenter, + children: [ + ExtendedTextField( + autocorrect: widget.autocorrect, + autofillHints: widget.autofillHints, + autofocus: widget.autofocus, + buildCounter: widget.buildCounter, + canRequestFocus: widget.canRequestFocus, + clipBehavior: widget.clipBehavior, + controller: controller, + cursorColor: widget.cursorColor, + cursorHeight: widget.cursorHeight, + cursorRadius: widget.cursorRadius, + cursorWidth: widget.cursorWidth, + decoration: widget.decoration, + enableInteractiveSelection: widget.enableInteractiveSelection, + enableSuggestions: widget.enableSuggestions, + expands: widget.expands, + focusNode: widget.focusNode, + inputFormatters: widget.inputFormatters, + keyboardAppearance: widget.keyboardAppearance, + keyboardType: widget.keyboardType, + maxLength: widget.maxLength, + maxLines: widget.maxLines, + minLines: widget.minLines, + obscureText: widget.obscureText, + onAppPrivateCommand: widget.onAppPrivateCommand, + onChanged: (value) { + widget.onChanged?.call(value); + if (widget.isFormatting) { + JsonUtils.validateJson( + json: value, + onError: (error) { + _setJsonError(error); + widget.onError?.call(error); + }, + ); + } + }, + onEditingComplete: widget.onEditingComplete, + onSubmitted: widget.onSubmitted, + onTap: widget.onTap, + readOnly: widget.readOnly, + scrollController: widget.scrollController, + scrollPadding: widget.scrollPadding, + scrollPhysics: widget.scrollPhysics, + showCursor: widget.showCursor, + smartDashesType: widget.smartDashesType, + smartQuotesType: widget.smartQuotesType, + specialTextSpanBuilder: JsonHighlight( + boolHighlightStyle: boolHighlightStyle, + keyHighlightStyle: keyHighlightStyle, + nullHighlightStyle: nullHighlightStyle, + numberHighlightStyle: numberHighlightStyle, + specialCharHighlightStyle: stringHighlightStyle, + stringHighlightStyle: stringHighlightStyle, + commonTextStyle: commonTextStyle, + isFormating: widget.isFormatting, + ), + style: widget.style, + textAlign: widget.textAlign, + textAlignVertical: widget.textAlignVertical, + textCapitalization: widget.textCapitalization, + textDirection: widget.textDirection, + textInputAction: widget.textInputAction, + contentInsertionConfiguration: widget.contentInsertionConfiguration, + selectionControls: widget.selectionControls, + mouseCursor: widget.mouseCursor, + dragStartBehavior: widget.dragStartBehavior, + cursorOpacityAnimates: widget.cursorOpacityAnimates, + enableIMEPersonalizedLearning: widget.enableIMEPersonalizedLearning, + enabled: widget.enabled, + maxLengthEnforcement: widget.maxLengthEnforcement, + obscuringCharacter: widget.obscuringCharacter, + onTapOutside: widget.onTapOutside, + restorationId: widget.restorationId, + scribbleEnabled: widget.scribbleEnabled, + selectionHeightStyle: widget.selectionHeightStyle, + selectionWidthStyle: widget.selectionWidthStyle, + strutStyle: widget.strutStyle, + undoController: widget.undoController, + ), + if (widget.isFormatting && widget.showErrorMessage) + ErrorMessageContainer( + jsonError: jsonError, + errorTextStyle: errorTextStyle, + decoration: + widget.errorContainerDecoration ?? + const BoxDecoration(color: Colors.amber), + ), + ], + ); + } +} diff --git a/packages/json_field_editor/lib/src/json_highlight/highlight_strategy.dart b/packages/json_field_editor/lib/src/json_highlight/highlight_strategy.dart new file mode 100644 index 00000000..caf32051 --- /dev/null +++ b/packages/json_field_editor/lib/src/json_highlight/highlight_strategy.dart @@ -0,0 +1,71 @@ +import 'package:flutter/material.dart'; + +abstract class HighlightStrategy { + final TextStyle? textStyle; + + HighlightStrategy({required this.textStyle}); + + bool match(String word); + + TextSpan textSpan(String word); +} + +class KeyHighlightStrategy extends HighlightStrategy { + KeyHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'\".*?\"\s*:').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} + +class StringHighlightStrategy extends HighlightStrategy { + StringHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'\".*?\"').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} + +class NumberHighlightStrategy extends HighlightStrategy { + NumberHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'\s*\b(\d+(\.\d+)?)\b').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} + +class BoolHighlightStrategy extends HighlightStrategy { + BoolHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'\b(true|false)\b').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} + +class NullHighlightStrategy extends HighlightStrategy { + NullHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'\bnull\b').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} + +class SpecialCharHighlightStrategy extends HighlightStrategy { + SpecialCharHighlightStrategy({required super.textStyle}); + + @override + bool match(String word) => RegExp(r'[{}\[\],:]').hasMatch(word); + + @override + TextSpan textSpan(String word) => TextSpan(text: word, style: textStyle); +} diff --git a/packages/json_field_editor/lib/src/json_highlight/json_highlight.dart b/packages/json_field_editor/lib/src/json_highlight/json_highlight.dart new file mode 100644 index 00000000..3b61073e --- /dev/null +++ b/packages/json_field_editor/lib/src/json_highlight/json_highlight.dart @@ -0,0 +1,79 @@ +import 'package:extended_text_field/extended_text_field.dart'; +import 'package:flutter/material.dart'; +import 'package:json_field_editor/src/json_highlight/highlight_strategy.dart'; + +class JsonHighlight extends SpecialTextSpanBuilder { + final TextStyle? keyHighlightStyle; + final TextStyle? stringHighlightStyle; + final TextStyle? numberHighlightStyle; + final TextStyle? boolHighlightStyle; + final TextStyle? nullHighlightStyle; + final TextStyle? specialCharHighlightStyle; + final TextStyle? commonTextStyle; + final bool isFormating; + + JsonHighlight({ + this.keyHighlightStyle, + this.stringHighlightStyle, + this.numberHighlightStyle, + this.boolHighlightStyle, + this.nullHighlightStyle, + this.specialCharHighlightStyle, + this.commonTextStyle, + required this.isFormating, + }); + + @override + TextSpan build( + String data, { + TextStyle? textStyle, + SpecialTextGestureTapCallback? onTap, + }) { + List strategies = [ + KeyHighlightStrategy(textStyle: keyHighlightStyle), + StringHighlightStrategy(textStyle: stringHighlightStyle), + NumberHighlightStrategy(textStyle: numberHighlightStyle), + BoolHighlightStrategy(textStyle: boolHighlightStyle), + NullHighlightStrategy(textStyle: nullHighlightStyle), + SpecialCharHighlightStrategy(textStyle: specialCharHighlightStyle), + ]; + + List spans = []; + + data.splitMapJoin( + RegExp( + r'\".*?\"\s*:|\".*?\"|\s*\b(\d+(\.\d+)?)\b|\b(true|false|null)\b|[{}\[\],]', + ), + onMatch: (m) { + String word = m.group(0)!; + if (isFormating) { + spans.add( + strategies + .firstWhere((element) => element.match(word)) + .textSpan(word), + ); + + return ''; + } + spans.add(TextSpan(text: word, style: commonTextStyle)); + return ''; + }, + onNonMatch: (n) { + spans.add(TextSpan(text: n, style: commonTextStyle)); + return ''; + }, + ); + + return TextSpan(children: spans); + } + + @override + SpecialText? createSpecialText( + String flag, { + TextStyle? textStyle, + SpecialTextGestureTapCallback? onTap, + required int index, + }) { + throw UnimplementedError(); + } +} diff --git a/packages/json_field_editor/lib/src/json_text_field_controller.dart b/packages/json_field_editor/lib/src/json_text_field_controller.dart new file mode 100644 index 00000000..2809917a --- /dev/null +++ b/packages/json_field_editor/lib/src/json_text_field_controller.dart @@ -0,0 +1,13 @@ +import 'package:flutter/material.dart'; +import 'package:json_field_editor/src/json_utils.dart'; + +class JsonTextFieldController extends TextEditingController { + JsonTextFieldController(); + + /// Format the JSON text in the controller. Use [sortJson] to sort the JSON keys. + formatJson({required bool sortJson}) { + if (JsonUtils.isValidJson(text)) { + JsonUtils.formatTextFieldJson(sortJson: sortJson, controller: this); + } + } +} diff --git a/packages/json_field_editor/lib/src/json_utils.dart b/packages/json_field_editor/lib/src/json_utils.dart new file mode 100644 index 00000000..ed3e04dc --- /dev/null +++ b/packages/json_field_editor/lib/src/json_utils.dart @@ -0,0 +1,94 @@ +import 'dart:collection'; +import 'dart:convert'; + +import 'package:flutter/material.dart'; + +class JsonUtils { + static bool isValidJson(String? jsonString) { + if (jsonString == null) { + return false; + } + try { + json.decode(jsonString); + return true; + } on FormatException catch (_) { + return false; + } + } + + static String? getJsonParsingError(String? jsonString) { + if (jsonString == null) { + return null; + } + try { + json.decode(jsonString); + + return null; + } on FormatException catch (e) { + return e.message; + } + } + + static String getPrettyPrintJson(String jsonString) { + var jsonObject = json.decode(jsonString); + JsonEncoder encoder = const JsonEncoder.withIndent(' '); + String prettyString = encoder.convert(jsonObject); + return prettyString; + } + + static String getSortJsonString(String jsonString) { + dynamic sort(dynamic value) { + if (value is Map) { + return SplayTreeMap.from( + value.map((key, value) => MapEntry(key, sort(value))), + ); + } else if (value is List) { + return value.map(sort).toList(); + } else { + return value; + } + } + + var jsonObject = json.decode(jsonString); + var sortedMap = sort(jsonObject); + String sortedJsonString = json.encode(sortedMap); + return sortedJsonString; + } + + static void formatTextFieldJson({ + required bool sortJson, + required TextEditingController controller, + }) { + final oldText = controller.text; + final oldSelection = controller.selection; + + controller.text = + sortJson + ? JsonUtils.getPrettyPrintJson( + JsonUtils.getSortJsonString(controller.text), + ) + : JsonUtils.getPrettyPrintJson(controller.text); + + final addedCharacters = controller.text.length - oldText.length; + final newSelectionStart = oldSelection.start + addedCharacters; + final newSelectionEnd = oldSelection.end + addedCharacters; + + controller.selection = TextSelection( + baseOffset: newSelectionStart, + extentOffset: newSelectionEnd, + ); + } + + static validateJson({ + required String json, + required Function(String?) onError, + }) { + if (json.isEmpty) return onError(null); + + if (JsonUtils.isValidJson(json)) { + onError(null); + } else { + onError(JsonUtils.getJsonParsingError(json)); + } + } +} diff --git a/packages/json_field_editor/pubspec.yaml b/packages/json_field_editor/pubspec.yaml new file mode 100644 index 00000000..e2d1ca23 --- /dev/null +++ b/packages/json_field_editor/pubspec.yaml @@ -0,0 +1,23 @@ +name: json_field_editor +description: "JSON field editor allows easy editing of JSON text. Unlock the full capabilities of JSON syntax highlighting, validation & formatting." +version: 1.2.0 +repository: https://github.com/foss42/apidash/tree/main/packages/json_textfield +homepage: https://github.com/foss42/apidash/tree/main/packages/json_textfield +issue_tracker: https://github.com/foss42/apidash/issues +documentation: https://github.com/foss42/apidash/tree/main/packages/json_textfield + +environment: + sdk: ^3.7.0 + flutter: ">=1.17.0" + +dependencies: + flutter: + sdk: flutter + extended_text_field: ^16.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: diff --git a/packages/json_field_editor/test/src/error_message_container_test.dart b/packages/json_field_editor/test/src/error_message_container_test.dart new file mode 100644 index 00000000..57e78421 --- /dev/null +++ b/packages/json_field_editor/test/src/error_message_container_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:json_field_editor/src/error_message_container.dart'; + +void main() { + testWidgets('ErrorMessageContainer shows correct error message', ( + WidgetTester tester, + ) async { + // Build our app and trigger a frame. + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ErrorMessageContainer( + jsonError: 'Test error message', + errorTextStyle: TextStyle(), + ), + ), + ), + ); + + // Verify that our widget shows the correct error message. + expect(find.text('Test error message'), findsOneWidget); + }); + + testWidgets( + 'ErrorMessageContainer does not show error message when jsonError is null', + (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget( + const MaterialApp( + home: Scaffold( + body: ErrorMessageContainer( + jsonError: null, + errorTextStyle: TextStyle(), + ), + ), + ), + ); + + // Verify that our widget does not show an error message. + expect(find.byType(Text), findsNothing); + }, + ); +} diff --git a/packages/json_field_editor/test/src/json_highlight/json_highlight_test.dart b/packages/json_field_editor/test/src/json_highlight/json_highlight_test.dart new file mode 100644 index 00000000..46bef7ac --- /dev/null +++ b/packages/json_field_editor/test/src/json_highlight/json_highlight_test.dart @@ -0,0 +1,26 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:json_field_editor/src/json_highlight/json_highlight.dart'; + +void main() { + group('JsonHighlight', () { + test( + 'build method should create TextSpan with correct number of children', + () { + // Arrange + var jsonHighlight = JsonHighlight(isFormating: true); + var data = + '{"key": "value", "number": 123, "bool": true, "null": null}'; + + // Act + TextSpan result = jsonHighlight.build(data); + + // Assert + expect( + result.children!.length, + 23, + ); // adjust this based on your expected result + }, + ); + }); +} diff --git a/packages/json_field_editor/test/src/json_text_field_editor_test.dart b/packages/json_field_editor/test/src/json_text_field_editor_test.dart new file mode 100644 index 00000000..153c946a --- /dev/null +++ b/packages/json_field_editor/test/src/json_text_field_editor_test.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:json_field_editor/json_field_editor.dart'; + +void main() { + testWidgets('JsonTextField Widget Test. Formatting a valid json', ( + WidgetTester tester, + ) async { + // Build our app and trigger a frame. + final controller = JsonTextFieldController(); + controller.value = const TextEditingValue( + text: '{"key": "value"}', + selection: TextSelection.collapsed(offset: 0), + ); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 300, + width: 300, + child: JsonField(isFormatting: true, controller: controller), + ), + ), + ), + ); + + // Verify that JsonTextField is present. + expect(find.byType(JsonField), findsOneWidget); + expect(controller.text, equals('{\n "key": "value"\n}')); + }); + + testWidgets( + 'JsonTextField Widget Test. Formatting a valid json using controller', + (WidgetTester tester) async { + // Build our app and trigger a frame. + final controller = JsonTextFieldController(); + controller.value = const TextEditingValue( + text: '{"key": "value"}', + selection: TextSelection.collapsed(offset: 0), + ); + controller.formatJson(sortJson: false); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: SizedBox( + height: 300, + width: 300, + child: JsonField(isFormatting: true, controller: controller), + ), + ), + ), + ); + + // Verify that JsonTextField is present. + expect(find.byType(JsonField), findsOneWidget); + expect(controller.text, equals('{\n "key": "value"\n}')); + }, + ); + + testWidgets('JsonTextField Widget Test, invalid Json', ( + WidgetTester tester, + ) async { + final controller = JsonTextFieldController(); + controller.value = const TextEditingValue( + text: '{"key": "value"', + selection: TextSelection.collapsed(offset: 0), + ); + + controller.formatJson(sortJson: false); + + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JsonField(isFormatting: true, controller: controller), + ), + ), + ); + + // Verify that JsonTextField is present. + expect(find.byType(JsonField), findsOneWidget); + expect(controller.text, equals('\n{"key": "value"')); + }); + testWidgets('JsonTextField Widget Test, in a valid Json', ( + WidgetTester tester, + ) async { + final controller = JsonTextFieldController(); + controller.value = const TextEditingValue( + text: '{"key": "value","anotherKey": "anotherValue","list": [3,2,1]}', + selection: TextSelection.collapsed(offset: 0), + ); + + // Build our app and trigger a frame. + controller.formatJson(sortJson: true); + await tester.pumpWidget( + MaterialApp( + home: Scaffold( + body: JsonField(isFormatting: true, controller: controller), + ), + ), + ); + + // Verify that JsonTextField is present. + expect(find.byType(JsonField), findsOneWidget); + expect( + controller.text, + equals( + '{\n "anotherKey": "anotherValue",\n "key": "value",\n "list": [\n 3,\n 2,\n 1\n ]\n}', + ), + ); + }); +} diff --git a/packages/json_field_editor/test/src/json_utils_test.dart b/packages/json_field_editor/test/src/json_utils_test.dart new file mode 100644 index 00000000..5a369fac --- /dev/null +++ b/packages/json_field_editor/test/src/json_utils_test.dart @@ -0,0 +1,44 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:json_field_editor/src/json_utils.dart'; + +void main() { + group('getJsonParsingError', () { + test('returns error message for invalid JSON', () { + String? invalidJson = '{ "name": "John", "age": 30, }'; + expect(JsonUtils.getJsonParsingError(invalidJson), isNotNull); + }); + + test('returns null for valid JSON', () { + String? validJson = '{ "name": "John", "age": 30 }'; + expect(JsonUtils.getJsonParsingError(validJson), isNull); + }); + }); + + group('validateJson', () { + test('calls onError with null for valid JSON', () { + String validJson = '{ "name": "John", "age": 30 }'; + bool onErrorCalled = false; + JsonUtils.validateJson( + json: validJson, + onError: (error) { + onErrorCalled = true; + expect(error, isNull); + }, + ); + expect(onErrorCalled, isTrue); + }); + + test('calls onError with error message for invalid JSON', () { + String invalidJson = '{ "name": "John", "age": 30, }'; + bool onErrorCalled = false; + JsonUtils.validateJson( + json: invalidJson, + onError: (error) { + onErrorCalled = true; + expect(error, isNotNull); + }, + ); + expect(onErrorCalled, isTrue); + }); + }); +}