active_element 0.0.1 → 0.0.3

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/.strong_versions.yml +2 -0
  4. data/Gemfile +10 -2
  5. data/Gemfile.lock +229 -4
  6. data/Rakefile +1 -0
  7. data/active_element.gemspec +7 -0
  8. data/app/assets/config/active_element/manifest.js +2 -0
  9. data/app/assets/javascripts/active_element/application.js +10 -0
  10. data/app/assets/javascripts/active_element/confirm.js +67 -0
  11. data/app/assets/javascripts/active_element/form.js +61 -0
  12. data/app/assets/javascripts/active_element/json_field.js +316 -0
  13. data/app/assets/javascripts/active_element/pagination.js +18 -0
  14. data/app/assets/javascripts/active_element/search_field.js +127 -0
  15. data/app/assets/javascripts/active_element/secret.js +40 -0
  16. data/app/assets/javascripts/active_element/setup.js +36 -0
  17. data/app/assets/javascripts/active_element/theme.js +42 -0
  18. data/app/assets/stylesheets/active_element/_variables.scss +142 -0
  19. data/app/assets/stylesheets/active_element/application.scss +77 -0
  20. data/app/controllers/active_element/application_controller.rb +41 -0
  21. data/app/controllers/active_element/text_searches_controller.rb +189 -0
  22. data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
  23. data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
  24. data/app/views/active_element/components/button.html.erb +27 -0
  25. data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
  26. data/app/views/active_element/components/form/_check_box.html.erb +3 -0
  27. data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
  28. data/app/views/active_element/components/form/_field.html.erb +28 -0
  29. data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
  30. data/app/views/active_element/components/form/_json.html.erb +12 -0
  31. data/app/views/active_element/components/form/_label.html.erb +17 -0
  32. data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
  33. data/app/views/active_element/components/form/_select.html.erb +4 -0
  34. data/app/views/active_element/components/form/_summary.html.erb +40 -0
  35. data/app/views/active_element/components/form/_templates.html.erb +85 -0
  36. data/app/views/active_element/components/form/_text_area.html.erb +4 -0
  37. data/app/views/active_element/components/form/_text_search.html.erb +16 -0
  38. data/app/views/active_element/components/form.html.erb +78 -0
  39. data/app/views/active_element/components/json.html.erb +8 -0
  40. data/app/views/active_element/components/page_description.html.erb +3 -0
  41. data/app/views/active_element/components/secret/_field.html.erb +1 -0
  42. data/app/views/active_element/components/secret/_templates.html.erb +11 -0
  43. data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
  44. data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
  45. data/app/views/active_element/components/table/_pagination.html.erb +17 -0
  46. data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
  47. data/app/views/active_element/components/table/collection.html.erb +39 -0
  48. data/app/views/active_element/components/table/item.html.erb +39 -0
  49. data/app/views/active_element/components/tabs.html.erb +7 -0
  50. data/app/views/active_element/decorators/_boolean.html.erb +5 -0
  51. data/app/views/active_element/decorators/_date.html.erb +3 -0
  52. data/app/views/active_element/decorators/_datetime.html.erb +3 -0
  53. data/app/views/active_element/decorators/_time.html.erb +3 -0
  54. data/app/views/active_element/forbidden.html.erb +33 -0
  55. data/app/views/active_element/main_menu/_item.html.erb +9 -0
  56. data/app/views/active_element/navbar/_menu.html.erb +30 -0
  57. data/app/views/active_element/theme/_select.html.erb +1 -0
  58. data/app/views/active_element/theme/_templates.html.erb +6 -0
  59. data/app/views/kaminari/_first_page.html.erb +3 -0
  60. data/app/views/kaminari/_gap.html.erb +3 -0
  61. data/app/views/kaminari/_last_page.html.erb +3 -0
  62. data/app/views/kaminari/_next_page.html.erb +3 -0
  63. data/app/views/kaminari/_page.html.erb +9 -0
  64. data/app/views/kaminari/_paginator.html.erb +17 -0
  65. data/app/views/kaminari/_prev_page.html.erb +3 -0
  66. data/app/views/layouts/active_element.html.erb +65 -0
  67. data/app/views/layouts/active_element_error.html.erb +40 -0
  68. data/config/routes.rb +5 -0
  69. data/lib/active_element/active_menu_link.rb +80 -0
  70. data/lib/active_element/active_record_text_search_authorization.rb +12 -0
  71. data/lib/active_element/colorized_string.rb +33 -0
  72. data/lib/active_element/component.rb +122 -0
  73. data/lib/active_element/components/button.rb +156 -0
  74. data/lib/active_element/components/collection_table.rb +118 -0
  75. data/lib/active_element/components/form.rb +210 -0
  76. data/lib/active_element/components/item_table.rb +57 -0
  77. data/lib/active_element/components/json.rb +31 -0
  78. data/lib/active_element/components/link_helpers.rb +9 -0
  79. data/lib/active_element/components/page_description.rb +28 -0
  80. data/lib/active_element/components/secret_fields.rb +15 -0
  81. data/lib/active_element/components/tab.rb +37 -0
  82. data/lib/active_element/components/tabs.rb +35 -0
  83. data/lib/active_element/components/translations.rb +18 -0
  84. data/lib/active_element/components/util/association_mapping.rb +80 -0
  85. data/lib/active_element/components/util/decorator.rb +107 -0
  86. data/lib/active_element/components/util/display_value_mapping.rb +48 -0
  87. data/lib/active_element/components/util/field_mapping.rb +144 -0
  88. data/lib/active_element/components/util/form_field_mapping.rb +104 -0
  89. data/lib/active_element/components/util/form_value_mapping.rb +49 -0
  90. data/lib/active_element/components/util/i18n.rb +66 -0
  91. data/lib/active_element/components/util/record_mapping.rb +111 -0
  92. data/lib/active_element/components/util.rb +43 -0
  93. data/lib/active_element/components.rb +20 -0
  94. data/lib/active_element/controller_action.rb +91 -0
  95. data/lib/active_element/engine.rb +26 -0
  96. data/lib/active_element/permissions_check.rb +101 -0
  97. data/lib/active_element/rails_component.rb +40 -0
  98. data/lib/active_element/route.rb +112 -0
  99. data/lib/active_element/routes.rb +62 -0
  100. data/lib/active_element/version.rb +1 -1
  101. data/lib/active_element.rb +91 -1
  102. data/lib/tasks/active_element.rake +23 -0
  103. data/rspec-documentation/dummy +1 -0
  104. data/rspec-documentation/pages/Components/Forms.md +1 -0
  105. data/rspec-documentation/pages/Components/Tables.md +47 -0
  106. data/rspec-documentation/pages/Components/Tabs.md +1 -0
  107. data/rspec-documentation/pages/Components.md +1 -0
  108. data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
  109. data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
  110. data/rspec-documentation/pages/Index.md +3 -0
  111. data/rspec-documentation/pages/Util/I18n.md +1 -0
  112. data/rspec-documentation/spec_helper.rb +35 -0
  113. metadata +191 -3
