-
Notifications
You must be signed in to change notification settings - Fork 118
Expand file tree
/
Copy pathrdaMetadata.js
More file actions
400 lines (376 loc) · 15.7 KB
/
rdaMetadata.js
File metadata and controls
400 lines (376 loc) · 15.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
import { isUndefined, isObject } from '../utils/isType';
$(() => {
// url for the api we will be querying
let url = '';
// key/value lookup for standards
const descriptions = {};
// cleaned up structure of the API results
const minTree = {};
// keeps track of how many waiting api-requests still need to run
let noWaiting = 0;
// prune the min_tree where there are no standards
// opporates on the principal that no two subjects have the same name
function removeUnused(name) {
const num = Object.keys(minTree).find((x) => minTree[x].name === name);
// if not top level standard
if (isUndefined(num)) {
// for each top level standard
Object.keys(minTree).forEach((knum) => {
const child = Object.keys(minTree[knum].children)
.find((x) => minTree[knum].children[x].name === name);
if (isObject(child)) {
delete minTree[knum].children[child];
$(`.rda_metadata .sub-subject select option[value="${name}"]`).remove();
}
});
} else {
delete minTree[num];
// remove min_tree[num] from top-level dropdowns
$(`.rda_metadata .subject select option[value="${name}"]`).remove();
}
}
function getDescription(id) {
$.ajax({
url: url + id.slice(4),
type: 'GET',
crossDomain: true,
dataType: 'json',
}).done((results) => {
descriptions[id] = {};
descriptions[id].title = results.title;
descriptions[id].description = results.description;
noWaiting -= 1;
});
}
// init descriptions lookup table based on passed ids
function initDescriptions(ids) {
ids.forEach((id) => {
if (!(id in descriptions)) {
noWaiting += 1;
getDescription(id);
}
});
}
// takes in a subset of the min_tree which has name and standards properties
// initializes the standards property to the result of an AJAX POST
function getStandards(name, num, child) {
// slice -4 from url to remove '/api/'
noWaiting += 1;
$.ajax({
url: `${url.slice(0, -4)}query/schemes`,
type: 'POST',
crossDomain: true,
data: `keyword=${name}`,
dataType: 'json',
}).done((result) => {
if (isUndefined(child)) {
minTree[num].standards = result.ids;
} else {
minTree[num].children[child].standards = result.ids;
}
if (result.ids.length < 1) {
removeUnused(name);
}
noWaiting -= 1;
initDescriptions(result.ids);
});
}
// clean up the data initially returned from the API
function cleanTree(apiTree) {
// iterate over api_tree
Object.keys(apiTree).forEach((num) => {
minTree[num] = {};
minTree[num].name = apiTree[num].name;
minTree[num].children = [];
if (apiTree[num].children !== undefined) {
Object.keys(apiTree[num].children).forEach((child) => {
minTree[num].children[child] = {};
minTree[num].children[child].name = apiTree[num].children[child].name;
minTree[num].children[child].standards = [];
getStandards(minTree[num].children[child].name, num, child);
});
}
// init a standards on top level
minTree[num].standards = [];
getStandards(minTree[num].name, num, undefined);
});
}
// create object for typeahead
function initTypeahead() {
const data = [];
const simpdat = [];
Object.keys(descriptions).forEach((id) => {
data.push({ value: descriptions[id].title, id });
simpdat.push(descriptions[id].title);
});
const typ = $('.standards-typeahead');
typ.typeahead({ source: simpdat });
}
function initStandards() {
// for each metadata question, init selected standards according to html
$('.rda_metadata').each(function () { // eslint-disable-line func-names
// list of selected standards
const selectedStandards = $(this).find('.selected_standards .list');
// form listing of standards
const formStandards = $(this).next('form').find('#standards');
// need to pull in the value from frm_stds
const standardsArray = JSON.parse(formStandards.val());
// init the data value
formStandards.data('standard', standardsArray);
Object.keys(standardsArray).forEach((key) => {
// add the standard to list
if (key === standardsArray[key]) {
selectedStandards.append(`<li class="${key}">${key}<button class="remove-standard"><i class="fas fa-circle-xmark"></i></button></li`);
} else {
selectedStandards.append(`<li class="${key}">${descriptions[key].title}<button class="remove-standard"><i class="fas fa-circle-xmark"></i></button></li>`);
}
});
});
}
function waitAndUpdate() {
if (noWaiting > 0) {
// if we are waiting on api responses, call this function in 1 second
setTimeout(waitAndUpdate, 1000);
} else {
// update all the dropdowns/ standards explore box (calling on subject
// will suffice since it will necisarily update sub-subject)
$('.rda_metadata .subject select').change();
initStandards();
initTypeahead();
}
}
// given a subject name, returns the portion of the min_tree applicable
function getSubject(subjectText) {
const num = Object.keys(minTree).find((x) => minTree[x].name === subjectText);
return minTree[num];
}
// given a subsubject name and an array of children, data, return the
// applicable child
function getSubSubject(subsubjectText, data) {
const child = Object.keys(data).find((x) => data[x].name === subsubjectText);
return data[child];
}
function updateSaveStatus(group) {
// update save/autosave status
group.next('form').find('fieldset input').change();
}
// change sub-subjects and standards based on selected subject
$('.rda_metadata').on('change', '.subject select', (e) => {
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const subSubject = group.find('.sub-subject select');
const subjectText = target.find(':selected').text();
// find subject in min_tree
const subject = getSubject(subjectText);
// check to see if this object has no children(and thus it's own standards)
if (subject.children.length === 0) {
// hide sub-subject since there's no data for it
subSubject.closest('div').hide();
// update the standards display selector
$('.rda_metadata .sub-subject select').change();
} else {
// show the sub-subject in case it was previously hidden
subSubject.closest('div').show();
// update the sub-subject display selector
subSubject.find('option').remove();
subject.children.forEach((child) => {
$('<option />', { value: child.name, text: child.name }).appendTo(subSubject);
});
// once we have updated the sub-standards, ensure the standards displayed
// get updated as well
$('.rda_metadata .sub-subject select').change();
}
});
// change standards based on selected sub-subject
$('.rda_metadata').on('change', '.sub-subject select', (e) => {
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const subject = group.find('.subject select');
const subSubject = group.find('.sub-subject select');
const subjectText = subject.find(':selected').text();
const subjectData = getSubject(subjectText);
const standards = group.find('.browse-standards-border');
let standardsData;
if (subjectData.children.length === 0) {
// update based on subject's standards
standardsData = subjectData.standards;
} else {
// update based on sub-subject's standards
const subsubjectText = subSubject.find(':selected').text();
standardsData = getSubSubject(subsubjectText, subjectData.children).standards;
}
// clear list of standards
standards.empty();
// update list of standards
Object.keys(standardsData).forEach((num) => {
const standard = descriptions[standardsData[num]];
standards.append(`<div style="background-color:#EAEAEA;border-radius:3px"><strong>${standard.title}</strong><div style="float:right"><button class="btn btn-primary select_standard" data-standard="${standardsData[num]}">Add Standard</button></br></div><p>${standard.description}</p></div>`);
});
});
// when 'Add Standard' button next to the search is clicked, we need to add
// this to the user's selected list of standards.
// update the data and val of hidden standards field in form
$('.rda_metadata').on('click', '.select_standard_typeahead', (e) => {
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const selected = group.find('ul.typeahead li.active');
const selectedStandards = group.find('.selected_standards .list');
// the title of the standard
const standardTitle = selected.data('value');
// need to find the standard
let standard;
Object.keys(descriptions).forEach((standardId) => {
if (descriptions[standardId].title === standardTitle) {
standard = standardId;
}
});
selectedStandards.append(`<li class="${standard}">${descriptions[standard].title}<button class="remove-standard"><i class="fas fa-circle-xmark"></i></button></li>`);
const formStandards = group.next('form').find('#standards');
// get the data for selected standards from the data attribute 'standard'
// of the hidden field #standards within the answer form
let frmStdsDat = formStandards.data('standard');
// need to init data object for first time
if (typeof frmStdsDat === 'undefined') {
frmStdsDat = {};
}
// init the key to standard id and value to standard.
// NOTE: is there any point in storing the title or description here?
// storing the title could make export easier as we wolnt need to query api
// but queries to the api would be 1 per-standard if we dont store these
frmStdsDat[standard] = descriptions[standard].title;
// update data value
formStandards.data('standard', frmStdsDat);
// update input value
formStandards.val(JSON.stringify(frmStdsDat));
updateSaveStatus(group);
});
// when a 'Add standard' button is clicked, we need to add this to the user's
// selected list of standards
// update the data and val of hidden standards field in form
$('.rda_metadata').on('click', '.select_standard', (e) => {
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const selectedStandards = group.find('.selected_standards .list');
// the identifier for the standard which was selected
const standard = target.data('standard');
// append the standard to the displayed list of selected standards
selectedStandards.append(`<li class="${standard}">${descriptions[standard].title}<button class="remove-standard"><i class="fas fa-circle-xmark"></i></button></li>`);
const formStandards = group.next('form').find('#standards');
// get the data for selected standards from the data attribute 'standard'
// of the hidden field #standards within the answer form
let frmStdsDat = formStandards.data('standard');
// need to init data object for first time
if (isUndefined(frmStdsDat)) {
frmStdsDat = {};
}
// init the key to standard id and value to standard.
frmStdsDat[standard] = descriptions[standard].title;
// update data value
formStandards.data('standard', frmStdsDat);
// update input value
formStandards.val(JSON.stringify(frmStdsDat));
updateSaveStatus(group);
});
// when a 'Remove Standard' button is clicked, we need to remove this from the
// user's selected list of standards. Additionally, we need to remove the
// standard from the data/val fields of standards in hidden form
$('.rda_metadata').on('click', '.remove-standard', (e) => {
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const listedStandard = target.closest('li');
const standardId = listedStandard.attr('class');
// remove the standard from the list
listedStandard.remove();
// update the data for the form
const formStandards = group.next('form').find('#standards');
const frmStdsDat = formStandards.data('standard');
delete frmStdsDat[standardId];
// update data value
formStandards.data('standard', frmStdsDat);
// update input value
formStandards.val(JSON.stringify(frmStdsDat));
updateSaveStatus(group);
});
// show the add custom standard div when standard not listed clicked
$('.rda_metadata').on('click', '.custom-standard', (e) => {
e.preventDefault();
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const addStandardDiv = $(group.find('.add-custom-standard'));
addStandardDiv.show();
});
// when this button is clicked, we add the typed standard to the list of
// selected standards
$('.rda_metadata').on('click', '.submit_custom_standard', (e) => {
e.preventDefault();
const target = $(e.currentTarget);
const group = target.closest('.rda_metadata');
const selectedStandards = group.find('.selected_standards .list');
const standardName = group.find('.custom-standard-name').val();
selectedStandards.append(`<li class="${standardName}">${standardName}<button class="remove-standard"><i class="fas fa-circle-xmark"></i></button></li>`);
const formStandards = group.next('form').find('#standards');
// get the data for selected standards from the data attribute 'standard'
// of the hidden field #standards within the answer form
let frmStdsDat = formStandards.data('standard');
// need to init data object for first time
if (typeof frmStdsDat === 'undefined') {
frmStdsDat = {};
}
// init the key to standard id and value to standard.
frmStdsDat[standardName] = standardName;
// update data value
formStandards.data('standard', frmStdsDat);
// update input value
formStandards.val(JSON.stringify(frmStdsDat));
updateSaveStatus(group);
});
function initMetadataQuestions() {
// find all elements with rda_metadata div
$('.rda_metadata').each((idx, el) => {
// $(this) is the element
const sub = $(el).find('.subject select');
// var sub_subject = $(this).find(".sub-subject select");
Object.keys(minTree).forEach((num) => {
$('<option />', { value: minTree[num].name, text: minTree[num].name }).appendTo(sub);
});
});
waitAndUpdate();// $(".rda_metadata .subject select").change();
}
// callback from url+subject-index
// define api_tree and call to initMetadataQuestions
function subjectCallback(data) {
// remove unused standards/substandards
cleanTree(data);
// initialize the dropdowns/selected standards for the page
initMetadataQuestions();
}
// callback from get request to rda_api_address
// define url and make a call to url+subject-index
function urlCallback(data) {
// init url
({ url } = data);
// get api_tree structure from api
$.ajax({
url: `${url}subject-index`,
type: 'GET',
crossDomain: true,
dataType: 'json',
}).done((result) => {
subjectCallback(result);
});
}
// get the url we will be using for the api
// only do this if page has an rda_metadata div
if ($('.rda_metadata').length) {
$.getJSON('/question_formats/rda_api_address', urlCallback);
}
// when the autosave or save action occurs, this clears out both the list of
// selected standards, and the selectors for new standards, as it re-renders
// the partial. This "autosave" event is triggered by that JS in order to
// allow us to know when the save has happened and re-init the question
$('.rda_metadata').on('autosave', (e) => {
e.preventDefault();
// re-initialize the metadata question
initMetadataQuestions();
});
});