super_settings 0.0.0.rc1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +9 -0
  3. data/MIT-LICENSE +20 -0
  4. data/README.md +313 -0
  5. data/VERSION +1 -0
  6. data/app/helpers/super_settings/settings_helper.rb +32 -0
  7. data/app/views/layouts/super_settings/settings.html.erb +20 -0
  8. data/config/routes.rb +13 -0
  9. data/db/migrate/20210414004553_create_super_settings.rb +34 -0
  10. data/lib/super_settings/application/api.js +88 -0
  11. data/lib/super_settings/application/helper.rb +119 -0
  12. data/lib/super_settings/application/images/edit.svg +1 -0
  13. data/lib/super_settings/application/images/info.svg +1 -0
  14. data/lib/super_settings/application/images/plus.svg +1 -0
  15. data/lib/super_settings/application/images/slash.svg +1 -0
  16. data/lib/super_settings/application/images/trash.svg +1 -0
  17. data/lib/super_settings/application/index.html.erb +169 -0
  18. data/lib/super_settings/application/layout.html.erb +22 -0
  19. data/lib/super_settings/application/layout_styles.css +193 -0
  20. data/lib/super_settings/application/scripts.js +718 -0
  21. data/lib/super_settings/application/styles.css +122 -0
  22. data/lib/super_settings/application.rb +38 -0
  23. data/lib/super_settings/attributes.rb +24 -0
  24. data/lib/super_settings/coerce.rb +66 -0
  25. data/lib/super_settings/configuration.rb +144 -0
  26. data/lib/super_settings/controller_actions.rb +81 -0
  27. data/lib/super_settings/encryption.rb +76 -0
  28. data/lib/super_settings/engine.rb +70 -0
  29. data/lib/super_settings/history_item.rb +26 -0
  30. data/lib/super_settings/local_cache.rb +306 -0
  31. data/lib/super_settings/rack_middleware.rb +210 -0
  32. data/lib/super_settings/rest_api.rb +195 -0
  33. data/lib/super_settings/setting.rb +599 -0
  34. data/lib/super_settings/storage/active_record_storage.rb +123 -0
  35. data/lib/super_settings/storage/http_storage.rb +279 -0
  36. data/lib/super_settings/storage/redis_storage.rb +293 -0
  37. data/lib/super_settings/storage/test_storage.rb +158 -0
  38. data/lib/super_settings/storage.rb +254 -0
  39. data/lib/super_settings/version.rb +5 -0
  40. data/lib/super_settings.rb +213 -0
  41. data/lib/tasks/super_settings.rake +9 -0
  42. data/super_settings.gemspec +35 -0
  43. metadata +113 -0
