@ea-lab/reactive-json 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/README.md +83 -0
  2. package/dist/reactive-json.css +5 -0
  3. package/dist/reactive-json.js +56303 -0
  4. package/dist/reactive-json.umd.cjs +382 -0
  5. package/lib/component/action/HashChangeListener.jsx +66 -0
  6. package/lib/component/action/Hide.jsx +14 -0
  7. package/lib/component/action/MessageListener.jsx +62 -0
  8. package/lib/component/action/Popover.jsx +53 -0
  9. package/lib/component/action/ReactOnEvent.jsx +118 -0
  10. package/lib/component/action/Redirect.jsx +26 -0
  11. package/lib/component/action/Tooltip.jsx +27 -0
  12. package/lib/component/action/VisuallyHide.jsx +15 -0
  13. package/lib/component/element/chart/BarChart.jsx +40 -0
  14. package/lib/component/element/chart/DoughnutChart.jsx +32 -0
  15. package/lib/component/element/chart/LineChart.jsx +40 -0
  16. package/lib/component/element/chart/PolarAreaChart.jsx +32 -0
  17. package/lib/component/element/form/CheckBoxField.jsx +215 -0
  18. package/lib/component/element/form/DateField.jsx +42 -0
  19. package/lib/component/element/form/NumberField.jsx +29 -0
  20. package/lib/component/element/form/SelectField.jsx +130 -0
  21. package/lib/component/element/form/TextAreaField.jsx +48 -0
  22. package/lib/component/element/form/TextField.jsx +65 -0
  23. package/lib/component/element/form/formElementsCommon.jsx +54 -0
  24. package/lib/component/element/html/AccordionItem.jsx +42 -0
  25. package/lib/component/element/html/FolderSortableTree.jsx +307 -0
  26. package/lib/component/element/html/FormatNumeral.jsx +118 -0
  27. package/lib/component/element/html/Html.jsx +107 -0
  28. package/lib/component/element/html/LabelFromValue.jsx +89 -0
  29. package/lib/component/element/html/Modal.jsx +77 -0
  30. package/lib/component/element/html/ModalForm.jsx +30 -0
  31. package/lib/component/element/html/Paragraph.jsx +10 -0
  32. package/lib/component/element/html/PreformattedMarkup.jsx +54 -0
  33. package/lib/component/element/html/SortableTreeItemCollapseButton.jsx +20 -0
  34. package/lib/component/element/html/Tabs.jsx +55 -0
  35. package/lib/component/element/special/BootstrapElement.jsx +32 -0
  36. package/lib/component/element/special/Count.jsx +46 -0
  37. package/lib/component/element/special/DataFilter.jsx +156 -0
  38. package/lib/component/element/special/DelayedActions.jsx +119 -0
  39. package/lib/component/element/special/PageControls.jsx +19 -0
  40. package/lib/component/element/special/Phantom.jsx +25 -0
  41. package/lib/component/element/special/Switch.jsx +131 -0
  42. package/lib/component/hook/usePagination.jsx +184 -0
  43. package/lib/component/reaction/addData.jsx +23 -0
  44. package/lib/component/reaction/fetchData.jsx +83 -0
  45. package/lib/component/reaction/moveData.jsx +52 -0
  46. package/lib/component/reaction/postMessage.jsx +43 -0
  47. package/lib/component/reaction/redirectNow.jsx +17 -0
  48. package/lib/component/reaction/removeData.jsx +48 -0
  49. package/lib/component/reaction/setClipboardData.jsx +20 -0
  50. package/lib/component/reaction/setData.jsx +23 -0
  51. package/lib/component/reaction/submitData.jsx +136 -0
  52. package/lib/component/reaction/triggerEvent.jsx +62 -0
  53. package/lib/component/utility/formatString.jsx +59 -0
  54. package/lib/engine/Actions.jsx +392 -0
  55. package/lib/engine/EventDispatcherContext.jsx +16 -0
  56. package/lib/engine/EventDispatcherProvider.jsx +80 -0
  57. package/lib/engine/GlobalDataContext.jsx +13 -0
  58. package/lib/engine/GlobalDataContextProvider.jsx +33 -0
  59. package/lib/engine/PaginationContext.jsx +10 -0
  60. package/lib/engine/PaginationProvider.jsx +61 -0
  61. package/lib/engine/ReactiveJsonRoot.jsx +315 -0
  62. package/lib/engine/TemplateContext.jsx +13 -0
  63. package/lib/engine/TemplateSystem.jsx +302 -0
  64. package/lib/engine/View.jsx +240 -0
  65. package/lib/main.jsx +41 -0
  66. package/package.json +72 -0
