Line data Source code
1 : // Copyright 2023 The terCAD team. All rights reserved.
2 : // Use of this source code is governed by a CC BY-NC-ND 4.0 license that can be found in the LICENSE file.
3 :
4 : import 'dart:collection';
5 : import 'dart:math';
6 :
7 : import 'package:app_finance/_classes/controller/delayed_call.dart';
8 : import 'package:app_finance/_configs/theme_helper.dart';
9 : import 'package:material_ui/material_ui.dart';
10 :
11 : class FocusController {
12 : final DelayedCall _scrollTo = DelayedCall(300);
13 : final DelayedCall _focus = DelayedCall(300);
14 : final _controller = ScrollController();
15 :
16 : final scope = <int?, FocusNode>{};
17 : final values = <int?, dynamic>{};
18 : final order = SplayTreeMap<int, int?>();
19 : int? focus;
20 : int? idButton;
21 : static bool force = false;
22 :
23 1 : FocusController();
24 :
25 1 : ScrollController get controller {
26 2 : order.clear();
27 2 : values.clear();
28 1 : return _controller;
29 : }
30 :
31 2 : int key(dynamic item) => item?.hashCode ?? 0;
32 :
33 1 : FocusNode last(dynamic item) {
34 2 : idButton = key(item);
35 4 : scope[idButton] ??= FocusNode();
36 3 : return scope[idButton]!;
37 : }
38 :
39 1 : FocusNode bind(dynamic item, {required BuildContext context, dynamic value}) {
40 1 : final id = key(item);
41 2 : if (!order.containsValue(id)) {
42 4 : order[order.length] = id;
43 : }
44 3 : scope[id] ??= FocusNode();
45 2 : values[id] = value;
46 : if (!force) {
47 3 : _focus.run(onEditingComplete);
48 : }
49 2 : return scope[id]!;
50 : }
51 :
52 3 : bool _isControllerActive() => _controller.hasClients;
53 :
54 1 : double _getPosition(BuildContext context) {
55 1 : RenderObject? obj = context.findRenderObject();
56 3 : return obj is RenderBox ? obj.localToGlobal(Offset.zero).dy : 0;
57 : }
58 :
59 3 : double _getMinPosition() => order.values
60 5 : .map((id) => scope[id]?.context)
61 2 : .where((element) => element != null)
62 3 : .map((e) => _getPosition(e!))
63 1 : .reduce(min);
64 :
65 6 : bool _isLast(int? idx) => idx == order[order.lastKey()];
66 :
67 3 : TextInputAction getAction(dynamic item) => _isLast(key(item)) ? TextInputAction.done : TextInputAction.next;
68 :
69 1 : void scrollToFocusedElement(dynamic item, [int? idx]) {
70 0 : idx ??= key(item!);
71 3 : if (!_isControllerActive() || scope.isEmpty) {
72 : return;
73 : }
74 7 : if (order.containsValue(idx) && scope[idx] != null && scope[idx]!.context != null) {
75 7 : double position = _getPosition(scope[idx]!.context!) - _getMinPosition() - ThemeHelper.barHeight;
76 1 : if (position < 0) {
77 : position = 0;
78 : }
79 2 : _controller.animateTo(
80 : position,
81 : duration: const Duration(milliseconds: 600),
82 : curve: Curves.easeInOut,
83 : );
84 : }
85 : }
86 :
87 1 : void onEditingComplete([dynamic item]) {
88 4 : int? targetKey = item == null ? focus ?? order.values.firstOrNull : null;
89 : int? idx;
90 : force = false;
91 : if (item != null) {
92 1 : idx = key(item);
93 2 : values[idx] = true;
94 : }
95 3 : List<int?> check = order.values.toList();
96 1 : if (idButton != null) {
97 3 : values[idButton] = null;
98 2 : check.add(idButton);
99 : }
100 2 : for (int? key in check) {
101 6 : if (targetKey != null && key != targetKey && (values[key] == '' || values[key] == null)) {
102 1 : onFocus(null, key, false);
103 : targetKey = null;
104 : }
105 1 : if (key == idx) {
106 : targetKey = key;
107 : }
108 : }
109 : }
110 :
111 4 : bool isFocused(dynamic item) => key(item) == focus;
112 :
113 5 : void blur() => scope.forEach((_, value) => value.unfocus());
114 :
115 1 : void onFocus(dynamic item, [int? idx, bool isForced = true]) {
116 1 : idx ??= key(item);
117 1 : focus = idx;
118 4 : if ((order.containsValue(idx) || idx == idButton) &&
119 2 : scope[idx] != null &&
120 3 : scope[idx]!.context != null &&
121 4 : scope[idx]!.context!.mounted) {
122 1 : blur();
123 3 : scope[idx]?.requestFocus();
124 : if (item != null) {
125 4 : _scrollTo.run(() => scrollToFocusedElement(item, idx));
126 : }
127 : }
128 : force = isForced;
129 : }
130 :
131 1 : void dispose() {
132 4 : scope.forEach((_, node) => node.dispose());
133 2 : scope.clear();
134 2 : values.clear();
135 2 : order.clear();
136 2 : _controller.dispose();
137 1 : focus = null;
138 : }
139 : }
|