Completed
Push — main ( 97c1ff...2a7d5a )
by Jochen
05:40
created

byceps/static/behavior/seating.js   D

Complexity

Total Complexity 59
Complexity/F 1.48

Size

Lines of Code 316
Function Count 40

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 165
dl 0
loc 316
rs 4.08
c 0
b 0
f 0
wmc 59
mnd 19
bc 19
fnc 40
bpm 0.475
cpm 1.475
noi 0

21 Functions

Rating   Name   Duplication   Size   Complexity  
A seating.js ➔ mark_managed_seats 0 10 3
A seating.js ➔ get_selected_ticket_id 0 3 1
A seating.js ➔ set_current_managed_seat 0 11 3
B seating.js ➔ init_occupiable_seats 0 26 8
A seating.js ➔ get_seats_with_ticket_code 0 3 1
A seating.js ➔ reload_with_selected_ticket 0 3 1
A seating.js ➔ _select_ticket_on_click 0 8 3
A seating.js ➔ escape_html 0 5 1
A seating.js ➔ find_ticket_by_id 0 11 2
A seating.js ➔ mark_seat_for_selected_managed_ticket 0 7 2
A seating.js ➔ init_ticket_selector 0 4 1
A seating.js ➔ is_ticket_occupying_seat 0 3 1
A seating.js ➔ select_ticket 0 18 2
A seating.js ➔ mark_seats_as_occupiable 0 10 3
A seating.js ➔ get_selected_seat_label 0 6 1
A seating.js ➔ get_selected_ticket_code 0 3 1
A seating.js ➔ send_request 0 6 1
A seating.js ➔ _make_ticket_selector_open_on_click 0 8 3
B seating.js ➔ wire_seat_release_button 0 25 8
A seating.js ➔ init_seat_management 0 22 3
A seating.js ➔ get_managed_ticket_ids 0 11 2

How to fix   Complexity   

Complexity

Complex classes like byceps/static/behavior/seating.js often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

1
/**
2
 * Make tooltips appear on hover over seats.
3
 */
4
function init_seat_tooltips() {
5
  document.querySelectorAll('.seat-with-tooltip')
6
    .forEach(function(seatContainer) {
7
      seatContainer.addEventListener('mouseover', function() {
8
        const dataset = seatContainer.dataset;
9
10
        let tooltipHTML = '<div class="seat-label">' + dataset.label + '</div>';
11
12
        const ticketId = dataset.ticketId;
13
        if (ticketId !== undefined) {
14
          if (seatContainer.querySelector('.seat').classList.contains('seat--managed')) {
15
            const ticketCode = document
16
              .getElementById('ticket-selection')
17
              .querySelector('.ticket[data-id="' + ticketId + '"]')
18
              .dataset.code;
19
20
            tooltipHTML += '<div class="seat-ticket-code"><span class="dimmed">Ticket:</span> <strong>' + ticketCode + '</strong></div>';
21
          }
22
23
          const occupierName = dataset.occupierName;
24
          if (occupierName !== undefined) {
25
            tooltipHTML += '<div class="seat-occupier">'
26
                         + '<div class="seat-occupier-avatar"><div class="avatar size-48"><img src="' + dataset.occupierAvatar + '"></div></div>'
27
                         + '<div class="seat-occupier-name"><span class="dimmed">reserviert von</span><br><strong>' + escape_html(occupierName) + '</strong></div>'
28
                         + '</div>';
29
          }
30
        }
31
32
        const tooltipNode = document.createElement('div');
33
        tooltipNode.classList.add('seat-tooltip');
34
        tooltipNode.innerHTML = tooltipHTML;
35
        seatContainer.appendChild(tooltipNode);
36
      });
37
38
      seatContainer.addEventListener('mouseout', function() {
39
        const tooltip = seatContainer.querySelector('.seat-tooltip');
40
        // Tooltip element won't exist at this point if the mouse
41
        // cursor is above a seat when the page is reloaded, so the
42
        // 'mouseout' event will fire upon mouse move even though no
43
        // 'mouseover' has occurred, thus no tooltip has been
44
        // generated that could be removed.
45
        if (tooltip !== null) {
46
          tooltip.remove();
47
        }
48
      });
49
    });
50
}
51
52
53
/**
54
 * Initialize the seat management system.
55
 */