@@ -0,0 +1,80 @@
1
+ import {memo} from "react";
2
+ import EventDispatcherContext from "./EventDispatcherContext";
3
+
4
+ /**
5
+ * Contains the registered listeners. It's shared between all the components.
6
+ *
7
+ * @type {{}}
8
+ */
9
+ const events = {};
10
+
11
+ /**
12
+ * The main dispatcher which will really be called by the browser's event system.
13
+ *
14
+ * @param {Event} event The event sent by the browser, which will be transferred to the registered listeners.
15
+ */
16
+ const mainDispatcher = (event) => {
17
+ if (!events.hasOwnProperty(event.type)) {
18
+ // Ignore this event.
19
+ return;
20
+ }
21
+
22
+ for (const registeredListener of events[event.type]) {
23
+ // Inject the event details into the registered listener.
24
+ registeredListener(event);
25
+ }
26
+ };
27
+
28
+ /**
29
+ * Adds the listener to registered listeners.
30
+ *
31
+ * @param {string} type The event type.
32
+ * @param {Function} listener The event listener.
33
+ */
34
+ export const addEventListener = (type, listener) => {
35
+ if (events.hasOwnProperty(type)) {
36
+ events[type].push(listener);
37
+ } else {
38
+ // This is a new event type. Add the event listener.
39
+ window.addEventListener(type, mainDispatcher);
40
+
41
+ events[type] = [listener];
42
+ }
43
+ };
44
+
45
+ /**
46
+ * Removes the specified listener.
47
+ *
48
+ * @param {string} type Event type.
49
+ * @param {Function} listener Event listener.
50
+ */
51
+ export const removeEventListener = (type, listener) => {
52
+ if (!events.hasOwnProperty(type)) {
53
+ return;
54
+ }
55
+
56
+ const index = events[type].indexOf(listener);
57
+
58
+ if (index > -1) {
59
+ // Remove once the item.
60
+ events[type].splice(index, 1);
61
+ }
62
+ };
63
+
64
+ /**
65
+ * Use this provider in combination with EventDispatcherContext.
66
+ *
67
+ * @param props
68
+ *
69
+ * @returns {JSX.Element}
70
+ *
71
+ * @constructor
72
+ */
73
+ const EventDispatcherProvider = (props) => {
74
+ return <EventDispatcherContext.Provider value={{addEventListener, removeEventListener}}>
75
+ {props.children}
76
+ </EventDispatcherContext.Provider>;
77
+ }
78
+
79
+ // TODO: evaluate if memo is useful here.
80
+ export default memo(EventDispatcherProvider);
@@ -0,0 +1,13 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * This context contains all the build data for the current app.
5
+ *
6
+ * The use of this context is similar to TemplateContext,
7
+ * but the difference is that this one has only one instance.
8
+ *
9
+ * @type {React.Context<{}>}
10
+ */
11
+ const GlobalDataContext = createContext({});
12
+
13
+ export default GlobalDataContext;
@@ -0,0 +1,33 @@
1
+ import GlobalDataContext from "./GlobalDataContext";
2
+
3
+ /**
4
+ * Standard implementation of the provider for GlobalDataContext.
5
+ *
6
+ * @param props Component props. Must have the "value" key.
7
+ *
8
+ * @returns {JSX.Element}
9
+ *
10
+ * @constructor
11
+ */
12
+ const GlobalDataContextProvider = (props) => {
13
+ // Shallow copy. This will help to keep the original data representation.
14
+ const valueCopy = {...props.value};
15
+
16
+ if (valueCopy.getRootContext === undefined) {
17
+ // The root context is unset.
18
+ // Self reference this root object with this closure.
19
+ // It's useful so that new global data contexts such as those created
20
+ // by the DataFilter component can access the root global data context value
21
+ // instead of the current GlobalDataContext value from useContext().
22
+ // The submitData reaction function uses such root value.
23
+ valueCopy.getRootContext = () => {
24
+ return props.value;
25
+ };
26
+ }
27
+
28
+ return <GlobalDataContext.Provider value={valueCopy}>
29
+ {props.children}
30
+ </GlobalDataContext.Provider>;
31
+ }
32
+
33
+ export default GlobalDataContextProvider;
@@ -0,0 +1,10 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * This is a context opened when a pagination is needed.
5
+ *
6
+ * @type {React.Context<{}>}
7
+ */
8
+ const PaginationContext = createContext({pagination: {}});
9
+
10
+ export default PaginationContext;
@@ -0,0 +1,61 @@
1
+ import {memo, useReducer, useRef, useState} from "react";
2
+ import PaginationContext from "./PaginationContext";
3
+
4
+ /**
5
+ * Use this provider in combination with EventDispatcherContext.
6
+ *
7
+ * @param props
8
+ *
9
+ * @returns {JSX.Element}
10
+ *
11
+ * @constructor
12
+ * @deprecated ne fonctionne pas, envisager un système de preemptiveActions pour filtrer les items en amont
13
+ */
14
+ const PaginationProvider = (props) => {
15
+ const {
16
+ after,
17
+ before,
18
+ contentToPaginate,
19
+ pagination,
20
+ } = props;
21
+ // TODO: essayer d'obtenir le nombre réel à afficher.
22
+
23
+ // TODO: essayer d'obtenir seulement les objets à afficher. (Hide lance dispatch(remove) par ex
24
+ const completeItemCount = contentToPaginate.length;
25
+
26
+
27
+ // Slice for the pagination if in effect.
28
+ const contentSlice = props?.paginated
29
+ ? contentToPaginate.slice(pagination.firstShownItemIndex, pagination.maxShownItemIndexExcluded)
30
+ : contentToPaginate;
31
+
32
+
33
+
34
+ // Dev note: on PhpStorm, disregard the Function signatures inspection errors of reducers.
35
+ // See: https://youtrack.jetbrains.com/issue/WEB-53963.
36
+ // noinspection JSCheckFunctionSignatures
37
+ const [itemCountAdjustment, dispatchItemCountAdjustment] = useReducer((prevState, dispatched) => {
38
+ switch (dispatched.type) {
39
+ case "increment":
40
+ return prevState + 1;
41
+
42
+ case "decrement":
43
+ return prevState - 1;
44
+
45
+ default:
46
+ // Unknown type.
47
+ return prevState;
48
+ }
49
+ }, {updateId: 0, realCurrentData: {}});
50
+
51
+
52
+
53
+ return <PaginationContext.Provider value={{dispatchItemCountAdjustment, pagination}}>
54
+ {before}
55
+ {contentSlice}
56
+ {after}
57
+ </PaginationContext.Provider>;
58
+ }
59
+
60
+ // TODO: evaluate if memo is useful here.
61
+ export default PaginationProvider;
@@ -0,0 +1,315 @@
1
+ import EventDispatcherProvider from "./EventDispatcherProvider";
2
+ import GlobalDataContextProvider from "./GlobalDataContextProvider";
3
+ import TemplateContext from "./TemplateContext";
4
+ import View from "./View";
5
+ import axios from "axios";
6
+ import {load} from 'js-yaml';
7
+ import {isEqual} from "lodash";
8
+ import {useEffect, useReducer, useState} from 'react';
9
+
10
+ /**
11
+ * Production ready app root.
12
+ *
13
+ * @param {string} dataFetchMethod The fetch method for the init data. Case-insensitive.
14
+ * Use "POST" for post. Other values mean "GET".
15
+ * @param {string} dataUrl The URL of the document containing the build data. Either JSON or YAML.
16
+ * @param {{}} headersForData Headers for the data request, such as authentication info.
17
+ * @param {boolean} debugMode Set to true to show the data structure and debug info.
18
+ * @param {React.Element|null} DebugModeContentWrapper Wrapper around the main reactive-json content when in debug mode.
19
+ * @param {React.Element|null} DebugModeDataWrapper Wrapper around the reactive-json debug data when in debug mode.
20
+ * @param {React.Element|null} DebugModeMainWrapper Wrapper around the reactive-json root when in debug mode.
21
+ *
22
+ * @returns {JSX.Element}
23
+ *
24
+ * @constructor
25
+ */
26
+ function ReactiveJsonRoot({
27
+ dataFetchMethod,
28
+ dataUrl,
29
+ headersForData,
30
+ debugMode,
31
+ DebugModeContentWrapper,
32
+ DebugModeDataWrapper,
33
+ DebugModeRootWrapper,
34
+ }) {
35
+ // Dev note: on PhpStorm, disregard the Function signatures inspection errors of reducers.
36
+ // See: https://youtrack.jetbrains.com/issue/WEB-53963.
37
+ // noinspection JSCheckFunctionSignatures
38
+ const [currentData, dispatchCurrentData] = useReducer((prevState, dispatched) => {
39
+ switch (dispatched.type) {
40
+ case "setData":
41
+ return {updateId: 0, realCurrentData: dispatched.data};
42
+
43
+ case "updateData":
44
+ return updateObject(prevState, dispatched.path, dispatched.value, dispatched.updateMode);
45
+
46
+ default:
47
+ // Unknown type.
48
+ return prevState;
49
+ }
50
+ }, {updateId: 0, realCurrentData: {}});
51
+ const [updatable, setUpdatable] = useState(0);
52
+ const [templates, setTemplates] = useState({});
53
+ const [renderView, setRenderView] = useState({});
54
+ const [items, setItems] = useState([]);
55
+ const [rawAppData, setRawAppData] = useState();
56
+
57
+ useEffect(() => {
58
+ if (!dataUrl) {
59
+ return;
60
+ }
61
+
62
+ if (typeof dataFetchMethod === "string" && dataFetchMethod.toLowerCase() === "post") {
63
+ // TODO: support form data.
64
+ axios.post(
65
+ dataUrl,
66
+ {
67
+ headers: headersForData,
68
+ }).then((res) => {
69
+ setRawAppData(res.data);
70
+ });
71
+ } else {
72
+ axios.get(
73
+ dataUrl,
74
+ {
75
+ headers: headersForData,
76
+ }).then((res) => {
77
+ setRawAppData(res.data);
78
+ });
79
+ }
80
+ }, [dataUrl, headersForData]);
81
+
82
+ useEffect(() => {
83
+ if (!rawAppData) {
84
+ // Not yet initialized.
85
+ return;
86
+ }
87
+
88
+ let parsedData = rawAppData;
89
+
90
+ if (typeof parsedData !== "object") {
91
+ try {
92
+ // Parse as JSON.
93
+ parsedData = JSON.parse(rawAppData);
94
+ } catch {
95
+ try {
96
+ // Parse as YAML.
97
+ parsedData = load(rawAppData);
98
+ } catch {
99
+ console.log("Tried to load app data but the content could not be parsed as JSON nor YAML.");
100
+ return;
101
+ }
102
+ }
103
+ }
104
+
105
+ // Dev note: listForms is deprecated; will be removed later.
106
+ setTemplates(parsedData.templates ?? parsedData.listForms);
107
+
108
+ if (!parsedData.templates && parsedData.listForms) {
109
+ console.log("'listForms' needs to be renamed to 'templates'. The support for 'listForms' will be removed in the next releases of reactive-json.");
110
+ }
111
+
112
+ // noinspection JSCheckFunctionSignatures
113
+ dispatchCurrentData({type: "setData", "data": parsedData.data});
114
+ setRenderView(parsedData.renderView);
115
+ setItems(Object.keys(parsedData.renderView));
116
+ }, [rawAppData]);
117
+
118
+ const updateData = (newValue, pathInData, updateMode = undefined) => {
119
+ let path = pathInData.replace('data.', '');
120
+
121
+ // noinspection JSCheckFunctionSignatures
122
+ dispatchCurrentData({
123
+ type: "updateData",
124
+ path: path,
125
+ value: newValue,
126
+ updateMode: updateMode,
127
+ });
128
+ }
129
+
130
+ /**
131
+ * Updates the given data object.
132
+ *
133
+ * This must be a function to be used in the currentData's reducer.
134
+ *
135
+ * @param {{updateId: Number, realCurrentData: {}}} data The current data to edit. It will be mutated.
136
+ * updateId will increment when a re-render is needed.
137
+ * @param {string} path The path where to put (or remove) the data.
138
+ * @param {any} value The value to set. If undefined, the value will be removed at the given path.
139
+ * @param {string} updateMode The update mode, either "add", "move", "remove", or leave empty for replace.
140
+ *
141
+ * @returns {{updateId: Number, realCurrentData: {}}} Data with update ID changed if a render is needed.
142
+ */
143
+ function updateObject(data, path, value, updateMode = undefined) {
144
+ const splitPath = path.split(".");
145
+
146
+ // This will point to the current nested object.
147
+ let pointer = data.realCurrentData;
148
+
149
+ for (let i = 0, len = splitPath.length; i < len; i++) {
150
+ const currentNodeKey = splitPath[i];
151
+
152
+ if (i === len - 1) {
153
+ // This is the last key from the path.
154
+ if (updateMode === "remove" && Array.isArray(pointer)) {
155
+ // Remove the entry from the array.
156
+ pointer.splice(currentNodeKey, 1);
157
+ } else if (updateMode === "move") {
158
+ // "value" contains the info about how to move.
159
+ if (value.increment) {
160
+ // Towards the start of the array.
161
+ if (!Array.isArray(pointer)) {
162
+ // Not a valid "up" value. Do nothing.
163
+ return data;
164
+ }
165
+
166
+ const newIndex = Math.min(pointer.length, Math.max(0, parseInt(currentNodeKey) + parseInt(value.increment)));
167
+
168
+ if (newIndex === parseInt(currentNodeKey)) {
169
+ // No changes.
170
+ return data;
171
+ }
172
+
173
+ const itemToMove = pointer.splice(currentNodeKey, 1);
174
+
175
+ if (itemToMove.length < 1) {
176
+ // Nothing to move.
177
+ return data;
178
+ }
179
+
180
+ pointer.splice(newIndex, 0, itemToMove[0]);
181
+ } else {
182
+ // Nothing to move.
183
+ return data;
184
+ }
185
+ } else {
186
+ if (value === undefined) {
187
+ // Unset the key.
188
+ delete pointer[currentNodeKey];
189
+ } else if (isEqual(value, pointer[currentNodeKey])) {
190
+ // The value doesn't change.
191
+ return data;
192
+ } else {
193
+ if (updateMode === "add") {
194
+ // Add the value on the property.
195
+ if (pointer[currentNodeKey] === undefined) {
196
+ pointer[currentNodeKey] = [];
197
+ }
198
+
199
+ pointer[currentNodeKey].push(value);
200
+ } else {
201
+ // Set the value on the property.
202
+ pointer[currentNodeKey] = value;
203
+ }
204
+ }
205
+ }
206
+
207
+ return {
208
+ // Using modulo in case of massive update counts in long frontend sessions.
209
+ updateId: ((data.updateId ?? 0) % (Number.MAX_SAFE_INTEGER - 1)) + 1,
210
+ realCurrentData: data.realCurrentData
211
+ };
212
+ }
213
+
214
+ if (pointer.hasOwnProperty(currentNodeKey)) {
215
+ // The pointer already has the specified key.
216
+
217
+ // Dig deeper.
218
+ if (typeof pointer[currentNodeKey] !== "object" || pointer[currentNodeKey] === null) {
219
+ // Ensure the data is writable.
220
+ pointer[currentNodeKey] = {};
221
+ }
222
+
223
+ // Move the pointer.
224
+ pointer = pointer[currentNodeKey];
225
+ continue;
226
+ }
227
+
228
+ // This is a new property.
229
+ pointer[currentNodeKey] = {};
230
+ pointer = pointer[currentNodeKey];
231
+ }
232
+
233
+ // This should never happen.
234
+ throw new Error("Could not update data.");
235
+ }
236
+
237
+ if (!rawAppData) {
238
+ return null;
239
+ }
240
+
241
+ const rootViews = items.map(view => {
242
+ return (<View
243
+ datafield={view}
244
+ key={view}
245
+ props={renderView[view]}
246
+ path={"data." + view}
247
+ currentData={currentData.realCurrentData[view]}/>)
248
+ });
249
+
250
+ // Snippet from https://stackoverflow.com/a/1414175.
251
+ // Enhanced to support other value types.
252
+ function stringToBoolean(stringValue) {
253
+ if (!stringValue) {
254
+ return false;
255
+ }
256
+
257
+ if (typeof stringValue === "boolean") {
258
+ return stringValue;
259
+ }
260
+
261
+ if (typeof stringValue !== "string") {
262
+ return true;
263
+ }
264
+
265
+ switch (stringValue?.toLowerCase()?.trim()) {
266
+ case "true":
267
+ case "yes":
268
+ case "1":
269
+ return true;
270
+
271
+ case "false":
272
+ case "no":
273
+ case "0":
274
+ case "null":
275
+ case "undefined":
276
+ return false;
277
+
278
+ default:
279
+ return stringValue.length > 0;
280
+ }
281
+ }
282
+
283
+ const debugMode_bool = stringToBoolean(debugMode);
284
+
285
+ const mainBuild = (
286
+ <EventDispatcherProvider>
287
+ <GlobalDataContextProvider
288
+ value={{
289
+ element: templates,
290
+ headersForData,
291
+ setRawAppData,
292
+ templateData: currentData.realCurrentData,
293
+ templatePath: "data",
294
+ updateData
295
+ }}>
296
+ <TemplateContext.Provider value={{templateData: currentData.realCurrentData, templatePath: "data"}}>
297
+ {(debugMode_bool && DebugModeContentWrapper)
298
+ ? <DebugModeContentWrapper>{rootViews}</DebugModeContentWrapper>
299
+ : rootViews}
300
+ </TemplateContext.Provider>
301
+ {debugMode_bool
302
+ ? (DebugModeDataWrapper && <DebugModeDataWrapper>
303
+ {JSON.stringify(currentData.realCurrentData, null, ' ')}
304
+ </DebugModeDataWrapper>)
305
+ : null}
306
+ </GlobalDataContextProvider>
307
+ </EventDispatcherProvider>
308
+ );
309
+
310
+ return (debugMode_bool && DebugModeContentWrapper)
311
+ ? <DebugModeRootWrapper>{mainBuild}</DebugModeRootWrapper>
312
+ : mainBuild;
313
+ }
314
+
315
+ export default ReactiveJsonRoot;
@@ -0,0 +1,13 @@
1
+ import { createContext } from 'react';
2
+
3
+ /**
4
+ * A template context contains the data that is shared between the contained components.
5
+ *
6
+ * A template context is created when using the "load" keyword in the render array,
7
+ * and it is supplied with the current data of the component that loaded the template.
8
+ *
9
+ * @type {React.Context<{}>}
10
+ */
11
+ const TemplateContext = createContext({});
12
+
13
+ export default TemplateContext;