@@ -0,0 +1,718 @@
1
+ (function() {
2
+ // Return the table row element for a setting.
3
+ function findSettingRow(id) {
4
+ if (id) {
5
+ return document.querySelector('#settings-table tr[data-id="' + id + '"]');
6
+ } else {
7
+ return null;
8
+ }
9
+ }
10
+
11
+ // Return the number of settings that have been edited.
12
+ function changesCount() {
13
+ return document.querySelectorAll("#settings-table tbody tr[data-edited=true]").length;
14
+ }
15
+
16
+ // Set the enabled status of the save button for submitting the form.
17
+ function enableSaveButton() {
18
+ const saveButton = document.querySelector("#save-settings");
19
+ const discardButton = document.querySelector("#discard-changes");
20
+ if (saveButton) {
21
+ const count = changesCount();
22
+ const countSpan = saveButton.querySelector(".count");
23
+ if (count === 0) {
24
+ saveButton.disabled = true;
25
+ countSpan.innerHTML = "";
26
+ discardButton.disabled = true;
27
+ } else {
28
+ saveButton.disabled = false;
29
+ countSpan.innerHTML = count;
30
+ discardButton.disabled = false;
31
+ }
32
+ }
33
+ }
34
+
35
+ // Set the display value for a setting.
36
+ function setSettingDisplayValue(element, setting) {
37
+ if (setting.value === null || setting.value === undefined) {
38
+ element.innerText = "";
39
+ } else if (Array.isArray(setting.value)) {
40
+ let arrayHTML = "";
41
+ setting.value.map(function(val) {
42
+ arrayHTML += `<div>${escapeHTML(val)}</div>`;
43
+ });
44
+ element.innerHTML = arrayHTML;
45
+ } else if (setting.value_type === "datetime") {
46
+ try {
47
+ const datetime = new Date(Date.parse(setting.value));
48
+ element.innerText = datetime.toUTCString().replace("GMT", "UTC");
49
+ } catch (e) {
50
+ element.innerText = "" + setting.value
51
+ }
52
+ } else if (setting.value_type === "secret") {
53
+ let placeholder = "••••••••••••••••••••••••";
54
+ if (!setting.encrypted) {
55
+ placeholder += '<br><span class="text-danger">not encrypted</span>'
56
+ }
57
+ element.innerHTML = placeholder;
58
+ } else {
59
+ element.innerText = "" + setting.value
60
+ }
61
+ }
62
+
63
+ // Get the value of a setting from the edit form field.
64
+ function getSettingEditValue(row) {
65
+ if (row.querySelector(".super-settings-value input.js-setting-value[type=checkbox]")) {
66
+ return row.querySelector(".super-settings-value input.js-setting-value[type=checkbox]").checked;
67
+ } else {
68
+ return row.querySelector(".super-settings-value .js-setting-value").value;
69
+ }
70
+ }
71
+
72
+ // Helper function to pad time values with a zero for making ISO-8601 date and time formats.
73
+ function padTimeVal(val) {
74
+ return ("" + val).padStart(2, "0");
75
+ }
76
+
77
+ // Escape special HTML characters in text.
78
+ function escapeHTML(text) {
79
+ if (text === null || text === undefined) {
80
+ return "";
81
+ }
82
+ const htmlEscapes = {'&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#x27;', '/': '&#x2F;'};
83
+ const htmlEscaper = /[&<>"'\/]/g;
84
+ return ('' + text).replace(htmlEscaper, function(match) {
85
+ return htmlEscapes[match];
86
+ });
87
+ }
88
+
89
+ // Helper function for use with templates to replace the text {{id}} with a setting's id value.
90
+ function mustacheSubstitute(html, setting) {
91
+ return html.replaceAll("{{id}}", escapeHTML(setting.id));
92
+ }
93
+
94
+ // Extract a new DOM element from a <template> element on the page.
95
+ function elementFromSettingTemplate(setting, templateSelector) {
96
+ let html = document.querySelector(templateSelector).innerHTML;
97
+ html = mustacheSubstitute(html, setting);
98
+ const template = document.createElement('template');
99
+ template.innerHTML = html.trim();
100
+ return template.content.firstChild;
101
+ }
102
+
103
+ // Create a table row element for displaying a setting.
104
+ function settingRow(setting) {
105
+ const row = elementFromSettingTemplate(setting, "#setting-row-template");
106
+ row.dataset.id = setting.id
107
+ row.dataset.key = setting.key
108
+ row.querySelector(".js-setting-key").value = setting.key;
109
+ if (setting.deleted) {
110
+ row.dataset.edited = true
111
+ row.dataset.deleted = true
112
+ row.querySelector(".js-setting-deleted").value = "1";
113
+ }
114
+ if (setting.key !== null && setting.key !== undefined) {
115
+ row.querySelector(".super-settings-key .js-value-placeholder").innerText = setting.key;
116
+ }
117
+ if (setting.value !== null && setting.value !== undefined) {
118
+ setSettingDisplayValue(row.querySelector(".super-settings-value .js-value-placeholder"), setting);
119
+ }
120
+ if (setting.value_type !== null && setting.value_type !== undefined) {
121
+ row.querySelector(".super-settings-value-type .js-value-placeholder").innerText = setting.value_type;
122
+ }
123
+ if (setting.description !== null && setting.description !== undefined) {
124
+ row.querySelector(".super-settings-description .js-value-placeholder").innerHTML = escapeHTML(setting.description).replaceAll("\n", "<br>");
125
+ }
126
+
127
+ return row
128
+ }
129
+
130
+ // Create an input element from a template depending on the value type.
131
+ function createValueInputElement(setting) {
132
+ let templateName = null;
133
+ if (setting.value_type === "integer") {
134
+ templateName = "#setting-value-field-integer-template";
135
+ } else if (setting.value_type === "float") {
136
+ templateName = "#setting-value-field-float-template";
137
+ } else if (setting.value_type === "datetime") {
138
+ templateName = "#setting-value-field-datetime-template";
139
+ } else if (setting.value_type === "boolean") {
140
+ templateName = "#setting-value-field-boolean-template";
141
+ } else if (setting.value_type === "array") {
142
+ templateName = "#setting-value-field-array-template";
143
+ } else {
144
+ templateName = "#setting-value-field-template";
145
+ }
146
+ const html = mustacheSubstitute(document.querySelector(templateName).innerHTML, setting);
147
+ const template = document.createElement('template');
148
+ template.innerHTML = html.trim();
149
+ return template.content.firstChild;
150
+ }
151
+
152
+ // Create the elements needed to edit a setting value and set the element value.
153
+ function valueInputElement(setting) {
154
+ const element = createValueInputElement(setting);
155
+ if (setting.value_type === "boolean") {
156
+ const checked = (`${setting.value}` === "true" || parseInt(setting.value) > 0);
157
+ const checkbox = element.querySelector('input[type="checkbox"]');
158
+ checkbox.checked = checked;
159
+ } else if (setting.value_type === "array") {
160
+ if (Array.isArray(setting.value)) {
161
+ element.value = setting.value.join("\n");
162
+ } else {
163
+ element.value = setting.value;
164
+ }
165
+ } else if (setting.value_type === "datetime") {
166
+ try {
167
+ const datetime = new Date(Date.parse(setting.value));
168
+ const isoDate = `${datetime.getUTCFullYear()}-${padTimeVal(datetime.getUTCMonth() + 1)}-${padTimeVal(datetime.getUTCDate())}`;
169
+ const isoTime = `${padTimeVal(datetime.getUTCHours())}:${padTimeVal(datetime.getUTCMinutes())}:${padTimeVal(datetime.getUTCSeconds())}`;
170
+ element.querySelector('input[type="date"]').value = isoDate;
171
+ element.querySelector('input[type="time"]').value = isoTime;
172
+ element.querySelector(".js-setting-value").value = datetime.toUTCString().replace("GMT", "UTC");
173
+ } catch(e) {
174
+ // ignore bad date format
175
+ }
176
+ } else if (setting.value_type === "integer") {
177
+ element.value = "" + parseInt("" + setting.value, 10);
178
+ } else if (setting.value_type === "float") {
179
+ element.value = "" + parseFloat("" + setting.value);
180
+ } else {
181
+ element.value = setting.value;
182
+ }
183
+
184
+ return element;
185
+ }
186
+
187
+ // Create a table row with form elements for editing a setting.
188
+ function editSettingRow(setting) {
189
+ const row = elementFromSettingTemplate(setting, "#setting-row-edit-template");
190
+ row.dataset.id = setting.id
191
+
192
+ row.querySelector(".super-settings-key input").value = setting.key;
193
+ if (setting.description) {
194
+ row.querySelector(".super-settings-description textarea").value = setting.description;
195
+ }
196
+
197
+ const valueInput = valueInputElement(setting);
198
+ const valuePlaceholder = row.querySelector(".super-settings-value .js-value-placeholder");
199
+ valuePlaceholder.innerHTML = "";
200
+ valuePlaceholder.appendChild(valueInput);
201
+
202
+ const valueType = row.querySelector(".super-settings-value-type select");
203
+ for (let i = 0; i < valueType.options.length; i++) {
204
+ if (valueType.options[i].value === setting.value_type) {
205
+ valueType.selectedIndex = i;
206
+ break;
207
+ }
208
+ }
209
+
210
+ if (setting.errors && setting.errors.length > 0) {
211
+ let errorsHTML = "";
212
+ setting.errors.forEach(function(error) {
213
+ errorsHTML += `<div>${escapeHTML(error)}</div>`
214
+ });
215
+ row.querySelector(".js-setting-errors").innerHTML = errorsHTML;
216
+ }
217
+
218
+ if (setting.new_record) {
219
+ row.dataset.newrecord = "true";
220
+ }
221
+
222
+ return row
223
+ }
224
+
225
+ // Create a table row with form elements for creating a new setting.
226
+ function newSettingRow() {
227
+ const randomId = "new" + Math.floor((Math.random() * 0xFFFFFFFFFFFFFF)).toString(16);
228
+ const setting = {id: randomId, key: "", value: "", value_type: "string", new_record: true}
229
+ row = editSettingRow(setting);
230
+ return row;
231
+ }
232
+
233
+ // Add a setting table row the table of settings.
234
+ function addRowToTable(row) {
235
+ const existingRow = findSettingRow(row.dataset.id);
236
+ if (existingRow) {
237
+ existingRow.replaceWith(row);
238
+ } else {
239
+ document.querySelector("#settings-table tbody").prepend(row);
240
+ }
241
+ bindSettingControlEvents(row);
242
+ filterSettings(document.querySelector("#filter").value);
243
+ row.scrollIntoView({block: "nearest"});
244
+ enableSaveButton();
245
+ return row;
246
+ }
247
+
248
+ // Update the window location URL to reflect the current filter text.
249
+ function updateFilterURL(filter) {
250
+ const queryParams = new URLSearchParams(window.location.search);
251
+ if (filter === "") {
252
+ queryParams.delete("filter");
253
+ } else {
254
+ queryParams.set("filter", filter);
255
+ }
256
+ if (queryParams.toString() !== "") {
257
+ history.replaceState(null, null, "?" + queryParams.toString());
258
+ } else {
259
+ history.replaceState(null, null, window.location.pathname);
260
+ }
261
+ }
262
+
263
+ // Apply the given filter to only show settings that have a key, value, or description
264
+ // that includes the filter text. Settings that are currently being edited will also be shown.
265
+ function filterSettings(filterText) {
266
+ const filters = [];
267
+ filterText.split(" ").forEach(function(filter) {
268
+ filter = filter.toUpperCase();
269
+ filters.push(function(tr) {
270
+ let text = "";
271
+ const settingKey = tr.querySelector(".super-settings-key");
272
+ if (settingKey) {
273
+ text += " " + settingKey.textContent.toUpperCase();
274
+ }
275
+ const settingValue = tr.querySelector(".super-settings-value");
276
+ if (settingValue) {
277
+ text += " " + settingValue.textContent.toUpperCase();
278
+ }
279
+ const settingDescription = tr.querySelector(".setting-description");
280
+ if (settingDescription) {
281
+ text += " " + settingDescription.textContent.toUpperCase();
282
+ }
283
+ return (text.indexOf(filter) > -1);
284
+ });
285
+ });
286
+
287
+ document.querySelectorAll("#settings-table tbody tr").forEach(function(tr) {
288
+ let matched = true;
289
+ if (!tr.dataset.edited) {
290
+ filters.forEach(function(filter) {
291
+ matched = matched && filter(tr);
292
+ });
293
+ }
294
+ if (matched) {
295
+ tr.style.display = "table-row";
296
+ } else {
297
+ tr.style.display = "none";
298
+ }
299
+ });
300
+ }
301
+
302
+ // Programatically apply the filter again to keep it up to date with other changes.
303
+ function applyFilter(value) {
304
+ const filter = document.querySelector("#filter");
305
+ if (filter) {
306
+ if (value) {
307
+ filter.value = value;
308
+ }
309
+ }
310
+ }
311
+
312
+ // Display validation errors on settings form.
313
+ function showValidationErrors(errors) {
314
+ const table = document.querySelector("#settings-table");
315
+ Object.keys(errors).forEach(function(key) {
316
+ table.querySelectorAll(".super-settings-edit-row").forEach(function(row) {
317
+ const settingKey = row.querySelector(".js-setting-key");
318
+ if (settingKey && settingKey.value === key) {
319
+ const errorsElement = row.querySelector(".js-setting-errors");
320
+ if (errorsElement) {
321
+ errorsElement.innerText = errors[key].join("; ");
322
+ errorsElement.style.display = "block";
323
+ }
324
+ }
325
+ });
326
+ });
327
+ }
328
+
329
+ // Show a temporary message to give the user feedback that an operation has succeeded or not.
330
+ function showFlash(message, success) {
331
+ const flash = document.querySelector(".js-flash");
332
+ if (success) {
333
+ flash.classList.add("text-success");
334
+ flash.classList.remove("text-danger");
335
+ } else {
336
+ flash.classList.add("text-danger");
337
+ flash.classList.remove("text-success");
338
+ }
339
+ flash.innerText = message;
340
+ flash.style.display = "inline-block";
341
+ dismissFlash();
342
+ }
343
+
344
+ // Automatically hide the flash message displaying the results of the last save operation.
345
+ function dismissFlash() {
346
+ if (document.querySelector(".js-flash")) {
347
+ setTimeout(function(){
348
+ document.querySelectorAll(".js-flash").forEach(function(element) {
349
+ element.style.display = "none";
350
+ });
351
+ }, 3000);
352
+ }
353
+ }
354
+
355
+ // Render a setting's history in a table.
356
+ function renderHistoryTable(parent, payload) {
357
+ parent.innerHTML = document.querySelector("#setting-history-table").innerHTML.trim();
358
+ parent.querySelector(".super-settings-history-key").innerText = payload.key;
359
+ const tbody = parent.querySelector("tbody");
360
+ let rowsHTML = "";
361
+ payload.histories.forEach(function(history) {
362
+ const date = (new Date(Date.parse(history.created_at))).toUTCString().replace("GMT", "UTC");
363
+ const value = (payload.encrypted ? "<em>n/a</em>" : escapeHTML(history.value));
364
+ rowsHTML += `<tr><td class="super-settings-text-nowrap">${escapeHTML(date)}</td><td>${escapeHTML(history.changed_by)}</td><td>${value}</td></tr>`;
365
+ });
366
+ tbody.insertAdjacentHTML("beforeend", rowsHTML);
367
+
368
+ if (payload.previous_page_params || payload.next_page_params) {
369
+ let paginationHTML = `<div class="align-center">`;
370
+ if (payload.previous_page_params) {
371
+ paginationHTML += `<div style="float:left;"><a href="#" class="js-show-history" title="Newer" data-offset="${payload.previous_page_params.offset}" data-limit="${payload.previous_page_params.limit}" data-key="${payload.previous_page_params.key}")>&#8592; Newer</a></div>`;
372
+ }
373
+ if (payload.next_page_params) {
374
+ paginationHTML += `<div style="float:right;"><a href="#" class="js-show-history" title="Older" data-offset="${payload.next_page_params.offset}" data-limit="${payload.next_page_params.limit}" data-key="${payload.next_page_params.key}")>Older &#8594;</a></div>`;
375
+ }
376
+ paginationHTML += '<div style="clear:both;"></div>';
377
+ parent.querySelector("table").insertAdjacentHTML("afterend", paginationHTML);
378
+ }
379
+ addListener(parent.querySelectorAll(".js-show-history"), "click", showHistoryModal);
380
+ }
381
+
382
+ // Show a modal window overlayed on the page.
383
+ function showModal() {
384
+ const modal = document.querySelector("#modal");
385
+ const content = document.querySelector(".super-settings-modal-content");
386
+ modal.style.display = "block";
387
+ modal.setAttribute("aria-hidden", "false");
388
+ modal.activator = document.activeElement;
389
+ focusableElements(document).forEach(function(element) {
390
+ if (!modal.contains(element)) {
391
+ element.dataset.saveTabIndex = element.getAttribute("tabindex");
392
+ element.setAttribute("tabindex", -1);
393
+ }
394
+ });
395
+ document.querySelector("body").style.overflow = "hidden";
396
+ }
397
+
398
+ // Hide the modal window overlayed on the page.
399
+ function hideModal() {
400
+ const modal = document.querySelector("#modal");
401
+ const content = document.querySelector(".super-settings-modal-content");
402
+ modal.style.display = "none";
403
+ modal.setAttribute("aria-hidden", "true");
404
+ focusableElements(document).forEach(function(element) {
405
+ const tabIndex = element.dataset.saveTabIndex;
406
+ delete element.dataset.saveTabIndex;
407
+ if (tabIndex) {
408
+ element.setAttribute("tabindex", tabIndex);
409
+ }
410
+ });
411
+ if (modal.activator) {
412
+ modal.activator.focus();
413
+ delete modal.activator;
414
+ }
415
+ content.innerHTML = "";
416
+ document.querySelector("body").style.overflow = "visible";
417
+ }
418
+
419
+ // Returns a list of all focusable elements so that they can be set to not take the focus
420
+ // when a modal is opened.
421
+ function focusableElements(parent) {
422
+ return parent.querySelectorAll("a[href], area[href], button, input:not([type=hidden]), select, textarea, iframe, [tabindex], [contentEditable=true]")
423
+ }
424
+
425
+ // Find a setting by key.
426
+ function findSetting(id) {
427
+ let found = null;
428
+ id = "" + id;
429
+ activeSettings.forEach(function(setting) {
430
+ if ("" + setting.id === id) {
431
+ found = setting;
432
+ return;
433
+ }
434
+ });
435
+ return found;
436
+ }
437
+
438
+ /*** Event Listeners ***/
439
+
440
+ // Listener for showing the setting history modal.
441
+ function showHistoryModal(event) {
442
+ event.preventDefault();
443
+ if (!event.target.dataset) {
444
+ return;
445
+ }
446
+
447
+ const modal = document.querySelector("#modal");
448
+ const content = document.querySelector(".super-settings-modal-content");
449
+ let key = event.target.dataset.key;
450
+ if (!key) {
451
+ const row = event.target.closest("tr");
452
+ if (row) {
453
+ const id = row.dataset.id;
454
+ const setting = findSetting(id);
455
+ if (setting) {
456
+ key = setting.key;
457
+ if (!key) {
458
+ return;
459
+ }
460
+ }
461
+ }
462
+ }
463
+ const params = {key: key, limit: 25};
464
+ if (event.target.dataset.limit) {
465
+ params["limit"] = event.target.dataset.limit;
466
+ }
467
+ if (event.target.dataset.offset) {
468
+ params["offset"] = event.target.dataset.offset;
469
+ }
470
+ SuperSettingsAPI.fetchHistory(params, function(settingHistory){
471
+ renderHistoryTable(content, settingHistory);
472
+ showModal();
473
+ });
474
+ }
475
+
476
+ // Listener for closing the modal window overlay.
477
+ function closeModal(event) {
478
+ if (event.target.classList.contains("js-close-modal")) {
479
+ event.preventDefault();
480
+ hideModal();
481
+ }
482
+ }
483
+
484
+ // Listener to just capture events.
485
+ function noOp(event) {
486
+ event.preventDefault();
487
+ }
488
+
489
+ // Listener for changing the setting value type select element. Different types will have
490
+ // different input elements for the setting value.
491
+ function changeSettingType(event) {
492
+ event.preventDefault();
493
+ const row = event.target.closest("tr");
494
+ const valueType = event.target.options[event.target.selectedIndex].value;
495
+ var setting = {
496
+ id: row.dataset.id,
497
+ key: row.querySelector(".super-settings-key input").value,
498
+ value: getSettingEditValue(row),
499
+ value_type: valueType,
500
+ description: row.querySelector(".super-settings-description textarea").value,
501
+ new_record: row.dataset.newrecord
502
+ }
503
+ const addedRow = addRowToTable(editSettingRow(setting));
504
+ if (addedRow.querySelector(".super-settings-value .js-date-input")) {
505
+ addedRow.querySelector(".super-settings-value .js-date-input").focus();
506
+ } else {
507
+ addedRow.querySelector(".super-settings-value .js-setting-value").focus();
508
+ }
509
+ }
510
+
511
+ // Listener for date and time input elements the combine the values into a hidden datetime field.
512
+ function changeDateTime(event) {
513
+ const parentNode = event.target.closest("span")
514
+ const dateValue = parentNode.querySelector(".js-date-input").value;
515
+ let timeValue = parentNode.querySelector(".js-time-input").value;
516
+ if (timeValue === "") {
517
+ timeValue = "00:00:00";
518
+ }
519
+ parentNode.querySelector(".js-setting-value").value = `${dateValue}T${timeValue}Z`
520
+ }
521
+
522
+ // Listener for the add setting button.
523
+ function addSetting(event) {
524
+ event.preventDefault();
525
+ const row = addRowToTable(newSettingRow());
526
+ row.querySelector(".super-settings-key input").focus();
527
+ }
528
+
529
+ // Listener for the edit setting button.
530
+ function editSetting(event) {
531
+ event.preventDefault();
532
+ const id = event.target.closest("tr").dataset.id;
533
+ const setting = findSetting(id);
534
+ const row = addRowToTable(editSettingRow(setting));
535
+ if (row.querySelector(".super-settings-value .js-date-input")) {
536
+ row.querySelector(".super-settings-value .js-date-input").focus();
537
+ } else {
538
+ row.querySelector(".super-settings-value .js-setting-value").focus();
539
+ }
540
+ }
541
+
542
+ // Listener for the restore setting button.
543
+ function restoreSetting(event) {
544
+ event.preventDefault();
545
+ const row = event.target.closest("tr");
546
+ const id = row.dataset.id;
547
+ const setting = findSetting(id);
548
+ if (setting) {
549
+ const newRow = settingRow(setting);
550
+ bindSettingControlEvents(newRow);
551
+ row.replaceWith(newRow);
552
+ } else {
553
+ row.remove();
554
+ }
555
+ enableSaveButton();
556
+ }
557
+
558
+ // Listener for the remove setting button.
559
+ function removeSetting(event) {
560
+ event.preventDefault();
561
+ const settingRow = event.target.closest("tr");
562
+ if (settingRow.dataset["id"]) {
563
+ settingRow.querySelector("input.js-setting-deleted").value = "1";
564
+ settingRow.dataset.edited = true;
565
+ settingRow.dataset.deleted = true;
566
+ settingRow.querySelector(".js-remove-setting").style.display = "none";
567
+ settingRow.querySelector(".js-restore-setting").style.display = "inline-block";
568
+ } else {
569
+ settingRow.remove();
570
+ }
571
+ enableSaveButton();
572
+ }
573
+
574
+ // Update the settings via the API.
575
+ function updateSettings(event) {
576
+ event.preventDefault();
577
+ event.target.disabled = true;
578
+ const settingsData = [];
579
+ document.querySelectorAll("#settings-table tbody tr[data-edited=true]").forEach(function(row) {
580
+ const data = {};
581
+ settingsData.push(data);
582
+ data.key = row.querySelector(".js-setting-key").value;
583
+ const deleted = row.querySelector(".js-setting-deleted");
584
+ if (deleted && deleted.value === "1") {
585
+ data.deleted = true;
586
+ } else {
587
+ if (row.querySelector(".js-setting-value")) {
588
+ data.value = getSettingEditValue(row);
589
+ }
590
+ if (row.querySelector(".js-setting-value-type")) {
591
+ const valueTypeSelect = row.querySelector(".js-setting-value-type");
592
+ data.value_type = valueTypeSelect.options[valueTypeSelect.selectedIndex].value;
593
+ }
594
+ if (row.querySelector(".super-settings-description textarea")) {
595
+ data.description = row.querySelector(".super-settings-description textarea").value;
596
+ }
597
+ }
598
+ });
599
+
600
+ SuperSettingsAPI.updateSettings({settings: settingsData}, function(results) {
601
+ if (results.success) {
602
+ fetchActiveSettings();
603
+ showFlash("Settings saved", true)
604
+ } else {
605
+ event.target.disabled = false;
606
+ showFlash("Failed to save settings", false)
607
+ if (results.errors) {
608
+ showValidationErrors(results.errors)
609
+ }
610
+ }
611
+ });
612
+ }
613
+
614
+ // Listener for the filter input field.
615
+ function filterListener(event) {
616
+ const filter = event.target.value;
617
+ filterSettings(filter);
618
+ updateFilterURL(filter);
619
+ }
620
+
621
+ // Listener for refresh page button.
622
+ function refreshPage(event) {
623
+ event.preventDefault();
624
+ let url = window.location.href.replace(/\?.*/, "");
625
+ const filter = document.querySelector("#filter").value;
626
+ if (filter !== "") {
627
+ url += "?filter=" + escape(filter);
628
+ }
629
+ window.location = url;
630
+ }
631
+
632
+ // Attach event listener to one or more elements.
633
+ function addListener(elements, event, handler) {
634
+ if (elements.addEventListener) {
635
+ elements = [elements];
636
+ }
637
+ elements.forEach(function(element) {
638
+ if (element) {
639
+ element.addEventListener(event, handler);
640
+ }
641
+ });
642
+ }
643
+
644
+ // Bind event listeners for setting controls on a setting table row.
645
+ function bindSettingControlEvents(parent) {
646
+ addListener(parent.querySelectorAll(".js-remove-setting"), "click", removeSetting);
647
+ addListener(parent.querySelectorAll(".js-edit-setting"), "click", editSetting);
648
+ addListener(parent.querySelectorAll(".js-restore-setting"), "click", restoreSetting);
649
+ addListener(parent.querySelectorAll(".js-show-history"), "click", showHistoryModal);
650
+ addListener(parent.querySelectorAll(".js-no-op"), "click", noOp);
651
+ addListener(parent.querySelectorAll(".js-setting-value-type"), "change", changeSettingType);
652
+ addListener(parent.querySelectorAll(".js-date-input"), "change", changeDateTime);
653
+ addListener(parent.querySelectorAll(".js-time-input"), "change", changeDateTime);
654
+ }
655
+
656
+ // Initialize the table with all the settings plus any changes from a failed form submission.
657
+ function renderSettingsTable(settings) {
658
+ const tbody = document.querySelector("#settings-table tbody");
659
+ tbody.innerHTML = "";
660
+ let count = settings.length;
661
+ settings.forEach(function(setting) {
662
+ const randomId = "setting" + Math.floor((Math.random() * 0xFFFFFFFFFFFFFF)).toString(16);
663
+ setting.id = (setting.id || randomId);
664
+ const row = settingRow(setting);
665
+ tbody.appendChild(row);
666
+ bindSettingControlEvents(row);
667
+ });
668
+ document.querySelector(".js-settings-count").textContent = `${count} ${count === 1 ? "Setting" : "Settings"}`;
669
+
670
+ const filter = document.querySelector("#filter");
671
+ if (filter) {
672
+ filter.dispatchEvent(new Event("input"));
673
+ }
674
+ }
675
+
676
+ function promptUnsavedChanges(event) {
677
+ if (changesCount() > 0) {
678
+ return "Are you sure you want to leave?";
679
+ } else {
680
+ return undefined;
681
+ }
682
+ }
683
+
684
+ // Run the supplied function when the document has been marked ready.
685
+ function docReady(fn) {
686
+ if (document.readyState === "complete" || document.readyState === "interactive") {
687
+ setTimeout(fn, 1);
688
+ } else {
689
+ document.addEventListener("DOMContentLoaded", fn);
690
+ }
691
+ }
692
+
693
+ function fetchActiveSettings() {
694
+ SuperSettingsAPI.fetchSettings(function(settings_hash) {
695
+ const settings = settings_hash["settings"];
696
+ activeSettings = settings;
697
+ renderSettingsTable(settings);
698
+ enableSaveButton();
699
+ });
700
+ }
701
+
702
+ let activeSettings = [];
703
+
704
+ docReady(function() {
705
+ addListener(document.querySelector("#filter"), "input", filterListener);
706
+ addListener(document.querySelector("#add-setting"), "click", addSetting);
707
+ addListener(document.querySelector("#discard-changes"), "click", refreshPage);
708
+ addListener(document.querySelector("#save-settings"), "click", updateSettings);
709
+ addListener(document.querySelector("#modal"), "click", closeModal);
710
+
711
+ const queryParams = new URLSearchParams(window.location.search);
712
+ applyFilter(queryParams.get("filter"));
713
+
714
+ fetchActiveSettings();
715
+
716
+ window.onbeforeunload = promptUnsavedChanges;
717
+ })
718
+ })();