56
function init_seat_management(selected_ticket_id) {
57
  init_ticket_selector();
58
59
  const tickets = document.querySelectorAll('.ticket');
60
  if (tickets.length > 0) {
61
    // Pre-select ticket.
62
    let ticket_to_preselect;
63
    if (selected_ticket_id !== null) {
64
      ticket_to_preselect = find_ticket_by_id(selected_ticket_id);
65
    } else {
66
      // Select first ticket by default.
67
      ticket_to_preselect = tickets[0];
68
    }
69
    select_ticket(ticket_to_preselect);
70
71
    mark_seats_as_occupiable();
72
    init_occupiable_seats();
73
    mark_managed_seats();
74
    mark_seat_for_selected_managed_ticket();
75
    wire_seat_release_button();
76
  }
77
}
78
79
80
/**
81
 * Make ticket selector work.
82
 */
83
function init_ticket_selector() {
84
  _make_ticket_selector_open_on_click();
85
  _select_ticket_on_click();
86
}
87
88
89
/**
90
 * Make ticket selector open on click.
91
 */
92
function _make_ticket_selector_open_on_click() {
93
  const ticket_selector = document.querySelector('.ticket-selector');
94
  if (ticket_selector !== null) {
95
    ticket_selector.addEventListener('click', function() {
96
      this.classList.toggle('ticket-selector--open');
97
    });
98
  }
99
}
100
101
102
/**
103
 * Select ticket on click on ticket in ticket selector.
104
 */
105
function _select_ticket_on_click() {
106
  document.querySelectorAll('.ticket')
107
    .forEach(function(ticket) {
108
      ticket.addEventListener('click', function() {
109
        select_ticket(ticket);
110
      });
111
    });
112
}
113
114
115
/**
116
 * Return the ticket node in the selector with that ticket ID,
117
 * or `null` if not found.
118
 */
119
function find_ticket_by_id(ticket_id) {
120
  const ticket = document
121
    .getElementById('ticket-selection')
122
    .querySelector('.ticket[data-id="' + ticket_id + '"]');
123
124
  if (ticket === null) {
125
    return null;
126
  }
127
128
  return ticket;
129
}
130
131
132
/**
133
 * Set the ticket as the selected one in the ticket selector (both
134
 * visually and regarding its internal state).
135
 */
