active_element 0.0.10 → 0.0.11
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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -2
- data/.strong_versions.yml +1 -0
- data/Gemfile +4 -0
- data/Gemfile.lock +108 -75
- data/Makefile +10 -0
- data/active_element.gemspec +1 -1
- data/app/assets/javascripts/active_element/application.js +1 -0
- data/app/assets/javascripts/active_element/form.js +16 -32
- data/app/assets/javascripts/active_element/json_field.js +391 -135
- data/app/assets/javascripts/active_element/setup.js +13 -8
- data/app/assets/javascripts/active_element/text_search_field.js +27 -28
- data/app/assets/javascripts/active_element/theme.js +1 -1
- data/app/assets/javascripts/active_element/timezones.js +6 -0
- data/app/assets/stylesheets/active_element/_dark.scss +86 -0
- data/app/assets/stylesheets/active_element/_variables.scss +2 -1
- data/app/assets/stylesheets/active_element/application.scss +166 -33
- data/app/controllers/active_element/application_controller.rb +5 -0
- data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
- data/app/views/active_element/_user.html.erb +20 -0
- data/app/views/active_element/components/fields/_json.html.erb +24 -0
- data/app/views/active_element/components/form/_check_box.html.erb +1 -0
- data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
- data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
- data/app/views/active_element/components/form/_field.html.erb +10 -7
- data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
- data/app/views/active_element/components/form/_json.html.erb +10 -2
- data/app/views/active_element/components/form/_label.html.erb +12 -1
- data/app/views/active_element/components/form/_select.html.erb +4 -1
- data/app/views/active_element/components/form/_summary.html.erb +11 -1
- data/app/views/active_element/components/form/_templates.html.erb +37 -22
- data/app/views/active_element/components/form/_text_area.html.erb +2 -1
- data/app/views/active_element/components/form/_text_search.html.erb +7 -3
- data/app/views/active_element/components/form.html.erb +20 -17
- data/app/views/active_element/components/json.html.erb +1 -0
- data/app/views/active_element/components/navbar.html.erb +26 -0
- data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
- data/app/views/active_element/components/table/_field.html.erb +8 -0
- data/app/views/active_element/components/table/collection.html.erb +1 -1
- data/app/views/active_element/components/table/item.html.erb +5 -4
- data/app/views/active_element/default_views/edit.html.erb +5 -0
- data/app/views/active_element/default_views/index.html.erb +15 -0
- data/app/views/active_element/default_views/new.html.erb +4 -0
- data/app/views/active_element/default_views/show.html.erb +7 -0
- data/app/views/active_element/navbar/_menu.html.erb +1 -30
- data/app/views/active_element/theme/_select.html.erb +1 -1
- data/app/views/layouts/active_element.html.erb +16 -1
- data/config/brakeman.ignore +48 -0
- data/example_app/.gitattributes +7 -0
- data/example_app/.gitignore +35 -0
- data/example_app/.ruby-version +1 -0
- data/example_app/Gemfile +34 -0
- data/example_app/Gemfile.lock +296 -0
- data/example_app/README.md +24 -0
- data/example_app/Rakefile +6 -0
- data/example_app/app/assets/config/manifest.js +4 -0
- data/example_app/app/assets/images/.keep +0 -0
- data/example_app/app/assets/stylesheets/application.css +15 -0
- data/example_app/app/channels/application_cable/channel.rb +4 -0
- data/example_app/app/channels/application_cable/connection.rb +4 -0
- data/example_app/app/controllers/application_controller.rb +12 -0
- data/example_app/app/controllers/concerns/.keep +0 -0
- data/example_app/app/controllers/pets_controller.rb +6 -0
- data/example_app/app/controllers/users_controller.rb +6 -0
- data/example_app/app/helpers/application_helper.rb +2 -0
- data/example_app/app/javascript/application.js +3 -0
- data/example_app/app/javascript/controllers/application.js +9 -0
- data/example_app/app/javascript/controllers/hello_controller.js +7 -0
- data/example_app/app/javascript/controllers/index.js +11 -0
- data/example_app/app/jobs/application_job.rb +7 -0
- data/example_app/app/mailers/application_mailer.rb +4 -0
- data/example_app/app/models/application_record.rb +3 -0
- data/example_app/app/models/concerns/.keep +0 -0
- data/example_app/app/models/pet.rb +3 -0
- data/example_app/app/models/user.rb +8 -0
- data/example_app/app/views/layouts/application.html.erb +16 -0
- data/example_app/app/views/layouts/mailer.html.erb +13 -0
- data/example_app/app/views/layouts/mailer.text.erb +1 -0
- data/example_app/app/views/pets/index.html.erb +3 -0
- data/example_app/app/views/users/show.html.erb +3 -0
- data/example_app/bin/bundle +109 -0
- data/example_app/bin/importmap +4 -0
- data/example_app/bin/rails +4 -0
- data/example_app/bin/rake +4 -0
- data/example_app/bin/setup +33 -0
- data/example_app/config/application.rb +22 -0
- data/example_app/config/boot.rb +4 -0
- data/example_app/config/cable.yml +10 -0
- data/example_app/config/credentials.yml.enc +1 -0
- data/example_app/config/database.yml +25 -0
- data/example_app/config/environment.rb +5 -0
- data/example_app/config/environments/development.rb +70 -0
- data/example_app/config/environments/production.rb +93 -0
- data/example_app/config/environments/test.rb +60 -0
- data/example_app/config/importmap.rb +7 -0
- data/example_app/config/initializers/assets.rb +12 -0
- data/example_app/config/initializers/content_security_policy.rb +25 -0
- data/example_app/config/initializers/devise.rb +16 -0
- data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
- data/example_app/config/initializers/inflections.rb +16 -0
- data/example_app/config/initializers/permissions_policy.rb +11 -0
- data/example_app/config/locales/devise.en.yml +65 -0
- data/example_app/config/locales/en.yml +33 -0
- data/example_app/config/puma.rb +43 -0
- data/example_app/config/routes.rb +8 -0
- data/example_app/config/storage.yml +34 -0
- data/example_app/config.ru +6 -0
- data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
- data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
- data/example_app/db/schema.rb +37 -0
- data/example_app/db/seeds.rb +33 -0
- data/example_app/lib/assets/.keep +0 -0
- data/example_app/lib/tasks/.keep +0 -0
- data/example_app/log/.keep +0 -0
- data/example_app/public/404.html +67 -0
- data/example_app/public/422.html +67 -0
- data/example_app/public/500.html +66 -0
- data/example_app/public/apple-touch-icon-precomposed.png +0 -0
- data/example_app/public/apple-touch-icon.png +0 -0
- data/example_app/public/favicon.ico +0 -0
- data/example_app/public/robots.txt +1 -0
- data/example_app/storage/.keep +0 -0
- data/example_app/test/application_system_test_case.rb +5 -0
- data/example_app/test/channels/application_cable/connection_test.rb +11 -0
- data/example_app/test/controllers/.keep +0 -0
- data/example_app/test/fixtures/files/.keep +0 -0
- data/example_app/test/fixtures/users.yml +11 -0
- data/example_app/test/helpers/.keep +0 -0
- data/example_app/test/integration/.keep +0 -0
- data/example_app/test/mailers/.keep +0 -0
- data/example_app/test/models/.keep +0 -0
- data/example_app/test/models/user_test.rb +7 -0
- data/example_app/test/system/.keep +0 -0
- data/example_app/test/test_helper.rb +13 -0
- data/example_app/tmp/.keep +0 -0
- data/example_app/tmp/pids/.keep +0 -0
- data/example_app/tmp/storage/.keep +0 -0
- data/example_app/vendor/.keep +0 -0
- data/example_app/vendor/javascript/.keep +0 -0
- data/lib/active_element/component.rb +9 -2
- data/lib/active_element/components/collection_table.rb +9 -2
- data/lib/active_element/components/email_fields.rb +14 -0
- data/lib/active_element/components/form.rb +48 -17
- data/lib/active_element/components/navbar.rb +64 -0
- data/lib/active_element/components/phone_fields.rb +14 -0
- data/lib/active_element/components/text_search/authorization.rb +9 -6
- data/lib/active_element/components/text_search/component.rb +4 -2
- data/lib/active_element/components/text_search.rb +4 -0
- data/lib/active_element/components/util/association_mapping.rb +74 -19
- data/lib/active_element/components/util/display_value_mapping.rb +13 -4
- data/lib/active_element/components/util/form_field_mapping.rb +127 -10
- data/lib/active_element/components/util/form_value_mapping.rb +3 -3
- data/lib/active_element/components/util/i18n.rb +1 -1
- data/lib/active_element/components/util/record_mapping.rb +43 -11
- data/lib/active_element/components/util/record_path.rb +21 -4
- data/lib/active_element/components/util.rb +12 -5
- data/lib/active_element/components.rb +3 -0
- data/lib/active_element/controller_action.rb +8 -2
- data/lib/active_element/controller_interface.rb +47 -5
- data/lib/active_element/default_controller.rb +93 -0
- data/lib/active_element/default_record_params.rb +62 -0
- data/lib/active_element/default_text_search.rb +110 -0
- data/lib/active_element/json_field_schema.rb +59 -0
- data/lib/active_element/pre_render_processors/json.rb +98 -0
- data/lib/active_element/pre_render_processors.rb +11 -0
- data/lib/active_element/route.rb +12 -0
- data/lib/active_element/routes.rb +2 -1
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +14 -32
- data/lib/tasks/active_element.rake +12 -1
- data/rspec-documentation/_head.html.erb +34 -0
- data/rspec-documentation/pages/000-Introduction.md +18 -0
- data/rspec-documentation/pages/005-Setup.md +75 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
- data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
- data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
- data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
- data/rspec-documentation/pages/010-Components/Forms.md +44 -0
- data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
- data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
- data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
- data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
- data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
- data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
- data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
- data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
- data/rspec-documentation/pages/010-Components/Tables.md +29 -0
- data/rspec-documentation/pages/010-Components.md +15 -0
- data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
- data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
- data/rspec-documentation/pages/020-Access Control.md +31 -0
- data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
- data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
- data/rspec-documentation/pages/040-Decorators.md +12 -0
- data/rspec-documentation/pages/300-Alternatives.md +21 -0
- data/rspec-documentation/pages/900-License.md +11 -0
- data/rspec-documentation/spec_helper.rb +53 -16
- data/rspec-documentation/support.rb +84 -0
- metadata +155 -14
- data/rspec-documentation/pages/Components/Forms.md +0 -1
- data/rspec-documentation/pages/Components/Tables.md +0 -47
- data/rspec-documentation/pages/Components.md +0 -1
- data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
- data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
- data/rspec-documentation/pages/Index.md +0 -3
- data/rspec-documentation/pages/Util/I18n.md +0 -1
- /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -2,6 +2,8 @@ ActiveElement.JsonField = (() => {
|
|
2
2
|
const cloneElement = (id) => ActiveElement.cloneElement('json', id);
|
3
3
|
|
4
4
|
const humanize = ({ string, singular = false }) => {
|
5
|
+
if (!string) return '';
|
6
|
+
|
5
7
|
const humanized = string.split('_').map(item => item.charAt(0).toUpperCase() + item.substring(1)).join(' ');
|
6
8
|
|
7
9
|
if (!singular) return humanized;
|
@@ -9,58 +11,187 @@ ActiveElement.JsonField = (() => {
|
|
9
11
|
return humanized.replace(/s$/, ''); // FIXME: Expose translations from back-end to make this more useful.
|
10
12
|
};
|
11
13
|
|
12
|
-
const
|
13
|
-
const buildState = ({ data, store, path = [] }) => {
|
14
|
-
const getPath = (key) => {
|
15
|
-
return path.concat([key]);
|
16
|
-
};
|
14
|
+
const isObject = (object) => object && typeof object === 'object';
|
17
15
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
16
|
+
const createStore = ({ data, schema, store = { data: {}, paths: {} } }) => {
|
17
|
+
const initializeState = ({ state, path, data, defaultValue }) => {
|
18
|
+
if (state) return state;
|
19
|
+
|
20
|
+
const id = ActiveElement.generateId();
|
21
|
+
store.paths[id] = path;
|
22
|
+
store.data[id] = data === undefined ? (defaultValue || null) : data;
|
23
|
+
return id;
|
24
|
+
};
|
25
|
+
|
26
|
+
const defaultState = ({ schema, path, defaultValue = null }) => {
|
27
|
+
if (schema.type === 'object') {
|
28
|
+
return Object.fromEntries(schema.shape.fields.map((field) => (
|
29
|
+
[field.name, defaultState({
|
30
|
+
schema: field, path: path.concat([field.name]),
|
31
|
+
defaultValue: defaultValue && defaultValue[field.name],
|
32
|
+
})]
|
33
|
+
)));
|
34
|
+
} else if (schema.type === 'array') {
|
35
|
+
return (Array.isArray(defaultValue) ? defaultValue : []).map((item, index) => (
|
36
|
+
defaultState({ schema: schema.shape, path: path.concat([index]), defaultValue: item })
|
37
|
+
));
|
26
38
|
} else {
|
27
|
-
const id =
|
28
|
-
store.data[id] =
|
39
|
+
const id = ActiveElement.generateId();
|
40
|
+
store.data[id] = defaultValue; // TODO: Default value from schema
|
29
41
|
store.paths[id] = path;
|
30
42
|
return id;
|
31
43
|
}
|
32
44
|
};
|
33
45
|
|
34
|
-
|
35
|
-
|
36
|
-
const setValue = (key, value) => store.data[key] = value;
|
46
|
+
store.state = defaultState({ schema, path: [], defaultValue: data });
|
47
|
+
console.log(store.state)
|
37
48
|
|
38
|
-
|
39
|
-
|
49
|
+
const stateChangedCallbacks = [];
|
50
|
+
const stateChanged = (callback, state) => stateChangedCallbacks.push([callback, state]);
|
51
|
+
const notifyStateChanged = () => {
|
52
|
+
stateChangedCallbacks.forEach(([callback, state]) => callback({ getState, value: state && getValue(state) }));
|
53
|
+
};
|
54
|
+
|
55
|
+
const getValueAsDateTime = (value) => {
|
56
|
+
// TODO: Deal with timezone offset ?
|
57
|
+
const datetime = new Date(value);
|
40
58
|
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
59
|
+
// https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/datetime-local
|
60
|
+
const isoString = datetime.toISOString();
|
61
|
+
return isoString.substring(0, isoString.indexOf("T") + 6);
|
62
|
+
};
|
63
|
+
|
64
|
+
const getValueWithSchema = (value, schema) => {
|
65
|
+
switch (schema.type) {
|
66
|
+
case 'datetime':
|
67
|
+
return value ? getValueAsDateTime(value) : '';
|
68
|
+
default:
|
69
|
+
return value;
|
70
|
+
}
|
71
|
+
};
|
72
|
+
|
73
|
+
const getValue = (id, schema) => schema ? getValueWithSchema(store.data[id], schema) : store.data[id];
|
74
|
+
const deleteValue = (state) => {
|
75
|
+
const deleteObject = (id) => {
|
76
|
+
if (Array.isArray(id)) {
|
77
|
+
id.forEach((item) => deleteObject(item));
|
78
|
+
} else if (isObject(id)) {
|
79
|
+
Object.entries(id).forEach(([key, value]) => deleteObject(value));
|
51
80
|
} else {
|
52
|
-
|
53
|
-
|
81
|
+
store.data[id] = undefined;
|
82
|
+
store.paths[id] = undefined;
|
83
|
+
}
|
84
|
+
};
|
85
|
+
|
86
|
+
deleteObject(state);
|
87
|
+
notifyStateChanged();
|
88
|
+
};
|
89
|
+
|
90
|
+
const setValue = (id, value) => {
|
91
|
+
const previousValue = getValue(id);
|
92
|
+
|
93
|
+
if (previousValue !== value) {
|
94
|
+
store.data[id] = value;
|
95
|
+
notifyStateChanged();
|
96
|
+
}
|
97
|
+
};
|
98
|
+
|
99
|
+
const appendValue = ({ path, schema }) => {
|
100
|
+
const getMaxIndex = (path) => {
|
101
|
+
const matchingPaths = Object.values(store.paths).filter((storePath) => {
|
102
|
+
const pathSlice = storePath?.slice(0, path.length);
|
103
|
+
|
104
|
+
return pathSlice && path.every((item, index) => item === pathSlice[index]);
|
105
|
+
});
|
106
|
+
|
107
|
+
if (!matchingPaths.length) return undefined;
|
108
|
+
|
109
|
+
return Math.max(...matchingPaths.map((matchingPath) => matchingPath[path.length]));
|
110
|
+
};
|
111
|
+
|
112
|
+
const id = ActiveElement.generateId();
|
113
|
+
const maxIndex = getMaxIndex(path);
|
114
|
+
const index = maxIndex === undefined ? 0 : maxIndex + 1;
|
115
|
+
const appendPath = path.concat([index]);
|
116
|
+
store.state[id] = defaultState({ schema: schema.shape, path: appendPath });
|
117
|
+
// store.paths[id] = appendPath; // XXX Needed ?
|
118
|
+
return { state: store.state[id], path: appendPath };
|
119
|
+
};
|
120
|
+
|
121
|
+
const getState = () => {
|
122
|
+
const getStructure = ({ path }) => {
|
123
|
+
return path.reduce((structure, key, index) => {
|
124
|
+
let structureField;
|
125
|
+
|
126
|
+
if (structure.type === 'object') {
|
127
|
+
structureField = structure.shape.fields.find((field) => field.name === key);
|
128
|
+
} else if (structure.type === 'array') {
|
129
|
+
structureField = structure.shape;
|
130
|
+
}
|
131
|
+
|
132
|
+
if (index === path.length - 1) {
|
133
|
+
return { array: [], object: {} }[structureField?.type || structure.shape.type];
|
134
|
+
} else {
|
135
|
+
return structureField;
|
136
|
+
}
|
137
|
+
}, schema);
|
138
|
+
};
|
139
|
+
|
140
|
+
const cleanEmpty = ((object) => {
|
141
|
+
if (Array.isArray(object)) {
|
142
|
+
const cleanedArray = Array.from(object.filter((item) => item !== undefined));
|
143
|
+
return cleanedArray.map((item) => cleanEmpty(item));
|
144
|
+
} else if (isObject(object)) {
|
145
|
+
const cleanedObject = Object.fromEntries(
|
146
|
+
Object.entries(object).filter(([key, value]) => value !== undefined)
|
147
|
+
);
|
148
|
+
return Object.fromEntries(
|
149
|
+
Object.entries(cleanedObject).map(([key, value]) => [key, cleanEmpty(value)])
|
150
|
+
);
|
151
|
+
} else {
|
152
|
+
return object;
|
54
153
|
}
|
55
154
|
});
|
56
155
|
|
57
|
-
|
58
|
-
|
59
|
-
|
156
|
+
const data = { array: [], object: {} }[schema.type];
|
157
|
+
|
158
|
+
Object.entries(store.paths).forEach(([id, path]) => {
|
159
|
+
let value = data;
|
60
160
|
|
61
|
-
|
161
|
+
path?.forEach((key, index) => {
|
162
|
+
if (index === path.length - 1) {
|
163
|
+
if (store.data[id] !== undefined) value[key] = store.data[id];
|
164
|
+
} else {
|
165
|
+
value[key] = value[key] || getStructure({ path: path.slice(0, index + 1) });
|
166
|
+
value = value[key];
|
167
|
+
}
|
168
|
+
});
|
169
|
+
});
|
170
|
+
|
171
|
+
return cleanEmpty(data);
|
172
|
+
};
|
173
|
+
|
174
|
+
const handleEvent = (ev) => {
|
175
|
+
const id = ev.target.id;
|
176
|
+
setValue(id, getValueFromElement({ element: ev.target }));
|
177
|
+
|
178
|
+
return true;
|
179
|
+
};
|
180
|
+
|
181
|
+
const connectState = ({ element }) => {
|
182
|
+
element.addEventListener('keyup', (ev) => handleEvent(ev));
|
183
|
+
element.addEventListener('change', (ev) => handleEvent(ev));
|
184
|
+
notifyStateChanged();
|
185
|
+
};
|
186
|
+
|
187
|
+
return {
|
188
|
+
stateChanged,
|
189
|
+
connectState,
|
190
|
+
store: { state: store.state, getValue, setValue, deleteValue, initializeState, appendValue },
|
191
|
+
};
|
62
192
|
};
|
63
193
|
|
194
|
+
|
64
195
|
const getValueFromElement = ({ element }) => {
|
65
196
|
if (element.type === 'checkbox') return element.checked;
|
66
197
|
|
@@ -79,41 +210,39 @@ ActiveElement.JsonField = (() => {
|
|
79
210
|
return ActiveElement.jsonData[dataKey].schema;
|
80
211
|
};
|
81
212
|
|
82
|
-
const
|
83
|
-
|
84
|
-
const
|
85
|
-
const
|
86
|
-
const newValue = getValueFromElement({ element: ev.target });
|
87
|
-
|
88
|
-
if (previousValue !== newValue) {
|
89
|
-
// setValue(key, newValue);
|
90
|
-
// TODO: Trigger callbacks
|
91
|
-
}
|
92
|
-
console.log(`Previous: ${previousValue}`);
|
93
|
-
console.log(`Updated: ${newValue}`);
|
94
|
-
return true;
|
95
|
-
});
|
96
|
-
};
|
213
|
+
const Component = ({ store, stateChanged, connectState, schema, element, fieldName }) => {
|
214
|
+
const ObjectField = ({ schema, state, path, omitLabel = false }) => {
|
215
|
+
const getPath = () => schema.name ? path.concat(schema.name) : path;
|
216
|
+
const currentPath = getPath();
|
97
217
|
|
98
|
-
const Component = ({ getValue, schema, state, element }) => {
|
99
|
-
const ObjectField = ({ schema, state, floating = true, omitLabel = false }) => {
|
100
218
|
let element;
|
219
|
+
|
101
220
|
switch (schema.type) {
|
102
221
|
case 'boolean':
|
103
|
-
return BooleanField({ state, omitLabel, schema });
|
222
|
+
return BooleanField({ state, omitLabel, schema, path: currentPath });
|
104
223
|
case 'string':
|
105
|
-
return StringField({ state, omitLabel,
|
106
|
-
|
224
|
+
return StringField({ state, omitLabel, schema, path: currentPath });
|
225
|
+
case 'date':
|
226
|
+
return DateField({ state, omitLabel, schema, path: currentPath });
|
227
|
+
case 'time':
|
228
|
+
return TimeField({ state, omitLabel, schema, path: currentPath });
|
229
|
+
case 'datetime':
|
230
|
+
return DateTimeField({ state, omitLabel, schema, path: currentPath });
|
231
|
+
case 'integer':
|
232
|
+
return IntegerField({ state, omitLabel, schema, path: currentPath });
|
233
|
+
case 'float':
|
234
|
+
return FloatField({ state, omitLabel, schema, path: currentPath });
|
235
|
+
case 'decimal':
|
236
|
+
return DecimalField({ state, omitLabel, schema, path: currentPath });
|
107
237
|
case 'object':
|
108
238
|
element = cloneElement('form-group-floating');
|
109
|
-
|
110
239
|
(schema.shape.fields).forEach((field) => {
|
111
240
|
element.append(
|
112
241
|
ObjectField({
|
113
242
|
name: field.name,
|
114
|
-
floating: false,
|
115
243
|
schema: field,
|
116
244
|
state: state ? state[field.name] : null,
|
245
|
+
path: currentPath,
|
117
246
|
})
|
118
247
|
);
|
119
248
|
});
|
@@ -121,20 +250,20 @@ ActiveElement.JsonField = (() => {
|
|
121
250
|
return element;
|
122
251
|
case 'array':
|
123
252
|
element = cloneElement('form-group');
|
124
|
-
const list = ArrayField({ schema, state });
|
125
|
-
|
253
|
+
const list = ArrayField({ schema, state, path: currentPath });
|
254
|
+
if (schema.shape?.type === 'object') list.classList.add('array-of-objects');
|
255
|
+
element.append(AppendButton({ list, schema, state, path: currentPath }));
|
126
256
|
element.append(Label({ title: schema.name }));
|
127
257
|
element.append(list);
|
128
|
-
element.append(AppendButton({ list, schema, state }));
|
129
258
|
return element;
|
130
259
|
}
|
131
260
|
};
|
132
261
|
|
133
|
-
const BooleanField = ({ omitLabel, schema, state }) => {
|
262
|
+
const BooleanField = ({ omitLabel, schema, state, path }) => {
|
134
263
|
const checkbox = cloneElement('checkbox-field');
|
135
264
|
|
136
|
-
checkbox.id = state;
|
137
|
-
checkbox.checked = getValue(state);
|
265
|
+
checkbox.id = store.initializeState({ state, path, defaultValue: false });
|
266
|
+
checkbox.checked = store.getValue(state);
|
138
267
|
|
139
268
|
if (omitLabel) return checkbox;
|
140
269
|
|
@@ -146,76 +275,167 @@ ActiveElement.JsonField = (() => {
|
|
146
275
|
return element;
|
147
276
|
};
|
148
277
|
|
149
|
-
const ArrayField = ({ schema, state }) => {
|
278
|
+
const ArrayField = ({ schema, state, path: objectPath }) => {
|
150
279
|
const element = cloneElement('list-group');
|
151
280
|
|
281
|
+
if (schema.focus) element.classList.add('focus');
|
282
|
+
element.classList.add('json-array-field');
|
283
|
+
|
152
284
|
if (state) {
|
153
|
-
state.forEach((
|
154
|
-
const
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
state: value,
|
159
|
-
});
|
285
|
+
state.forEach((eachState, index) => {
|
286
|
+
const path = objectPath.concat([index]);
|
287
|
+
element.append(ArrayItem({ state: eachState, path, schema }));
|
288
|
+
});
|
289
|
+
}
|
160
290
|
|
161
|
-
|
162
|
-
|
163
|
-
group.append(DeleteButton({ rootElement: listItem, template: 'delete-object-button' }));
|
164
|
-
group.append(objectField);
|
165
|
-
listItem.append(group);
|
166
|
-
} else {
|
167
|
-
listItem.append(objectField);
|
168
|
-
listItem.append(DeleteButton({ rootElement: listItem }));
|
169
|
-
}
|
291
|
+
return element;
|
292
|
+
};
|
170
293
|
|
171
|
-
|
172
|
-
|
294
|
+
const ArrayItem = ({ state, path, schema, newItem = false }) => {
|
295
|
+
const element = cloneElement('list-item');
|
296
|
+
const wrapper = document.createElement('div');
|
297
|
+
const objectField = ObjectField({
|
298
|
+
path,
|
299
|
+
omitLabel: true,
|
300
|
+
schema: { ...schema.shape },
|
301
|
+
state: state
|
302
|
+
});
|
303
|
+
|
304
|
+
// TODO: Use same template etc. for all delete buttons, use presentation layer to
|
305
|
+
// handle UI differences.
|
306
|
+
if (schema.shape.type == 'object') {
|
307
|
+
const group = cloneElement('form-group');
|
308
|
+
const deleteObjectButton = DeleteButton(
|
309
|
+
{ path, state, rootElement: element, template: 'delete-object-button' }
|
310
|
+
);
|
311
|
+
|
312
|
+
if (schema.focus) {
|
313
|
+
group.append(objectField);
|
314
|
+
wrapper.append(Focus({ state, schema, group, deleteObjectButton, newItem }));
|
315
|
+
} else {
|
316
|
+
// TODO: Tidy this up.
|
317
|
+
const deleteObjectButtonWrapper = document.createElement('div');
|
318
|
+
deleteObjectButtonWrapper.classList.add('delete-object-button-wrapper');
|
319
|
+
deleteObjectButtonWrapper.append(deleteObjectButton);
|
320
|
+
group.append(deleteObjectButtonWrapper);
|
321
|
+
group.append(objectField);
|
322
|
+
wrapper.append(group);
|
323
|
+
}
|
324
|
+
} else {
|
325
|
+
wrapper.append(objectField);
|
326
|
+
objectField.classList.add('deletable');
|
327
|
+
wrapper.append(DeleteButton({ path, state, rootElement: element }));
|
173
328
|
}
|
174
329
|
|
330
|
+
element.append(wrapper);
|
331
|
+
|
332
|
+
return element;
|
333
|
+
};
|
334
|
+
|
335
|
+
const Focus = ({ state, schema, group, deleteObjectButton, newItem }) => {
|
336
|
+
const element = cloneElement('focus');
|
337
|
+
const valueElement = document.createElement('a');
|
338
|
+
const modal = cloneElement('modal');
|
339
|
+
const modalBody = modal.querySelector('[data-field-type="modal-body"]');
|
340
|
+
const modalHeader = modal.querySelector('.modal-header .modal-buttons');
|
341
|
+
const titleElement = modal.querySelector('[data-field-type="modal-title"]');
|
342
|
+
const bootstrapModal = new bootstrap.Modal(modal);
|
343
|
+
|
344
|
+
stateChanged(() => {
|
345
|
+
const pairs = schema.focus
|
346
|
+
.map((field) => [field, store.getValue(state[field])])
|
347
|
+
.filter(([_field, value]) => value)
|
348
|
+
|
349
|
+
const [field, value] = (pairs.length && pairs[0]) || [null, '[New item]'];
|
350
|
+
const isBoolean = typeof value === 'boolean';
|
351
|
+
const fieldTitle = isBoolean ? humanize({ string: field }) : value;
|
352
|
+
titleElement.innerText = fieldTitle;
|
353
|
+
valueElement.innerText = fieldTitle;
|
354
|
+
if (isBoolean) {
|
355
|
+
valueElement.classList.add('text-success');
|
356
|
+
valueElement.classList.remove('text-primary');
|
357
|
+
} else {
|
358
|
+
valueElement.classList.add('text-primary');
|
359
|
+
valueElement.classList.remove('text-success');
|
360
|
+
}
|
361
|
+
});
|
362
|
+
|
363
|
+
connectState({ element: modal });
|
364
|
+
|
365
|
+
valueElement.classList.add('focus-field-value');
|
366
|
+
valueElement.href = '#';
|
367
|
+
modalBody.append(group);
|
368
|
+
modalBody.classList.add('json-field');
|
369
|
+
titleElement.append(deleteObjectButton);
|
370
|
+
modalHeader.append(deleteObjectButton);
|
371
|
+
deleteObjectButton.addEventListener('click', () => bootstrapModal.hide());
|
372
|
+
|
373
|
+
valueElement.addEventListener('click', (ev) => {
|
374
|
+
ev.preventDefault();
|
375
|
+
bootstrapModal.toggle();
|
376
|
+
});
|
377
|
+
element.append(valueElement);
|
378
|
+
element.classList.add('focus', 'json-highlight');
|
379
|
+
|
380
|
+
if (newItem) bootstrapModal.toggle();
|
381
|
+
|
175
382
|
return element;
|
176
383
|
};
|
177
384
|
|
178
|
-
const Label = ({ title, template }) => {
|
385
|
+
const Label = ({ title, template, labelFor }) => {
|
179
386
|
const element = cloneElement(template || 'label');
|
180
387
|
|
181
388
|
element.append(humanize({ string: title }));
|
182
389
|
|
390
|
+
if (labelFor) {
|
391
|
+
element.htmlFor = labelFor.id;
|
392
|
+
element.classList.add(`json-${labelFor.type}-field-label`);
|
393
|
+
}
|
394
|
+
|
183
395
|
return element;
|
184
396
|
}
|
185
397
|
|
398
|
+
const Option = ({ value, label, selected }) => {
|
399
|
+
const element = document.createElement('option');
|
400
|
+
element.value = value;
|
401
|
+
element.append(label || value);
|
402
|
+
element.selected = selected || false;
|
403
|
+
return element;
|
404
|
+
};
|
405
|
+
|
186
406
|
const Select = ({ state, schema }) => {
|
187
407
|
const element = cloneElement('select')
|
188
408
|
|
189
409
|
element.id = state;
|
190
410
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
optionElement.selected = option === getValue(state);
|
196
|
-
element.append(optionElement);
|
411
|
+
element.append(Option({ value: '' }));
|
412
|
+
|
413
|
+
schema.options.forEach((value) => {
|
414
|
+
element.append(Option({ value, selected: value === store.getValue(state) }));
|
197
415
|
});
|
198
416
|
|
199
417
|
return element;
|
200
418
|
};
|
201
419
|
|
202
|
-
const TextField = ({ template, state, schema }) => {
|
420
|
+
const TextField = ({ template, state, schema, path }) => {
|
203
421
|
const element = cloneElement(template || 'text-field');
|
204
422
|
|
205
|
-
element.value = getValue(state) || '';
|
206
423
|
element.id = state;
|
207
|
-
element.
|
424
|
+
element.value = store.getValue(state);
|
425
|
+
element.placeholder = schema.placeholder || schema.shape?.placeholder || ' ';
|
208
426
|
|
209
427
|
return element;
|
210
428
|
};
|
211
429
|
|
212
|
-
const StringField = ({ omitLabel,
|
430
|
+
const StringField = ({ omitLabel, schema, state, path }) => {
|
213
431
|
let element;
|
214
432
|
|
215
|
-
|
216
|
-
|
433
|
+
state = store.initializeState({ state, path, data: '' });
|
434
|
+
|
435
|
+
if (schema.options?.length) {
|
436
|
+
element = Select({ state, schema, path });
|
217
437
|
} else {
|
218
|
-
element = TextField({ state, schema });
|
438
|
+
element = TextField({ state, schema, path });
|
219
439
|
}
|
220
440
|
|
221
441
|
if (omitLabel) return element;
|
@@ -223,36 +443,67 @@ ActiveElement.JsonField = (() => {
|
|
223
443
|
const group = cloneElement('form-group-floating');
|
224
444
|
|
225
445
|
group.append(element);
|
226
|
-
group.append(Label({ title: schema.name }));
|
446
|
+
group.append(Label({ title: schema.name, labelFor: element }));
|
227
447
|
|
228
448
|
return group;
|
229
449
|
};
|
230
450
|
|
231
|
-
const
|
232
|
-
const
|
451
|
+
const CommonField = ({ template, omitLabel, schema, state, path, floating = true }) => {
|
452
|
+
const element = cloneElement(template);
|
453
|
+
const placeholder = schema.placeholder || schema.shape?.placeholder || ' ';
|
233
454
|
|
234
|
-
|
235
|
-
ev.stopPropagation();
|
236
|
-
element.classList.toggle('collapsed');
|
455
|
+
element.id = state;
|
237
456
|
|
238
|
-
|
239
|
-
button.innerText = 'Show';
|
240
|
-
} else {
|
241
|
-
button.innerText = 'Hide';
|
242
|
-
}
|
457
|
+
if (placeholder) element.placeholder = placeholder;
|
243
458
|
|
244
|
-
|
245
|
-
};
|
459
|
+
element.value = store.getValue(state, schema);
|
246
460
|
|
247
|
-
return
|
461
|
+
if (omitLabel) return element;
|
462
|
+
|
463
|
+
const group = cloneElement('form-group-floating');
|
464
|
+
|
465
|
+
if (floating) {
|
466
|
+
group.append(element);
|
467
|
+
group.append(Label({ title: schema.name, labelFor: element }));
|
468
|
+
} else {
|
469
|
+
group.append(Label({ title: schema.name, labelFor: element }));
|
470
|
+
group.append(element);
|
471
|
+
}
|
472
|
+
|
473
|
+
return group;
|
474
|
+
};
|
475
|
+
|
476
|
+
const DateTimeField = ({ omitLabel, schema, state, path }) => {
|
477
|
+
return CommonField({ template: 'datetime-field', floating: false, omitLabel, schema, state, path });
|
478
|
+
};
|
479
|
+
|
480
|
+
const DateField = ({ omitLabel, schema, state, path }) => {
|
481
|
+
return CommonField({ template: 'date-field', floating: false, omitLabel, schema, state, path });
|
248
482
|
};
|
249
483
|
|
250
|
-
const
|
484
|
+
const TimeField = ({ omitLabel, schema, state, path }) => {
|
485
|
+
return CommonField({ template: 'time-field', floating: false, omitLabel, schema, state, path });
|
486
|
+
};
|
487
|
+
|
488
|
+
const IntegerField = ({ omitLabel, schema, state, path }) => {
|
489
|
+
return CommonField({ template: 'integer-field', omitLabel, schema, state, path });
|
490
|
+
};
|
491
|
+
|
492
|
+
const FloatField = ({ omitLabel, schema, state, path }) => {
|
493
|
+
return CommonField({ template: 'float-field', omitLabel, schema, state, path });
|
494
|
+
};
|
495
|
+
|
496
|
+
const DecimalField = ({ omitLabel, schema, state, path }) => {
|
497
|
+
return CommonField({ template: 'decimal-field', omitLabel, schema, state, path });
|
498
|
+
};
|
499
|
+
|
500
|
+
const DeleteButton = ({ path, state, rootElement, template = 'delete-button' }) => {
|
251
501
|
const element = cloneElement(template);
|
252
502
|
|
253
503
|
element.onclick = (ev) => {
|
254
|
-
ev.
|
255
|
-
rootElement.remove();
|
504
|
+
ev.preventDefault();
|
505
|
+
rootElement.remove(); // TODO: Handle confirmation callback.
|
506
|
+
store.deleteValue(state);
|
256
507
|
|
257
508
|
return false;
|
258
509
|
};
|
@@ -260,27 +511,20 @@ ActiveElement.JsonField = (() => {
|
|
260
511
|
return element;
|
261
512
|
};
|
262
513
|
|
263
|
-
const AppendButton = ({ list, schema, state }) => {
|
514
|
+
const AppendButton = ({ list, schema, state, path: objectPath }) => {
|
264
515
|
const element = cloneElement('append-button');
|
265
|
-
|
266
|
-
const humanName = humanize({ string: schema.name, singular: true });
|
516
|
+
const humanName = humanize({ string: schema.name || fieldName, singular: true });
|
267
517
|
|
268
518
|
element.append(`Add ${humanName}`);
|
519
|
+
element.classList.add('append-button', 'float-end');
|
269
520
|
element.onclick = (ev) => {
|
270
|
-
ev.
|
271
|
-
const listItem = cloneElement('list-item');
|
272
|
-
const objectField = ObjectField(
|
273
|
-
{ name: schema.name, omitLabel: true, state, schema: { ...schema, ...schema.shape } }
|
274
|
-
);
|
521
|
+
ev.preventDefault();
|
275
522
|
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
listItem.append(DeleteButton({ rootElement: listItem }));
|
282
|
-
}
|
283
|
-
list.append(listItem);
|
523
|
+
const { path, state: appendState } = store.appendValue({ path: objectPath, schema });
|
524
|
+
const item = ArrayItem({ path, state: appendState, schema, newItem: true })
|
525
|
+
|
526
|
+
list.append(item);
|
527
|
+
item.scrollIntoView();
|
284
528
|
|
285
529
|
return false;
|
286
530
|
};
|
@@ -288,18 +532,30 @@ ActiveElement.JsonField = (() => {
|
|
288
532
|
return element;
|
289
533
|
};
|
290
534
|
|
291
|
-
element.append(ObjectField({ omitLabel: true,
|
535
|
+
element.append(ObjectField({ schema, omitLabel: true, state: store.state, path: [] }));
|
292
536
|
};
|
293
537
|
|
294
538
|
const JsonField = (element) => {
|
295
539
|
const data = getData(element);
|
540
|
+
const formId = element.dataset.formId;
|
541
|
+
const formFieldElement = document.querySelector(`#${element.dataset.fieldId}`);
|
542
|
+
const schemaFieldElement = document.querySelector(`#${element.dataset.schemaFieldId}`);
|
543
|
+
const fieldName = element.dataset.fieldName;
|
296
544
|
const schema = getSchema(element);
|
297
|
-
const {
|
545
|
+
const { store, stateChanged, connectState } = createStore({ data, schema });
|
546
|
+
|
547
|
+
schemaFieldElement.value = JSON.stringify(schema);
|
548
|
+
|
549
|
+
stateChanged(({ getState }) => {
|
550
|
+
const state = getState();
|
551
|
+
|
552
|
+
formFieldElement.value = JSON.stringify(state);
|
553
|
+
ActiveElement.log.debug(state);
|
554
|
+
});
|
298
555
|
|
299
|
-
|
556
|
+
connectState({ element });
|
300
557
|
|
301
|
-
|
302
|
-
const component = Component({ getValue, schema, state, element });
|
558
|
+
const component = Component({ store, stateChanged, connectState, schema, element, fieldName });
|
303
559
|
|
304
560
|
return component;
|
305
561
|
};
|