@@ -0,0 +1,316 @@
1
+ ActiveElement.JsonField = (() => {
2
+ const cloneElement = (id) => ActiveElement.cloneElement('json', id);
3
+
4
+ const humanize = ({ string, singular = false }) => {
5
+ const humanized = string.split('_').map(item => item.charAt(0).toUpperCase() + item.substring(1)).join(' ');
6
+
7
+ if (!singular) return humanized;
8
+
9
+ return humanized.replace(/s$/, ''); // FIXME: Expose translations from back-end to make this more useful.
10
+ };
11
+
12
+ const createStore = ({ data, store = { data: {}, paths: {} } }) => {
13
+ const buildState = ({ data, store, path = [] }) => {
14
+ const getPath = (key) => {
15
+ return path.concat([key]);
16
+ };
17
+
18
+ if (Array.isArray(data)) {
19
+ return data.map((value, index) => buildState({ data: value, store, path: getPath(index) }));
20
+ } else if (data && typeof(data) === 'object') {
21
+ return Object.fromEntries(
22
+ Object.entries(data).map(
23
+ ([key, value]) => [key, buildState({ data: value, store, path: getPath(key) })]
24
+ )
25
+ );
26
+ } else {
27
+ const id = crypto.randomUUID();
28
+ store.data[id] = data;
29
+ store.paths[id] = path;
30
+ return id;
31
+ }
32
+ };
33
+
34
+ const state = buildState({ data, store });
35
+ const getValue = (key) => store.data[key];
36
+ const setValue = (key, value) => store.data[key] = value;
37
+
38
+ return { state, store, getValue, setValue };
39
+ };
40
+
41
+ const getState = ({ store }) => {
42
+ const data = {};
43
+ const storeData = Object.entries(store.paths).forEach(([id, path]) => {
44
+ let value = data;
45
+ path.forEach((key, index) => {
46
+ if (index === path.length - 1) {
47
+ value[key] = store.data[id];
48
+ } else if (typeof(key) === 'string') {
49
+ value[key] = value[key] || {};
50
+ value = value[key];
51
+ } else {
52
+ value[key] = value[key] || [];
53
+ value = value[key];
54
+ }
55
+ });
56
+
57
+ return value;
58
+ },
59
+ {});
60
+
61
+ return data;
62
+ };
63
+
64
+ const getValueFromElement = ({ element }) => {
65
+ if (element.type === 'checkbox') return element.checked;
66
+
67
+ return element.value;
68
+ };
69
+
70
+ const getData = (element) => {
71
+ const dataKey = element.dataset.dataKey;
72
+
73
+ return ActiveElement.jsonData[dataKey].data;
74
+ };
75
+
76
+ const getSchema = (element) => {
77
+ const dataKey = element.dataset.dataKey;
78
+
79
+ return ActiveElement.jsonData[dataKey].schema;
80
+ };
81
+
82
+ const trackState = ({ element, schema, getValue }) => {
83
+ element.addEventListener('change', (ev) => {
84
+ const key = ev.target.id;
85
+ const previousValue = getValue(key);
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
+ };
97
+
98
+ const Component = ({ getValue, schema, state, element }) => {
99
+ const ObjectField = ({ schema, state, floating = true, omitLabel = false }) => {
100
+ let element;
101
+ switch (schema.type) {
102
+ case 'boolean':
103
+ return BooleanField({ state, omitLabel, schema });
104
+ case 'string':
105
+ return StringField({ state, omitLabel, floating, schema });
106
+ break;
107
+ case 'object':
108
+ element = cloneElement('form-group-floating');
109
+
110
+ (schema.shape.fields).forEach((field) => {
111
+ element.append(
112
+ ObjectField({
113
+ name: field.name,
114
+ floating: false,
115
+ schema: field,
116
+ state: state ? state[field.name] : null,
117
+ })
118
+ );
119
+ });
120
+
121
+ return element;
122
+ case 'array':
123
+ element = cloneElement('form-group');
124
+ const list = ArrayField({ schema, state });
125
+ element.append(ExpandCollapseButton({ element }));
126
+ element.append(Label({ title: schema.name }));
127
+ element.append(list);
128
+ element.append(AppendButton({ list, schema, state }));
129
+ return element;
130
+ }
131
+ };
132
+
133
+ const BooleanField = ({ omitLabel, schema, state }) => {
134
+ const checkbox = cloneElement('checkbox-field');
135
+
136
+ checkbox.id = state;
137
+ checkbox.checked = getValue(state);
138
+
139
+ if (omitLabel) return checkbox;
140
+
141
+ const element = cloneElement('form-check');
142
+
143
+ element.append(checkbox);
144
+ element.append(Label({ title: schema.name, template: 'form-check-label' }));
145
+
146
+ return element;
147
+ };
148
+
149
+ const ArrayField = ({ schema, state }) => {
150
+ const element = cloneElement('list-group');
151
+
152
+ if (state) {
153
+ state.forEach((value) => {
154
+ const listItem = cloneElement('list-item');
155
+ const objectField = ObjectField({
156
+ omitLabel: true,
157
+ schema: { ...schema, ...schema.shape },
158
+ state: value,
159
+ });
160
+
161
+ if (schema.shape.type == 'object') {
162
+ const group = cloneElement('form-group');
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
+ }
170
+
171
+ element.append(listItem);
172
+ });
173
+ }
174
+
175
+ return element;
176
+ };
177
+
178
+ const Label = ({ title, template }) => {
179
+ const element = cloneElement(template || 'label');
180
+
181
+ element.append(humanize({ string: title }));
182
+
183
+ return element;
184
+ }
185
+
186
+ const Select = ({ state, schema }) => {
187
+ const element = cloneElement('select')
188
+
189
+ element.id = state;
190
+
191
+ schema.shape.options.forEach((option) => {
192
+ const optionElement = document.createElement('option');
193
+ optionElement.value = option;
194
+ optionElement.append(option);
195
+ optionElement.selected = option === getValue(state);
196
+ element.append(optionElement);
197
+ });
198
+
199
+ return element;
200
+ };
201
+
202
+ const TextField = ({ template, state, schema }) => {
203
+ const element = cloneElement(template || 'text-field');
204
+
205
+ element.value = getValue(state) || '';
206
+ element.id = state;
207
+ element.placeholder = schema.shape?.placeholder || ' ';
208
+
209
+ return element;
210
+ };
211
+
212
+ const StringField = ({ omitLabel, floating, schema, state }) => {
213
+ let element;
214
+
215
+ if (schema.shape?.options?.length) {
216
+ element = Select({ state, schema });
217
+ } else {
218
+ element = TextField({ state, schema });
219
+ }
220
+
221
+ if (omitLabel) return element;
222
+
223
+ const group = cloneElement('form-group-floating');
224
+
225
+ group.append(element);
226
+ group.append(Label({ title: schema.name }));
227
+
228
+ return group;
229
+ };
230
+
231
+ const ExpandCollapseButton = ({ element }) => {
232
+ const button = cloneElement('expand-collapse-button');
233
+
234
+ button.onclick = (ev) => {
235
+ ev.stopPropagation();
236
+ element.classList.toggle('collapsed');
237
+
238
+ if (element.classList.contains('collapsed')) {
239
+ button.innerText = 'Show';
240
+ } else {
241
+ button.innerText = 'Hide';
242
+ }
243
+
244
+ return false;
245
+ };
246
+
247
+ return button;
248
+ };
249
+
250
+ const DeleteButton = ({ rootElement, template = 'delete-button' }) => {
251
+ const element = cloneElement(template);
252
+
253
+ element.onclick = (ev) => {
254
+ ev.stopPropagation();
255
+ rootElement.remove();
256
+
257
+ return false;
258
+ };
259
+
260
+ return element;
261
+ };
262
+
263
+ const AppendButton = ({ list, schema, state }) => {
264
+ const element = cloneElement('append-button');
265
+
266
+ const humanName = humanize({ string: schema.name, singular: true });
267
+
268
+ element.append(`Add ${humanName}`);
269
+ element.onclick = (ev) => {
270
+ ev.stopPropagation();
271
+ const listItem = cloneElement('list-item');
272
+ const objectField = ObjectField(
273
+ { name: schema.name, omitLabel: true, state, schema: { ...schema, ...schema.shape } }
274
+ );
275
+
276
+ if (schema.shape.type == 'object') {
277
+ listItem.append(DeleteButton({ rootElement: listItem, template: 'delete-object-button' }));
278
+ listItem.append(objectField);
279
+ } else {
280
+ listItem.append(objectField);
281
+ listItem.append(DeleteButton({ rootElement: listItem }));
282
+ }
283
+ list.append(listItem);
284
+
285
+ return false;
286
+ };
287
+
288
+ return element;
289
+ };
290
+
291
+ element.append(ObjectField({ omitLabel: true, schema, state, getValue }));
292
+ };
293
+
294
+ const JsonField = (element) => {
295
+ const data = getData(element);
296
+ const schema = getSchema(element);
297
+ const { state, store, getValue } = createStore({ data });
298
+
299
+ console.log(getState({ store }));
300
+
301
+ trackState({ element, schema, getValue });
302
+ const component = Component({ getValue, schema, state, element });
303
+
304
+ return component;
305
+ };
306
+
307
+ return JsonField;
308
+ })();
309
+
310
+ (() => {
311
+ window.addEventListener('DOMContentLoaded', () => {
312
+ document.querySelectorAll('.json-field').forEach((element) => {
313
+ ActiveElement.JsonField(element);
314
+ });
315
+ });
316
+ })();
@@ -0,0 +1,18 @@
1
+ (() => {
2
+ window.addEventListener('DOMContentLoaded', () => {
3
+ const paginationSelect = document.querySelector('#collection-table-page-size-selector');
4
+
5
+ if (paginationSelect) {
6
+ paginationSelect.addEventListener('change', (ev) => {
7
+ ev.stopPropagation();
8
+
9
+ const params = new URLSearchParams(window.location.search);
10
+
11
+ params.set('page_size', ev.target.value);
12
+ window.location.search = params.toString();
13
+
14
+ return false;
15
+ });
16
+ }
17
+ });
18
+ })();
@@ -0,0 +1,127 @@
1
+ (() => {
2
+ let lastRequestId;
3
+
4
+ const cloneElement = (id) => ActiveElement.cloneElement('form-search-field', id);
5
+
6
+ const tryParseJSON = (json, defaultValue) => {
7
+ try {
8
+ return JSON.parse(json);
9
+ } catch (error) {
10
+ ActiveElement.log(error);
11
+ return defaultValue;
12
+ }
13
+ };
14
+
15
+ const processResponse = ({
16
+ element, response, hiddenInput, spinner, clearButton, searchResultsContainer, responseErrorContainer
17
+ }) => {
18
+ spinner.classList.add('invisible');
19
+ clearButton.classList.remove('invisible');
20
+
21
+ if (response.ok) {
22
+ response.json().then((json) => {
23
+ if (json.request_id !== lastRequestId) {
24
+ return;
25
+ }
26
+
27
+ responseErrorContainer.innerText = '';
28
+
29
+ if (!json.results.length) {
30
+ responseErrorContainer.innerText = `No matching results for ${element.value}`;
31
+ return;
32
+ }
33
+
34
+ json.results.forEach(({ value, attributes }) => {
35
+ const resultsItem = cloneElement('results-item');
36
+
37
+ resultsItem.innerText = attributes.length === 0 ? value : `${attributes.join(', ')} (${value})`;
38
+ resultsItem.addEventListener('click', () => {
39
+ hiddenInput.value = value;
40
+ element.value = attributes.length == 0 ? value : `${attributes.join(', ')} (${value})`;
41
+ searchResultsContainer.replaceChildren();
42
+ searchResultsContainer.classList.add('d-none');
43
+ });
44
+ searchResultsContainer.append(resultsItem);
45
+ });
46
+
47
+ searchResultsContainer.classList.remove('d-none');
48
+ });
49
+ } else {
50
+ response.json().then((json) => responseErrorContainer.innerText = json.message)
51
+ .catch(() => responseErrorContainer.innerText = 'An unepxected error occurred');
52
+ }
53
+ };
54
+
55
+ window.addEventListener('DOMContentLoaded', () => {
56
+ document.querySelectorAll('[data-field-type="text-search"]').forEach((element) => {
57
+ const id = element.id;
58
+ const hiddenId = `${id}-hidden-value`;
59
+ const formId = element.dataset.formId;
60
+ const form = document.querySelector(`#${formId}`);
61
+ const model = element.dataset.searchModel;
62
+ const attributes = tryParseJSON(element.dataset.searchAttributes, []);
63
+ const value = element.dataset.searchValue;
64
+ const token = ActiveElement.getAntiCsrfToken();
65
+ const hiddenInput = cloneElement('hidden-input');
66
+ const responseErrorContainer = cloneElement('response-error');
67
+ const searchResultsContainer = cloneElement('results');
68
+ const spinner = cloneElement('spinner');
69
+ const clearButton = cloneElement('clear-button');
70
+ document.addEventListener('click', () => {
71
+ searchResultsContainer.classList.add('d-none');
72
+ });
73
+
74
+ clearButton.addEventListener('click', () => {
75
+ element.value = '';
76
+ hiddenInput.value = '';
77
+ responseErrorContainer.innerText = '';
78
+ spinner.classList.add('invisible');
79
+ clearButton.classList.add('invisible');
80
+ });
81
+
82
+ element.addEventListener('keyup', () => {
83
+ const query = element.value;
84
+ const requestId = crypto.randomUUID();
85
+ lastRequestId = requestId;
86
+
87
+ clearButton.classList.add('invisible');
88
+ spinner.classList.remove('invisible');
89
+ hiddenInput.value = query;
90
+ searchResultsContainer.classList.add('d-none');
91
+
92
+ if (!query || query.length < 3) {
93
+ spinner.classList.add('invisible');
94
+ return;
95
+ }
96
+
97
+ searchResultsContainer.replaceChildren();
98
+
99
+ fetch(
100
+ '/_text_search/',
101
+ {
102
+ method: 'POST',
103
+ headers: { 'Content-Type': 'application/json' },
104
+ body: JSON.stringify({
105
+ request_id: requestId,
106
+ model,
107
+ value,
108
+ attributes,
109
+ query,
110
+ [token.param]: token.value,
111
+ }),
112
+ }
113
+ ).then((response) => processResponse(
114
+ { element, response, spinner, clearButton, hiddenInput, searchResultsContainer, responseErrorContainer }
115
+ ));
116
+ });
117
+
118
+ hiddenInput.name = element.name;
119
+ if (element.value) hiddenInput.value = element.value;
120
+ form.append(hiddenInput);
121
+ element.parentElement.append(searchResultsContainer);
122
+ element.parentElement.append(clearButton);
123
+ element.parentElement.append(spinner);
124
+ element.parentElement.append(responseErrorContainer);
125
+ });
126
+ });
127
+ })();
@@ -0,0 +1,40 @@
1
+ (() => {
2
+ const cloneElement = (id) => ActiveElement.cloneElement('secret', id);
3
+
4
+ window.addEventListener('DOMContentLoaded', () => {
5
+ document.querySelectorAll('span[data-field-type="secret"]').forEach((element) => {
6
+ const secret = element.dataset.secret;
7
+ const showButton = cloneElement('show-button');
8
+ const hideButton = cloneElement('hide-button');
9
+ const content = cloneElement('content');
10
+ const placeholder = secret.replace(/./g, '*');
11
+
12
+ hideButton.classList.add('d-none');
13
+ content.classList.add('font-monospace');
14
+ content.classList.add('text-secondary');
15
+ content.innerText = placeholder;
16
+
17
+ showButton.addEventListener('click', () => {
18
+ showButton.classList.add('d-none');
19
+ hideButton.classList.remove('d-none');
20
+ content.classList.remove('text-secondary');
21
+ content.innerText = secret;
22
+
23
+ return false;
24
+ });
25
+
26
+ hideButton.addEventListener('click', () => {
27
+ showButton.classList.remove('d-none');
28
+ hideButton.classList.add('d-none');
29
+ content.classList.add('text-secondary');
30
+ content.innerText = placeholder;
31
+
32
+ return false;
33
+ });
34
+
35
+ element.append(content);
36
+ element.append(showButton);
37
+ element.append(hideButton);
38
+ });
39
+ });
40
+ })();
@@ -0,0 +1,36 @@
1
+ (() => {
2
+ const generateId = () => {
3
+ ActiveElement._id += 1;
4
+
5
+ return `active-element-element-${ActiveElement._id}`;
6
+ };
7
+
8
+ const getAntiCsrfToken = () => {
9
+ const param = document.querySelector('meta[name="csrf-param"]').content;
10
+ const value = document.querySelector('meta[name="csrf-token"]').content;
11
+
12
+ return { param, value };
13
+ };
14
+
15
+ const cloneElement = (category, id) => {
16
+ const element = document.querySelector(`#${category}-templates`)
17
+ .querySelector(`#${category}-${id}-template`)
18
+ .cloneNode(true);
19
+ element.id = ActiveElement.generateId();
20
+ return element;
21
+ };
22
+
23
+ const ActiveElement = {
24
+ log: (message) => { console.log(`[ActiveElement] ${message}`); },
25
+ _id: 0,
26
+ generateId,
27
+ getAntiCsrfToken,
28
+ cloneElement,
29
+ components: {},
30
+ jsonData: {},
31
+ };
32
+
33
+ window.ActiveElement = ActiveElement;
34
+ })();
35
+
36
+ ActiveElement.log('Initialized');
@@ -0,0 +1,42 @@
1
+ (() => {
2
+ const cloneElement = (id) => ActiveElement.cloneElement('theme', id);
3
+
4
+ window.addEventListener('DOMContentLoaded', () => {
5
+ const themeSelect = document.querySelector('#theme-select');
6
+
7
+ const setTheme = (theme) => {
8
+ const themeSelectButtons = themeSelect.children;
9
+
10
+ Object.entries(themeSelectButtons).forEach(([_, element]) => {
11
+ if (element.dataset.themeSwitchTo === theme) {
12
+ element.classList.add('d-none');
13
+ } else {
14
+ element.classList.remove('d-none');
15
+ }
16
+ });
17
+
18
+ localStorage.setItem('active_element-theme', theme);
19
+ document.querySelector('html').dataset.bsTheme = theme;
20
+ };
21
+
22
+
23
+ const initTheme = () => {
24
+ const theme = localStorage.getItem('active_element-theme') || 'light';
25
+ const themeSelectButtons = document.querySelector('#theme-select-buttons').children;
26
+
27
+ Object.entries(themeSelectButtons).forEach(([_, element]) => {
28
+ themeSelect.append(element);
29
+
30
+ element.addEventListener('click', (ev) => {
31
+ event.stopPropagation();
32
+ setTheme(element.dataset.themeSwitchTo);
33
+ return false;
34
+ });
35
+ });
36
+
37
+ setTheme(theme);
38
+ };
39
+
40
+ initTheme();
41
+ });
42
+ })();