Add package json_field_editor

This commit is contained in:
Ankit Mahato
2025-04-09 03:21:33 +05:30
parent 01871b397c
commit 474b9d7112
25 changed files with 1713 additions and 0 deletions

View File

@@ -0,0 +1,4 @@
library json_field_editor;
export 'src/json_field.dart';
export 'src/json_text_field_controller.dart';

View File

@@ -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),
),
),
),
);
}
}

View File

@@ -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,
),
);
}
}

View File

@@ -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<JsonField> {
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),
),
],
);
}
}

View File

@@ -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);
}

View File

@@ -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<HighlightStrategy> strategies = [
KeyHighlightStrategy(textStyle: keyHighlightStyle),
StringHighlightStrategy(textStyle: stringHighlightStyle),
NumberHighlightStrategy(textStyle: numberHighlightStyle),
BoolHighlightStrategy(textStyle: boolHighlightStyle),
NullHighlightStrategy(textStyle: nullHighlightStyle),
SpecialCharHighlightStrategy(textStyle: specialCharHighlightStyle),
];
List<TextSpan> 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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<String, dynamic>.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));
}
}
}