mirror of
https://github.com/foss42/apidash.git
synced 2025-12-03 19:39:25 +08:00
Add package json_field_editor
This commit is contained in:
4
packages/json_field_editor/lib/json_field_editor.dart
Normal file
4
packages/json_field_editor/lib/json_field_editor.dart
Normal file
@@ -0,0 +1,4 @@
|
||||
library json_field_editor;
|
||||
|
||||
export 'src/json_field.dart';
|
||||
export 'src/json_text_field_controller.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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
271
packages/json_field_editor/lib/src/json_field.dart
Normal file
271
packages/json_field_editor/lib/src/json_field.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
94
packages/json_field_editor/lib/src/json_utils.dart
Normal file
94
packages/json_field_editor/lib/src/json_utils.dart
Normal 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user