136
function select_ticket(ticket) {
137
  const ticket_id = ticket.dataset.id;
138
  const ticket_code = ticket.dataset.code;
139
140
  const ticket_selection = document.getElementById('ticket-selection');
141
  ticket_selection.dataset.selectedId = ticket_id;
142
  ticket_selection.dataset.selectedCode = ticket_code;
143
  ticket_selection.querySelectorAll('.ticket--current')
144
    .forEach(function(ticket) {
145
      ticket.classList.remove('ticket--current');
146
    });
147
148
  ticket.classList.add('ticket--current');
149
150
  set_current_managed_seat(ticket_id);
151
152
  document.getElementById('release-seat-trigger').disabled = !is_ticket_occupying_seat(ticket);
153
}
154
155
156
function set_current_managed_seat(ticket_id) {
157
  document.querySelectorAll('.area .seat--managed-current')
158
    .forEach(function(seat) {
159
      seat.classList.remove('seat--managed-current');
160
    });
161
162
  document.querySelectorAll('.seat-with-tooltip[data-ticket-id="' + ticket_id + '"] .seat--managed')
163
    .forEach(function(seat) {
164
      seat.classList.add('seat--managed-current');
165
    });
166
}
167
168
169
function wire_seat_release_button() {
170
  const release_seat_trigger = document.getElementById('release-seat-trigger');
171
  if (release_seat_trigger !== null) {
172
    release_seat_trigger.addEventListener('click', function() {
173
      const seat_label = get_selected_seat_label();
174
      const confirmation_label = seat_label + ' (belegt durch Ticket ' + get_selected_ticket_code() + ') freigeben?';
175
      if (confirm(confirmation_label)) {
176
        const ticket_id = get_selected_ticket_id();
177
178
        const request_url = '/seating/ticket/' + ticket_id + '/seat';
179
180
        send_request('DELETE', request_url, function() {
181
          if (this.readyState === XMLHttpRequest.DONE) {
182
            if (this.status === 204) {
183
              reload_with_selected_ticket(ticket_id);
184
            } else if (this.status === 403 || this.status === 404 || this.status === 500) {
185
              reload_with_selected_ticket(ticket_id);
186
            }
187
          }
188
        });
189
      };
190
      return false;
191
    });
192
  }
193
}
194
195
196
function get_selected_ticket_id() {
197
  return document.getElementById('ticket-selection').dataset.selectedId;
198
}
199
200
201
function get_selected_ticket_code() {
202
  return document.getElementById('ticket-selection').dataset.selectedCode;
203
}
204
205
206
function is_ticket_occupying_seat(ticket) {
207
  return ticket.dataset.seatLabel !== undefined;
208
}
209
210
211
function get_selected_seat_label() {
212
  const ticket_selection = document.getElementById('ticket-selection');
213
  const ticket_code = ticket_selection.dataset.selectedCode;
214
  const ticket = ticket_selection.querySelector('.ticket[data-code="' + ticket_code + '"]');
215
  return ticket.dataset.seatLabel;
216
}
217
218
219
function get_managed_ticket_ids() {
220
  const managed_ticket_ids = new Set();
221
222
  document.getElementById('ticket-selection')
223
    .querySelectorAll('.ticket')
224
    .forEach(function(ticket) {
225
      managed_ticket_ids.add(ticket.dataset.id);
226
    });
227
228
  return managed_ticket_ids;
229
}
230
231
232
function get_seats_with_ticket_code(ticket_id) {
233
  return document.querySelectorAll('.seat-with-tooltip[data-ticket-id="' + ticket_id + '"] .seat');
234
}
235
236
237
function mark_managed_seats() {
238
  const managed_ticket_ids = get_managed_ticket_ids();
239
240
  for (const ticket_id of managed_ticket_ids.values()) {
241
    get_seats_with_ticket_code(ticket_id)
242
      .forEach(function(seat) {
243
        seat.classList.add('seat--managed');
244
      });
245
  }
246
}
247
248
249
function mark_seat_for_selected_managed_ticket() {
250
  const ticket_id = get_selected_ticket_id();
251
  get_seats_with_ticket_code(ticket_id)
252
    .forEach(function(seat) {
253
      seat.classList.add('seat--managed-current');
254
    });
255
}
256
257
258
function mark_seats_as_occupiable() {
259
  document.querySelectorAll('.seat')
260
    .forEach(function(seat) {
261
      const ticket_id = seat.parentNode.dataset.ticketId;
262
      if (ticket_id === undefined) {
263
        seat.classList.add('seat--occupiable');
264
      }
265
    }
266
  );
267
}
268
269
270
function init_occupiable_seats() {
271
  document.querySelectorAll('.seat--occupiable')
272
    .forEach(function(seat) {
273
      seat.addEventListener('click', function() {
274
        const seat_label = seat.parentNode.dataset.label;
275
        const confirmation_label = seat_label + ' mit Ticket ' + get_selected_ticket_code() + ' reservieren?';
276
        if (confirm(confirmation_label)) {
277
          const seat_id = seat.parentNode.dataset.seatId;
278
          const ticket_id = get_selected_ticket_id();
279
280
          const request_url = '/seating/ticket/' + ticket_id + '/seat/' + seat_id;
281
282
          send_request('POST', request_url, function() {
283
            if (this.readyState === XMLHttpRequest.DONE) {
284
              if (this.status === 204) {
285
                reload_with_selected_ticket(ticket_id);
286
              } else if (this.status === 403 || this.status === 404 || this.status === 500) {
287
                reload_with_selected_ticket(ticket_id);
288
              }
289
            }
290
          });
291
        };
292
        return false;
293
      });
294
    });
295
}
296
297
298
function send_request(method, url, state_change_callback) {
299
  const request = new XMLHttpRequest();
300
  request.open(method, url);
301
  request.onreadystatechange = state_change_callback;
302
  request.send();
303
}
304
305
306
function reload_with_selected_ticket(ticket_id) {
307
  location.href = location.href.split('?')[0] + '?ticket=' + ticket_id;
308
}
309
310
311
/**
312
 * Use browser's built-in functionality to quickly and safely escape
313
 * HTML in the given string.
314
 */
315
function escape_html(str) {
316
  const elem = document.createElement('span');
317
  elem.appendChild(document.createTextNode(str));
318
  return elem.innerHTML;
319
}
320