summaryrefslogtreecommitdiff
path: root/static/fullnarp.js
diff options
context:
space:
mode:
authorerdgeist <erdgeist@erdgeist.org>2024-12-22 21:53:57 +0100
committererdgeist <erdgeist@erdgeist.org>2024-12-22 21:53:57 +0100
commite3481a4a35091b32b6fbee80c1c9ba2b6d7b50d6 (patch)
tree58f90b32cbd89599acfaab07377cc0447f1190c1 /static/fullnarp.js
Rework of halfnarp and fullnarp into a self contained repository. Still WIP
Diffstat (limited to 'static/fullnarp.js')
-rw-r--r--static/fullnarp.js641
1 files changed, 641 insertions, 0 deletions
diff --git a/static/fullnarp.js b/static/fullnarp.js
new file mode 100644
index 0000000..8b7d36d
--- /dev/null
+++ b/static/fullnarp.js
@@ -0,0 +1,641 @@
1let ws; // WebSocket instance
2
3function toggle_grid(whichDay) {
4 var vclasses= [['in-list'], ['in-calendar', 'onlyday1'], ['in-calendar', 'onlyday2'], ['in-calendar', 'onlyday3'],
5 ['in-calendar', 'onlyday4'], ['in-calendar', 'alldays']];
6 document.body.classList.remove( 'alldays', 'onlyday1', 'onlyday2', 'onlyday3', 'onlyday4', 'in-list', 'in-calendar');
7 if( whichDay < 0 || whichDay > 5 ) return;
8 document.body.classList.add(...vclasses[whichDay]);
9}
10
11function distribute_votes() {
12 document.querySelectorAll('.event').forEach( function(element) {
13 var eid = element.getAttribute('event_id');
14 var abs = window.votes[eid];
15 var klasse = 5000;
16 if (abs < 2000) { klasse = 2000; }
17 if (abs < 1000) { klasse = 1000; }
18 if (abs < 500) { klasse = 500; }
19 if (abs < 200) { klasse = 200; }
20 if (abs < 100) { klasse = 100; }
21 if (abs < 50) { klasse = 50; }
22 if (abs < 20) { klasse = 20; }
23 if (abs < 10) { klasse = 10; }
24 if (!abs) klasse = 10;
25
26 var abselem = element.querySelector('.absval');
27 if (abselem)
28 abselem.textContent = '' + abs;
29 else {
30 var abselem = document.createElement('div');
31 abselem.textContent = '' + abs;
32 abselem.classList.add('absval');
33 element.insertBefore(abselem, element.firstChild);
34 }
35 element.classList.add('class_' + klasse);
36 });
37}
38
39function corr_for_eventids(id1, id2) {
40 var d = 0, c = 0, cd = 0, l = window.raw_votes.length;
41 for (item of window.raw_votes) {
42 var x = 0;
43 if( item.indexOf(id1) > -1 ) { ++d; x++;}
44 if( item.indexOf(id2) > -1 ) { ++c; cd+=x; }
45 }
46
47 var mid = 0;
48 // if ( d * c ) mid = Math.round( 4.0 * ( ( cd * l ) / ( c * d ) ) * ( cd / d + cd / c ) );
49 if ( d * c ) mid = Math.round( 4 * l * cd * cd * ( c + d ) / ( c * c * d * d ) )
50 if (mid>9) mid=9;
51 return mid;
52}
53
54function show_all_correlates(el) {
55 /* First identify the room to see what other rooms to consider
56 correlates always grow from the top slot to the right,
57 unless there's an overlapping event to the left that starts earlier
58 */
59 var event_room = el.getAttribute('fullnarp-room');
60 var event_day = el.getAttribute('fullnarp-day');
61 var event_time = el.getAttribute('fullnarp-time');
62
63 if (!event_time) return;
64
65 var event_start;
66 try { event_start = time_to_mins(event_time); } catch(e) { return; }
67 var event_duration = el.getAttribute('fullnarp-duration') / 60;
68
69 /* Only test events to the right, if they start at the exact same time */
70 document.querySelectorAll('.event.day_'+event_day).forEach( function(check_el, index) {
71 var check_room = check_el.getAttribute('fullnarp-room');
72 if (event_room == check_room) return;
73
74 var check_time = check_el.getAttribute('fullnarp-time');
75 if (!check_time) return;
76 var check_start = time_to_mins(check_time);
77 var check_duration = check_el.getAttribute('fullnarp-duration') / 60;
78 var dist = check_el.getAttribute('fullnarp-room') - event_room;
79 var overlap = check_start < event_start + event_duration && event_start < check_start + check_duration;
80
81 if (!overlap) return;
82 if (event_start == check_start && dist <= 0) return;
83 if (event_start < check_start) return;
84
85 var corr = corr_for_eventids(el.getAttribute('event_id'), check_el.getAttribute('event_id'));
86 var dir = dist > 0 ? 'r' : 'l';
87 var div = document.createElement('div');
88 div.classList.add('corrweb', dir.repeat(Math.abs(dist)), 'day_' + event_day, 'room' + event_room, 'time_' + event_time, 'corr_d_' + corr);
89 document.body.appendChild(div);
90 })
91}
92
93function display_correlation() {
94 var selected = document.querySelectorAll('.selected');
95 if( selected.length == 1 ) {
96 selected = selected[0];
97 document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, selected));
98 }
99 if (document.body.classList.contains('correlate'))
100 distribute_votes();
101 document.body.classList.toggle('correlate');
102}
103
104function mark_correlation(dest, comp) {
105 var id1 = dest.getAttribute('event_id');
106 var id2 = comp.getAttribute('event_id');
107 var d = 0, c = 0, cd = 0, l = window.raw_votes.length;
108 for (vote of window.raw_votes) {
109 var x = 0;
110 if( vote.indexOf(id1) > -1 ) { ++d; x++;}
111 if( vote.indexOf(id2) > -1 ) { ++c; cd+=x; }
112 }
113
114 var mid = 0;
115 // if ( d * c ) mid = Math.round( 4.0 * ( ( cd * l ) / ( c * d ) ) * ( cd / d + cd / c ) );
116 if ( d * c ) mid = Math.round( 4 * l * cd * cd * ( c + d ) / ( c * c * d * d ) )
117 if (mid>9) mid=9;
118
119 dest.className = dest.className.replace(/\bcorr_\S+/g, '');
120 dest.setAttribute('corr', mid);
121 dest.querySelector('.absval').textContent = mid + ':' + Math.round( 100 * cd / d ) + '%:' + Math.round( 100 * cd / c ) + '%';
122
123}
124
125function mark_avail(el) {
126 el.classList.toggle('unavailable', !check_avail(el, el.getAttribute('fullnarp-day'), el.getAttribute('fullnarp-time')));
127}
128
129function time_to_mins(time) {
130 var hour_mins = /(\d\d)(\d\d)/.exec(time);
131 if( hour_mins[1] < 9 ) { hour_mins[1] = 24 + hour_mins[1]; }
132 return 60 * hour_mins[1] + 1 * hour_mins[2];
133}
134
135function check_avail(el, day, time ) {
136 var all_available = true;
137 var speakers = window.event_speakers[el.getAttribute('event_id')];
138
139 if (!speakers)
140 return false;
141
142 try {
143 var event_times = /(\d\d)(\d\d)/.exec(time);
144 var event_duration = el.getAttribute('fullnarp-duration') / 60;
145 var event_start = Number(event_times[1]);
146 if (event_start < 9) { event_start = 24 + event_start; }
147
148 var event_start_date = new Date("2024-12-27T00:00:00+01:00");
149 event_start_date.setTime(event_start_date.getTime() + 60000 * ( 24 * 60 * (day - 1) + event_start * 60 + 1 * Number(event_times[2])) );
150 var event_end_date = new Date();
151 event_end_date.setTime(event_start_date.getTime() + 60000 * event_duration);
152 } catch (error) {
153 return false;
154 }
155
156 /* Check availability of all speakers */
157 for (speaker of speakers) {
158 /* Now if at least one day is set, each missing
159 day means unavailable, */
160 var have_avails = false, unavail = true;
161 for (avail of speaker.availabilities) {
162 have_avails = true;
163
164 var availtime_start = new Date(avail.start);
165 var availtime_end = new Date(avail.end);
166
167 if( event_start_date >= availtime_start && event_end_date <= availtime_end )
168 unavail = false;
169 }
170
171 /* If at least one speaker is unavail, check fails */
172 if( have_avails && unavail ) {
173 all_available = false;
174 return false;
175 }
176 }
177
178 return all_available;
179}
180
181/* Needs to be done for each moved and all previously conflicting events */
182function mark_conflict(el) {
183 var event_start = time_to_mins(el.getAttribute('fullnarp-time'));
184 var event_duration = el.getAttribute('fullnarp-duration') / 60;
185
186 var conflict = false;
187
188 /* We do only need to check events in the same room at the same day for conflicts */
189 document.querySelectorAll('.event.day_'+el.getAttribute('fullnarp-day')+'.room'+el.getAttribute('fullnarp-room')).forEach( function(check_el) {
190
191 if( el.getAttribute('event_id') == check_el.getAttribute('event_id') ) { return true; }
192
193 var check_start = time_to_mins(check_el.getAttribute('fullnarp-time'));
194 var check_duration = check_el.getAttribute('fullnarp-duration') / 60;
195
196 if( check_start < event_start + event_duration &&
197 event_start < check_start + check_duration ) {
198 check_el.classList.add('conflict');
199 conflict = true;
200 }
201 });
202 el.classList.toggle('conflict', conflict);
203}
204
205/* remove day, room and time from an event */
206function remove_event(event_id) {
207 var el = document.getElementById(event_id);
208 el.classList.add('pending');
209 if (ws && ws.readyState === WebSocket.OPEN) {
210 var message = {
211 lastupdate: window.lastupdate,
212 removeevent: event_id
213 }
214 ws.send(JSON.stringify(message));
215 console.log('Sent:', message);
216 } else {
217 el.removeClass('pending');
218 el.addClass('failed');
219 }
220}
221
222/* provide time OR hour + minute, time overrides */
223function set_all_attributes(event_id, day, room, time, from_server) {
224 var el = document.getElementById(event_id);
225 el.className = el.className.replace( /\btime_\S+ ?/g, '').replace( /\broom\S+ ?/g, '').replace( /\bday_\S+ ?/g, '');
226 el.classList.add( time, day, room );
227 el.setAttribute('fullnarp-day', day.replace('day_',''));
228 el.setAttribute('fullnarp-time', time.replace('time_',''));
229 el.setAttribute('fullnarp-room', room.replace('room',''));
230 el.classList.remove('pending');
231
232 if (!from_server) {
233 el.classList.add('pending');
234 if (ws && ws.readyState === WebSocket.OPEN) {
235 var message = {
236 lastupdate: window.lastupdate,
237 setevent: event_id,
238 day: el.getAttribute('fullnarp-day'),
239 room: el.getAttribute('fullnarp-room'),
240 time: el.getAttribute('fullnarp-time')
241 }
242 ws.send(JSON.stringify(message));
243 console.log('Sent:', message);
244 } else {
245 el.classList.remove('pending');
246 el.classList.add('failed');
247 }
248 }
249
250 /* When moving an element, conflict may have been resolved ... */
251 document.querySelectorAll('.conflict').forEach(elem => mark_conflict(elem));
252
253 /* ... or introduced */
254 mark_conflict(el);
255 mark_avail(el);
256 if (document.body.classList.contains('showcorrweb')) {
257 document.querySelectorAll('.corrweb').forEach(elem => elem.remove());
258 document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem));
259 }
260}
261
262function signalFullnarpConnect(state) {
263 document.body.classList.remove('fullnarp-connected', 'fullnarp-connecting', 'fullnarp-disconnected');
264 document.body.classList.add(state);
265}
266
267function getFullnarpData(lastupdate) {
268 signalFullnarpConnect('fullnarp-connecting');
269 ws = new WebSocket('wss://erdgeist.org/38C3/halfnarp/fullnarp-ws');
270
271 ws.onopen = () => {
272 console.log('Connected to WebSocket server');
273 //stateElement.textContent = 'Connected';
274 };
275
276 ws.onmessage = (event) => {
277 signalFullnarpConnect('fullnarp-connected');
278 const data = JSON.parse(event.data);
279 console.log('Received:', data);
280 for (const [eventid, event_new] of Object.entries(data.data)) {
281 if (document.getElementById(eventid))
282 set_all_attributes(eventid, 'day_'+event_new['day'], 'room'+event_new['room'], 'time_'+event_new['time'], true )
283 }
284 window.lastupdate = data.current_version;
285 current_version_string = ('00000'+data.current_version).slice(-5);
286 document.querySelector('.version').innerHTML = '<a href="https://erdgeist.org/38C3/halfnarp/versions/fullnarp_'+current_version_string+'.json">Version: '+data.current_version+'</a>';
287 };
288
289 ws.onerror = (error) => {
290 console.error('WebSocket error:', error);
291 };
292
293 ws.onclose = () => {
294 console.log('Disconnected from WebSocket server');
295 signalFullnarpConnect('fullnarp-disconnected');
296 // stateElement.textContent = 'Disconnected';
297 // Optionally attempt to reconnect after a delay
298 setTimeout(getFullnarpData, 5000);
299 };
300};
301
302function do_the_fullnarp() {
303 var halfnarpAPI = 'talks_38C3.json';
304 var fullnarpAPI = 'votes_38c3.json';
305 var allrooms = ['1','2','3']
306 var allminutes = ['00','05','10','15','20','25','30','35','40','45','50','55']
307 var allhours = ['10','11','12','13','14','15','16','17','18','19','20','21','22','23','00','01','02'];
308 var alldays = ['1','2','3','4'];
309 var voted = 0;
310 window.event_speakers = {};
311 window.votes = {};
312
313 /* Add handler for type ahead search input field */
314 var filter = document.getElementById('filter');
315
316 filter.onpaste = filter.oncut = filter.onkeypress = filter.onkeydown = filter.onkeyup = function() {
317 var cnt = this.value.toLowerCase();
318 if( cnt.length )
319 document.querySelectorAll('.event').forEach(elem => elem.style.display = (elem.textContent || elem.innerText || '').toLowerCase().includes(cnt) ? "block" : "none" );
320 else
321 document.querySelectorAll('.event').forEach(elem => elem.style.display = "block");
322 };
323
324 /* Add click handlers for event div sizers */
325 document.querySelector('.vsmallboxes').onclick = function() {
326 document.body.classList.remove('size-medium', 'size-large');
327 document.body.classList.add('size-small');
328 };
329
330 document.querySelector('.vmediumboxes').onclick = function() {
331 document.body.classList.remove('size-small', 'size-large');
332 document.body.classList.add('size-medium');
333 };
334
335 document.querySelector('.vlargeboxes').onclick = function() {
336 document.body.classList.remove('size-small', 'size-medium');
337 document.body.classList.add('size-large');
338 };
339
340 /* Add callbacks for view selector */
341 document.querySelector('.vlist').onclick = function() { toggle_grid(0); };
342 document.querySelector('.vday1').onclick = function() { toggle_grid(1); };
343 document.querySelector('.vday2').onclick = function() { toggle_grid(2); };
344 document.querySelector('.vday3').onclick = function() { toggle_grid(3); };
345 document.querySelector('.vday4').onclick = function() { toggle_grid(4); };
346 document.querySelector('.vdays').onclick = function() { toggle_grid(5); };
347
348 document.querySelector('.vleft').onclick = function() { document.body.classList.toggle('still-left'); };
349 document.querySelector('.vhalf').onclick = function() { document.body.classList.toggle('absolute'); };
350 document.querySelector('.vcorr').onclick = display_correlation;
351 document.querySelector('.vlang').onclick = function() { document.body.classList.toggle('languages'); };
352 document.querySelector('.vtrack').onclick = function() { document.body.classList.toggle('all-tracks'); };
353 document.querySelector('.vweb').onclick = function() {
354 if (document.body.classList.contains('showcorrweb'))
355 document.querySelectorAll('.corrweb').forEach(elem => elem.remove());
356 else
357 document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem));
358 document.body.classList.toggle('showcorrweb');
359 };
360
361 /* Make the trashbin a drop target */
362 var trash = document.querySelector('.trashbin');
363 trash.setAttribute('dropzone','move');
364 trash.ondragover = function (event) {
365 event.preventDefault(); // allows us to drop
366 this.classList.add('over');
367 return false;
368 };
369 trash.ondragleave = function (event) { this.classList.remove('over'); };
370 trash.ondrop = function (event) {
371 event.stopPropagation();
372 set_all_attributes(event.dataTransfer.getData('Text'), 'day_0', 'room_0', 'time_0000', false);
373 return false;
374 };
375
376 /* Create hour guides */
377 for (hour of allhours) {
378 var elem = document.createElement('hr');
379 elem.classList.add('guide', 'time_' + hour + '00');
380 document.body.append(elem);
381 elem = document.createElement('div');
382 elem.textContent = hour + '00';
383 elem.classList.add('guide', 'time_' + hour + '00');
384 document.body.append(elem);
385
386 for (minute of allminutes) {
387 for (room of allrooms) {
388 for (day of alldays) {
389 elem = document.createElement('div');
390 elem.classList.add('grid', 'time_' + hour + minute, 'day_' + day, 'room' + room );
391 elem.textContent = minute;
392 elem.setAttribute('dropzone', 'move');
393 elem.setAttribute('hour', '' + hour + minute);
394 elem.setAttribute('day', '' + day);
395 elem.setAttribute('room', room);
396 document.body.append(elem);
397 elem.ondragover = function (event) {
398 event.preventDefault(); // allows us to drop
399 this.classList.add('over');
400 return false;
401 }
402 elem.ondragleave = function (event) { this.classList.remove('over'); }
403 elem.ondrop = function (event) {
404 event.stopPropagation();
405 set_all_attributes(event.dataTransfer.getData('Text'), 'day_' + this.getAttribute('day'), 'room' + this.getAttribute('room'), 'time_' + this.getAttribute('hour'), false );
406 /* Don't go back to list view on successful drop */
407 document.body.classList.remove('was-list');
408 return false;
409 }
410 }
411 }
412 }
413 }
414
415 /* Fetch list of votes to display */
416 fetch(`${fullnarpAPI}?format=json`)
417 .then(response => {
418 if (!response.ok) {
419 throw new Error(`HTTP error when fetching fullnarp data! status: ${response.status}`);
420 }
421 return response.json();
422 }).then(data => {
423 window.raw_votes = data;
424 for (eventidlist of data)
425 for (eventid of eventidlist)
426 window.votes[eventid] = 1 + (window.votes[eventid] || 0 );
427 if( ++voted == 2 ) {
428 window.lastupdate = 0;
429 distribute_votes();
430 getFullnarpData(0);
431 }
432 }).catch(error => {
433 console.error('Fetch error:', error);
434 });
435
436
437 /* Fetch list of lectures to display */
438 fetch(`${halfnarpAPI}?format=json`)
439 .then(response => {
440 if (!response.ok) {
441 throw new Error(`HTTP error when fetching halfnarp data! status: ${response.status}`);
442 }
443 return response.json();
444 }).then(data => {
445 for (item of data) {
446 /* Take copy of hidden event template div and select them, if they're in
447 list of previous prereferences */
448 var t = document.getElementById('template').cloneNode(true);
449 var event_id = item.event_id.toString();
450 t.classList.add('event', 'duration_' + item.duration, 'lang_' + (item.language || 'en'));
451 t.setAttribute('event_id', event_id);
452 t.setAttribute('id', 'event_' + event_id)
453 t.setAttribute( 'fullnarp-duration', item.duration);
454
455 /* Sort textual info into event div */
456 t.querySelector('.title').textContent = item.title;
457 t.querySelector('.speakers').textContent = item.speaker_names;
458 t.querySelector('.abstract').append(item.abstract);
459
460 /* Store speakers and their availabilities */
461 window.event_speakers[event_id] = item.speakers;
462 for (speaker of item.speakers) {
463 var have_avails = false;
464 if (!speaker.availabilities)
465 console.log("Foo");
466 for (avail of speaker.availabilities) {
467 if (avail.id ) {
468 have_avails = true;
469 break;
470 }
471 }
472 if (!have_avails)
473 t.classList.add('has_unavailable_speaker');
474 }
475
476 t.setAttribute('draggable', 'true');
477
478 /* Make the event drag&droppable */
479 t.ondragstart = function( event, ui ) {
480 event.stopPropagation();
481
482 event.dataTransfer.setData('text/plain', this.id );
483 event.dataTransfer.dropEffect = 'move';
484 event.dataTransfer.effectAllowed = 'move';
485 event.target.classList.add('is-dragged');
486 }
487
488 /* While dragging make source element small enough to allow
489 dropping below its original area */
490 t.ondrag = function( event, ui ) {
491 event.stopPropagation();
492 event.target.classList.add('is-dragged');
493
494 /* When drag starts in list view, switch to calendar view */
495 if( document.body.classList.contains('in-list') ) {
496 toggle_grid(5);
497 document.body.classList.add('was-list');
498 }
499 if( document.body.classList.contains('in-drag') )
500 return;
501
502 document.body.classList.add('in-drag');
503 /* mark all possible drop points regarding to availability */
504 for (hour of allhours)
505 for (minute of allminutes)
506 for (day of alldays)
507 document.querySelectorAll('.grid.day_'+day+'.time_'+hour+minute).forEach(elem => elem.classList.toggle('possible', check_avail(event.target, day, hour+minute)));
508
509 }
510
511 t.ondragend = function( event, ui ) {
512 event.stopPropagation();
513
514 /* We removed in-list and the drop did not succeed. Go back to list view */
515 if (document.body.classList.contains('was-list'))
516 toggle_grid(0);
517
518 document.querySelectorAll('.over').forEach(elem => elem.classList.remove('over'));
519 document.querySelectorAll('.is-dragged').forEach(elem => elem.classList.remove('id-dragged'));
520 document.querySelectorAll('.possible').forEach(elem => elem.classList.remove('possible'));
521 document.body.classList.remove('in-drag', 'was-list');
522 }
523
524 /* start_time: 2014-12-29T21:15:00+01:00" */
525 var start_time = new Date(item.start_time);
526
527 var day = start_time.getDate()-26;
528 var hour = start_time.getHours();
529 var mins = start_time.getMinutes();
530
531 /* After midnight: sort into yesterday */
532 if( hour < 9 )
533 day--;
534
535 /* Fix up room for 38c3 */
536 room = (item.room_id || 'room_unknown').toString().replace('471','room1').replace('472','room2').replace('473','room3');
537
538 /* Apply attributes to sort events into calendar */
539 t.classList.add(room, 'day_' + day, 'time_' + (hour<10?'0':'') + hour + (mins<10?'0':'') + mins);
540 t.setAttribute('fullnarp-day', day);
541 t.setAttribute('fullnarp-time', (hour<10?'0':'') + hour + (mins<10?'0':'') + mins );
542 t.setAttribute('fullnarp-room', room.replace('room',''));
543
544 mark_avail(t);
545
546 t.onclick = function(event) {
547 _this = this;
548 document.body.classList.remove('in-drag');
549 if (document.body.classList.contains('correlate')) {
550 document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected'));
551 document.querySelectorAll('.event').forEach(elem => mark_correlation(elem, _this));
552 }
553 _this.classList.toggle('selected');
554 document.querySelectorAll('.info').forEach(elem => elem.classList.add('hidden'));
555 event.stopPropagation();
556 }
557
558 /* Put new event into DOM tree. Track defaults to 'Other' */
559 var track = item.track_id.toString();
560 t.classList.add('track_' + track );
561 var d = document.getElementById(track);
562 if (!d)
563 d = document.querySelector('#Other');
564 d.append(t);
565 };
566
567 if( ++voted == 2 ) {
568 window.lastupdate = 0;
569 distribute_votes();
570 getFullnarpData(0);
571 }
572 }).catch(error => {
573 console.error('Fetch error:', error);
574 });
575
576 document.onkeypress = function(e) {
577 document.body.classList.remove('in-drag');
578 if( document.activeElement.tagName == 'INPUT' || document.activeElement.tagName == 'TEXTAREA' )
579 return;
580 switch( e.charCode ) {
581 case 115: case 83: /* s */
582 var selected = document.querySelectorAll('.selected');
583 if( selected.length != 2 ) return;
584
585 var id0 = selected[0].getAttribute('id');
586 var day0 = selected[0].getAttribute('fullnarp-day');
587 var hour0 = selected[0].getAttribute('fullnarp-time');
588 var room0 = selected[0].getAttribute('fullnarp-room');
589
590 var id1 = selected[1].getAttribute('id');
591 var day1 = selected[1].getAttribute('fullnarp-day');
592 var hour1 = selected[1].getAttribute('fullnarp-time');
593 var room1 = selected[1].getAttribute('fullnarp-room');
594
595 set_all_attributes(id0, day1, room1, hour1, false);
596 set_all_attributes(id1, day0, room0, hour0, false);
597
598 break;
599 case 48: case 94: /* 0 */
600 toggle_grid(5);
601 break;
602 case 49: case 50: case 51: case 52: /* 1-4 */
603 toggle_grid(e.charCode-48);
604 break;
605 case 76: case 108: /* l */
606 toggle_grid(0);
607 break;
608 case 68: case 100: /* d */
609 toggle_grid(5);
610 break;
611 case 73: case 105: /* i */
612 document.body.classList.remove('all-tracks');
613 document.body.classList.toggle('languages');
614 break;
615 case 65: case 97: /* a */
616 case 72: case 104: /* h */
617 document.body.classList.toggle('absolute');
618 break;
619 case 81: case 113: /* q */
620 document.querySelectorAll('.selected').forEach(elem => elem.classList.remove('selected'));
621 break;
622 case 84: case 116: /* t */
623 document.body.classList.remove('languages');
624 document.body.classList.toggle('all-tracks');
625 break;
626 case 85: case 117: /* u */
627 document.body.classList.toggle('still-left');
628 break;
629 case 67: case 99: /* c */
630 display_correlation();
631 break;
632 case 87: case 119: /* w */
633 if (document.body.classList.contains('showcorrweb'))
634 document.querySelectorAll('.corrweb').forEach(elem => elem.remove());
635 else
636 document.querySelectorAll('.event').forEach(elem => show_all_correlates(elem));
637 document.body.classList.toggle('showcorrweb');
638 break;
639 }
640 };
641}