super_settings 0.0.0.rc1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +9 -0
- data/MIT-LICENSE +20 -0
- data/README.md +313 -0
- data/VERSION +1 -0
- data/app/helpers/super_settings/settings_helper.rb +32 -0
- data/app/views/layouts/super_settings/settings.html.erb +20 -0
- data/config/routes.rb +13 -0
- data/db/migrate/20210414004553_create_super_settings.rb +34 -0
- data/lib/super_settings/application/api.js +88 -0
- data/lib/super_settings/application/helper.rb +119 -0
- data/lib/super_settings/application/images/edit.svg +1 -0
- data/lib/super_settings/application/images/info.svg +1 -0
- data/lib/super_settings/application/images/plus.svg +1 -0
- data/lib/super_settings/application/images/slash.svg +1 -0
- data/lib/super_settings/application/images/trash.svg +1 -0
- data/lib/super_settings/application/index.html.erb +169 -0
- data/lib/super_settings/application/layout.html.erb +22 -0
- data/lib/super_settings/application/layout_styles.css +193 -0
- data/lib/super_settings/application/scripts.js +718 -0
- data/lib/super_settings/application/styles.css +122 -0
- data/lib/super_settings/application.rb +38 -0
- data/lib/super_settings/attributes.rb +24 -0
- data/lib/super_settings/coerce.rb +66 -0
- data/lib/super_settings/configuration.rb +144 -0
- data/lib/super_settings/controller_actions.rb +81 -0
- data/lib/super_settings/encryption.rb +76 -0
- data/lib/super_settings/engine.rb +70 -0
- data/lib/super_settings/history_item.rb +26 -0
- data/lib/super_settings/local_cache.rb +306 -0
- data/lib/super_settings/rack_middleware.rb +210 -0
- data/lib/super_settings/rest_api.rb +195 -0
- data/lib/super_settings/setting.rb +599 -0
- data/lib/super_settings/storage/active_record_storage.rb +123 -0
- data/lib/super_settings/storage/http_storage.rb +279 -0
- data/lib/super_settings/storage/redis_storage.rb +293 -0
- data/lib/super_settings/storage/test_storage.rb +158 -0
- data/lib/super_settings/storage.rb +254 -0
- data/lib/super_settings/version.rb +5 -0
- data/lib/super_settings.rb +213 -0
- data/lib/tasks/super_settings.rake +9 -0
- data/super_settings.gemspec +35 -0
- 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 = {'&': '&', '<': '<', '>': '>', '"': '"', "'": ''', '/': '/'};
|
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}")>← 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 →</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
|
+
})();
|