GitHub Access Token became invalid

It seems like the GitHub access token used for retrieving details about this repository from GitHub became invalid. This might prevent certain types of inspections from being run (in particular, everything related to pull requests).
Please ask an admin of your repository to re-new the access token on this website.
Passed
Push — master ( 6812c5...04c9e1 )
by Richard
02:58
created

ticketer.initTemplates   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
dl 0
loc 5
rs 9.4285
c 1
b 0
f 0
cc 2
nc 2
nop 3
1
/**
2
 * Created by wechsler on 04/08/15.
3
 */
4
var ticketer = (function() {
5
  'use strict';
6
7
  return {
8
    upcomingTicketTemplate: null,
9
    manageTemplate: null,
10
    songAutocompleteItemTemplate: null,
11
    editTicketTemplate: null,
12
    songDetailsTemplate: null,
13
    appMessageTarget: null,
14
    searchCount: 10,
15
    instrumentOrder: ['V', 'G', 'B', 'D', 'K'],
16
    defaultSongLengthSeconds: 240,
17
    defaultSongIntervalSeconds: 120,
18
    messageTimer: null,
19
20
    /**
21
     * @var {{songInPreview,upcomingCount,iconMapHtml}}
22
     */
23
    displayOptions: {},
24
25
    /**
26
     * List of all performers (objects) who've signed up in this session
27
     */
28
    performers: [],
29
30
    /**
31
     * List of all platform names in the system
32
     */
33
    platforms: [],
34
35
    performerExists: function(performerName) {
36
      for (var i = 0; i < this.performers.length; i++) {
37
        if (this.performers[i].performerName.toLowerCase() == performerName.toLowerCase()) {
38
          return true;
39
        }
40
      }
41
      return false;
42
    },
43
44
    addPerformerByName: function(performerName) {
45
      this.performers.push({performerName: performerName});
46
      // Now resort it
47
      this.performers.sort(function(a, b) {
48
        return a.performerName.localeCompare(b.performerName);
49
      });
50
    },
51
52
    /**
53
     * Run the "upcoming" panel
54
     */
55
    go: function() {
56
      this.initTemplates();
57
58
      ticketer.reloadTickets();
59
      setInterval(function() {
60
        ticketer.reloadTickets();
61
      }, 10000);
62
    },
63
64
    /**
65
     * Draw an "upcoming" ticket
66
     * @param ticket {{band}}
67
     * @returns {*}
68
     */
69
    drawDisplayTicket: function(ticket) {
70
      // Sort band into standard order
71
      var unsortedBand = ticket.band;
72
      var sortedBand = {};
73
      for (var i = 0; i < this.instrumentOrder.length; i++) {
74
        var instrument = this.instrumentOrder[i];
75
        if (unsortedBand.hasOwnProperty(instrument)) {
76
          sortedBand[instrument] = unsortedBand[instrument];
77
        }
78
      }
79
      ticket.band = sortedBand;
80
      var ticketParams = {ticket: ticket, icons: this.displayOptions.iconMapHtml};
81
      return this.upcomingTicketTemplate(ticketParams);
82
    },
83
84
    /**
85
     * Draw a "queue management" ticket
86
     * @param ticket
87
     * @returns {*}
88
     */
89
    drawManageableTicket: function(ticket) {
90
      ticket.used = Number(ticket.used); // Force int
91
92
      return this.manageTemplate({ticket: ticket});
93
    },
94
95
    /**
96
     * Reload all tickets on the upcoming page
97
     */
98
    reloadTickets: function() {
99
      var that = this;
100
101
      $.get('/api/next', function(tickets) {
102
103
        var out = '';
104
        for (var i = 0; i < tickets.length; i++) {
105
          var ticket = tickets[i];
106
          out += that.drawDisplayTicket(ticket);
107
        }
108
109
        var target = $('#target');
110
        target.html(out);
111
112
        target.find('.auto-font').each(
113
          function() {
114
            var fixedWidth = $(this).data('fixed-assetwidth');
115
            if (!fixedWidth) {
116
              fixedWidth = 0;
117
            }
118
            fixedWidth = Number(fixedWidth);
119
120
            var spaceUsedByText = (this.scrollWidth - fixedWidth);
121
            var spaceAvailableForText = (this.clientWidth - fixedWidth);
122
            var rawScale = Math.max(spaceUsedByText / spaceAvailableForText, 1);
123
            var scale = 1.05 * rawScale;
124
125
            if (that.displayOptions.adminQueueHasControls && that.displayOptions.isAdmin) {
126
              scale *= 1.25;
127
            }
128
129
            // 1.05 extra scale to fit neatly, fixedWidth is non-scaling elements
130
            var font = Number($(this).css('font-size').replace(/[^0-9]+$/, ''));
131
            $(this).css('font-size', Number(font / scale).toFixed() + 'px');
132
          }
133
        );
134
135
        target.find('.performingButton').click(function() {
136
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
137
          if (window.confirm('Mark song as performing?')) {
138
            that.performButtonCallback(this);
139
          }
140
        });
141
        target.find('.removeButton').click(function() {
142
          var ticketId = $(this).data('ticket-id');
0 ignored issues
show
Unused Code introduced by
The variable ticketId seems to be never used. Consider removing it.
Loading history...
143
          if (window.confirm('Remove song?')) {
144
            that.removeButtonCallback(this);
145
          }
146
        });
147
148
      });
149
    },
150
151
    /**
152
     * Enable queue management ticket buttons in the specified element
153
     * @param topElement
154
     */
155
    enableButtons: function(topElement) {
156
      var that = this;
157
158
      $(topElement).find('.performButton').click(function() {
159
        that.performButtonCallback(this);
160
      });
161
162
      $(topElement).find('.removeButton').click(function() {
163
        that.removeButtonCallback(this);
164
      });
165
166
      $(topElement).find('.editButton').click(function() {
167
        that.editButtonCallback(this);
168
      });
169
    },
170
171
    enableSongSearchBox: function(songSearchInput, songSearchResultsTarget, songClickHandler) {
172
      var that = this;
173
      $(songSearchInput).keyup(
174
        function() {
175
          var songComplete = $(songSearchResultsTarget);
176
          var input = $(this);
177
          var searchString = input.val();
178
          if (searchString.length >= 3) {
179
            $.ajax({
180
              method: 'POST',
181
              data: {
182
                searchString: searchString,
183
                searchCount: that.searchCount
184
              },
185
              url: '/api/songSearch',
186
              /**
187
               * @param {{songs, searchString}} data
188
               */
189
              success: function(data) {
190
                var songs = data.songs;
191
                if (input.val() == data.searchString) {
192
                  // Ensure autocomplete response is still valid for current input value
193
                  var out = '';
194
                  var song; // Used twice below
195
                  for (var i = 0; i < songs.length; i++) {
196
                    song = songs[i];
197
                    out += that.songAutocompleteItemTemplate({song: song});
198
                  }
199
                  songComplete.html(out).show();
200
201
                  // Now attach whole song as data:
202
                  for (i = 0; i < songs.length; i++) {
203
                    song = songs[i];
204
                    var songId = song.id;
205
                    songComplete.find('.acSong[data-song-id=' + songId + ']').data('song', song);
206
                  }
207
208
                  that.enableAcSongSelector(songComplete, songClickHandler);
209
                }
210
              },
211
              error: function(xhr, status, error) {
212
                void(error);
213
              }
214
            });
215
          } else {
216
            songComplete.html('');
217
          }
218
        }
219
      );
220
    },
221
222
    /**
223
     * Completely (re)generate the add ticket control panel and enable its controls
224
     * @param {?number} currentTicket Optional
225
     */
226
    resetEditTicketBlock: function(currentTicket) {
227
      var that = this;
228
      var controlPanelOuter = $('.editTicketOuter');
229
230
      // Current panel state in function scope
231
      var selectedInstrument = 'V';
232
      var currentBand = {};
233
234
      // Reset band to empty (or to ticket band state)
235
      for (var instrumentIdx = 0; instrumentIdx < that.instrumentOrder.length; instrumentIdx++) {
236
        var instrument = that.instrumentOrder[instrumentIdx];
237
        currentBand[instrument] = [];
238
239
        if (currentTicket && currentTicket.band) {
240
          // Ticket.band is a complex datatype. Current band is just one array of names per instrument. Unpack to show.
241
          if (currentTicket.band.hasOwnProperty(instrument)) {
242
            var instrumentPerformerObjects = currentTicket.band[instrument];
243
            for (var pIdx = 0; pIdx < instrumentPerformerObjects.length; pIdx++) {
244
              currentBand[instrument].push(instrumentPerformerObjects[pIdx].performerName);
245
            }
246
          }
247
        }
248
        // Store all instruments as arrays - most can only be single, but vocals is 1..n potentially
249
      }
250
251
      drawEditTicketForm(currentTicket);
252
      // X var editTicketBlock = $('.editTicket'); // only used in inner scope (applyNewSong)
253
254
      // Enable 'Add' button
255
      $('.editTicketButton').click(editTicketCallback);
256
      $('.cancelTicketButton').click(cancelTicketCallback);
257
      $('.removeSongButton').click(removeSong);
258
259
      $('.toggleButton').click(
260
        function() {
261
          var check = $(this).find('input[type=checkbox]');
262
          check.prop('checked', !check.prop('checked'));
263
        }
264
      );
265
266
      // Enable the instrument tabs
267
      var allInstrumentTabs = controlPanelOuter.find('.instrument');
268
269
      allInstrumentTabs.click(
270
        function() {
271
          selectedInstrument = $(this).data('instrumentShortcode');
272
          setActiveTab(selectedInstrument);
273
        }
274
      );
275
276
      var ticketTitleInput = $('.editTicketTitle');
277
278
      // Copy band name into summary area on Enter
279
      ticketTitleInput.keydown(function(e) {
280
        if (e.keyCode == 13) {
281
          updateBandSummary();
282
        }
283
      });
284
285
      $('.newPerformer').keydown(function(e) {
286
        if (e.keyCode == 13) {
287
          var newPerformerInput = $('.newPerformer');
288
          var newName = newPerformerInput.val();
289
          if (newName.trim().length) {
290
            alterInstrumentPerformerList(selectedInstrument, newName, true);
291
          }
292
          newPerformerInput.val('');
293
        }
294
      });
295
296
      // Set up the song search box in this control panel and set the appropriate callback
297
      var songSearchInput = '.addSongTitle';
298
      var songSearchResultsTarget = '.songComplete';
299
300
      this.enableSongSearchBox(songSearchInput, songSearchResultsTarget, applyNewSong);
301
302
      // ************* Inner functions **************
303
      /**
304
       * Switch to the next visible instrument tab
305
       */
306
      function nextInstrumentTab() {
307
        // Find what offset we're at in instrumentOrder
308
        var currentOffset = 0;
309
        for (var i = 0; i < that.instrumentOrder.length; i++) {
310
          if (that.instrumentOrder[i] == selectedInstrument) {
311
            currentOffset = i;
312
          }
313
        }
314
        var nextOffset = currentOffset + 1;
315
        if (nextOffset >= that.instrumentOrder.length) {
316
          nextOffset = 0;
317
        }
318
        var instrument = that.instrumentOrder[nextOffset];
319
        selectedInstrument = instrument; // Reset before we redraw tabs
320
        var newActiveTab = setActiveTab(instrument);
321
322
        // Make sure we switch to a *visible* tab
323
        if (newActiveTab.hasClass('instrumentUnused')) {
324
          nextInstrumentTab();
325
        }
326
      }
327
328
      /**
329
       * (re)Draw the add/edit ticket control panel in the .editTicketOuter element
330
       */
331
      function drawEditTicketForm(ticket) {
332
        var templateParams = {performers: that.performers};
333
        if (ticket) {
334
          templateParams.ticket = ticket;
335
        }
336
        controlPanelOuter.html(that.editTicketTemplate(templateParams));
337
        updateInstrumentTabs();
338
        rebuildPerformerList(controlPanelOuter.find('.performers'));
0 ignored issues
show
Bug introduced by
The call to rebuildPerformerList seems to have too many arguments starting with controlPanelOuter.find(".performers").
Loading history...
339
        if (ticket && ticket.song) {
340
          applyNewSong(ticket.song);
341
        }
342
      }
343
344
      function findPerformerInstrument(name) {
345
        var instrumentPlayers;
346
        for (var instrumentCode in currentBand) {
347
          if (currentBand.hasOwnProperty(instrumentCode)) {
348
            instrumentPlayers = currentBand[instrumentCode];
349
            for (var i = 0; i < instrumentPlayers.length; i++) {
350
              if (instrumentPlayers[i].toUpperCase() == name.toUpperCase()) {
351
                return instrumentCode;
352
              }
353
            }
354
          }
355
        }
356
        return null;
357
      }
358
359
      /**
360
       * Rebuild list of performer buttons according to overall performers list
361
       * and which instruments they are assigned to
362
       */
363
      function rebuildPerformerList() {
364
        var newButton;
365
        var targetElement = controlPanelOuter.find('.performers');
366
        targetElement.text(''); // Remove existing list
367
368
        var lastInitial = '';
369
        var performerCount = that.performers.length;
370
        var letterSpan;
371
        for (var pIdx = 0; pIdx < performerCount; pIdx++) {
372
          var performerName = that.performers[pIdx].performerName;
373
          var performerInstrument = findPerformerInstrument(performerName);
374
          var isPerforming = performerInstrument ? 1 : 0;
375
          var initialLetter = performerName.charAt(0).toUpperCase();
376
          if (lastInitial !== initialLetter) {
377
            if (letterSpan) {
378
              targetElement.append(letterSpan);
379
            }
380
            letterSpan = $('<span class="letterSpan"></span>');
381
            if ((performerCount > 15)) {
382
              letterSpan.append($('<span class="initialLetter">' + initialLetter + '</span>'));
383
            }
384
          }
385
          lastInitial = initialLetter;
386
387
          newButton = $('<span></span>');
388
          newButton.addClass('btn addPerformerButton');
389
          newButton.addClass(isPerforming ? 'btn-primary' : 'btn-default');
390
          if (isPerforming && (performerInstrument !== selectedInstrument)) { // Dim out buttons for other instruments
391
            newButton.attr('disabled', 'disabled');
392
          }
393
          newButton.text(performerName);
394
          newButton.data('selected', isPerforming); // This is where it gets fun - check if user is in band!
395
          letterSpan.append(newButton);
0 ignored issues
show
Bug introduced by
The variable letterSpan seems to not be initialized for all possible execution paths.
Loading history...
396
        }
397
        targetElement.append(letterSpan);
398
399
        // Enable the new buttons
400
        $('.addPerformerButton').click(function() {
401
          var name = $(this).text();
402
          var selected = $(this).data('selected') ? 0 : 1; // Reverse to get new state
403
          if (selected) {
404
            $(this).removeClass('btn-default').addClass('btn-primary');
405
          } else {
406
            $(this).removeClass('btn-primary').addClass('btn-default');
407
          }
408
          $(this).data('selected', selected); // Toggle
409
410
          alterInstrumentPerformerList(selectedInstrument, name, selected);
411
        });
412
      }
413
414
      /**
415
       * Handle click on edit ticket button
416
       */
417
      function editTicketCallback() {
418
        var titleInput = $('.editTicketTitle');
419
        var ticketTitle = titleInput.val();
420
        var songInput = $('.selectedSongId');
421
        var songId = songInput.val();
422
        var privateCheckbox = $('input.privateCheckbox');
423
        var isPrivate = privateCheckbox.is(':checked');
424
        var blockingCheckbox = $('input.blockingCheckbox');
425
        var isBlocked = blockingCheckbox.is(':checked');
426
427
        var data = {
428
          title: ticketTitle,
429
          songId: songId,
430
          band: currentBand,
431
          private: isPrivate,
432
          blocking: isBlocked
433
        };
434
435
        if (currentTicket) {
436
          data.existingTicketId = currentTicket.id;
437
        }
438
439
        that.showAppMessage('Saving ticket');
440
441
        $.ajax({
442
            method: 'POST',
443
            data: data,
444
            url: '/api/saveTicket',
445
            success: function(data, status) {
446
              that.showAppMessage('Saved ticket', 'success');
447
448
              void(status);
449
              var ticketId = data.ticket.id;
450
451
              var ticketBlockSelector = '.ticket[data-ticket-id="' + ticketId + '"]';
452
              var existingTicketBlock = $(ticketBlockSelector);
453
              if (existingTicketBlock.length) {
454
                // Replace existing
455
                existingTicketBlock.after(that.drawManageableTicket(data.ticket));
456
                existingTicketBlock.remove();
457
              } else {
458
                // Append new
459
                $('#target').append(that.drawManageableTicket(data.ticket));
460
              }
461
462
              var ticketBlock = $(ticketBlockSelector);
463
              ticketBlock.data('ticket', data.ticket);
464
              that.enableButtons(ticketBlock);
465
466
              if (data.performers) {
467
                that.performers = data.performers;
468
              }
469
470
              that.updatePerformanceStats();
471
              that.resetEditTicketBlock();
472
473
            },
474
            error: function(xhr, status, error) {
475
              var message = 'Ticket save failed';
476
              that.reportAjaxError(message, xhr, status, error);
477
              void(error);
478
              // FIXME handle error
479
            }
480
          }
481
        );
482
      }
483
484
      function cancelTicketCallback() {
485
        that.resetEditTicketBlock();
486
      }
487
488
      function getTabByInstrument(instrument) {
489
        return controlPanelOuter.find('.instrument[data-instrument-shortcode=' + instrument + ']');
490
      }
491
492
      function setActiveTab(selectedInstrument) {
493
        allInstrumentTabs.removeClass('instrumentSelected');
494
        var selectedTab = getTabByInstrument(selectedInstrument);
495
        selectedTab.addClass('instrumentSelected');
496
        rebuildPerformerList(); // Rebuild in current context
497
        return selectedTab;
498
      }
499
500
      function updateBandSummary() {
501
        var bandName = $('.editTicketTitle').val();
502
        var members = [];
503
        for (var instrument in currentBand) {
504
          if (currentBand.hasOwnProperty(instrument)) {
505
            for (var i = 0; i < currentBand[instrument].length; i++) {
506
              members.push(currentBand[instrument][i]);
507
            }
508
          }
509
        }
510
        var memberList = members.join(', ');
511
        var summaryHtml = (bandName ? bandName + '<br />' : '') + memberList;
512
        $('.selectedBand').html(summaryHtml);
513
      }
514
515
      function updateInstrumentTabs() {
516
        var performersSpan;
517
        var performerString;
518
519
        for (var iIdx = 0; iIdx < that.instrumentOrder.length; iIdx++) {
520
          var instrument = that.instrumentOrder[iIdx];
521
522
          performersSpan = controlPanelOuter
523
            .find('.instrument[data-instrument-shortcode=' + instrument + ']')
524
            .find('.instrumentPerformer');
525
526
          performerString = currentBand[instrument].join(', ');
527
          if (!performerString) {
528
            performerString = '<i>Needed</i>';
529
          }
530
          performersSpan.html(performerString);
531
        }
532
533
        updateBandSummary();
534
      }
535
536
      /**
537
       * Handle performer add / remove by performer button / text input
538
       * @param instrument
539
       * @param changedPerformer
540
       * @param isAdd
541
       */
542
      function alterInstrumentPerformerList(instrument, changedPerformer, isAdd) {
543
        var currentInstrumentPerformers = currentBand[selectedInstrument];
544
545
        var newInstrumentPerformers = [];
546
        for (var i = 0; i < currentInstrumentPerformers.length; i++) {
547
          var member = currentInstrumentPerformers[i].trim(); // Trim only required when we draw data from manual input
548
          if (member.length) {
549
            if (member.toUpperCase() != changedPerformer.toUpperCase()) {
550
              // If it's not the name on our button, no change
551
              newInstrumentPerformers.push(member);
552
            }
553
          }
554
        }
555
556
        if (isAdd) { // If we've just selected a new user, append them
557
          newInstrumentPerformers.push(changedPerformer);
558
          if (!that.performerExists(changedPerformer)) {
559
            that.addPerformerByName(changedPerformer);
560
          }
561
        }
562
563
        currentBand[selectedInstrument] = newInstrumentPerformers;
564
        // Now update band with new performers of this instrument
565
566
        updateInstrumentTabs();
567
        rebuildPerformerList();
568
569
        if (newInstrumentPerformers.length) { // If we've a performer for this instrument, skip to next
570
          nextInstrumentTab();
571
        }
572
573
      }
574
575
      /**
576
       *
577
       * @param {{id, title, artist, hasKeys, hasHarmony}} song
578
       */
579
      function applyNewSong(song) {
580
        var selectedId = song.id;
581
        var selectedSong = song.artist + ': ' + song.title;
582
583
        var removeSongButton = $('.removeSongButton');
584
        removeSongButton.removeClass('hidden');
585
586
        // Perform actions with selected song
587
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
588
589
        editTicketBlock.find('input.selectedSongId').val(selectedId);
590
        editTicketBlock.find('.selectedSong').text(selectedSong);
591
        var keysTab = controlPanelOuter.find('.instrumentKeys');
592
        if (song.instruments && (song.instruments.indexOf('Keyboard') !== -1)) {
593
          keysTab.removeClass('instrumentUnused');
594
        } else {
595
          keysTab.addClass('instrumentUnused');
596
          // Also uncheck any performer for instrument (allow use elsewhere)
597
          currentBand.K = [];
598
          keysTab.find('.instrumentPerformer').html('<i>Needed</i>');
599
          rebuildPerformerList();
600
        }
601
      }
602
603
      function removeSong() {
604
        var editTicketBlock = $('.editTicket'); // Temp hack, should already be in scope?
605
        editTicketBlock.find('input.selectedSongId').val(0);
606
        editTicketBlock.find('.selectedSong').text('');
607
        $(songSearchInput).val('');
608
        var removeSongButton = $('.removeSongButton');
609
        removeSongButton.hide();
610
611
      }
612
    },
613
614
    manage: function(tickets) {
615
      var that = this;
616
      this.appMessageTarget = $('#appMessages');
617
      this.initTemplates();
618
      var ticket, ticketBlock; // For loop iterations
619
620
      var out = '';
621
      for (var i = 0; i < tickets.length; i++) {
622
        ticket = tickets[i];
623
        out += that.drawManageableTicket(ticket);
624
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
625
        ticketBlock.data('ticket', ticket);
626
      }
627
      $('#target').html(out);
628
629
      // Find new tickets (now they're DOM'd) and add data to them
630
      for (i = 0; i < tickets.length; i++) {
631
        ticket = tickets[i];
632
        ticketBlock = $('.ticket[data-ticket-id="' + ticket.id + '"]');
633
        ticketBlock.data('ticket', ticket);
634
      }
635
636
      var $sortContainer = $('.sortContainer');
637
      $sortContainer.sortable({
638
        axis: 'y',
639
        update: function(event, ui) {
640
          void(event);
641
          void(ui);
642
          that.ticketOrderChanged();
643
        }
644
      }).disableSelection().css('cursor', 'move');
645
646
      this.enableButtons($sortContainer);
647
648
      this.updatePerformanceStats();
649
650
      this.resetEditTicketBlock();
651
652
    },
653
654
    initSearchPage: function() {
655
      var that = this;
656
      this.initTemplates();
657
      this.enableSongSearchBox('.searchString', '.songComplete', that.searchPageSongSelectionClick);
658
    },
659
660
    initTemplates: function() {
661
      var that = this;
662
663
      // CommaList = each, with commas joining. Returns value at t as tuple {k,v}
664
      // "The options hash contains a function (options.fn) that behaves like a normal compiled Handlebars template."
665
      // If called without inner template, options.fn is not populated
666
      Handlebars.registerHelper('commalist', function(context, options) {
667
        var retList = [];
668
669
        for (var key in context) {
670
          if (context.hasOwnProperty(key)) {
671
            retList.push(options.fn ? options.fn({k: key, v: context[key]}) : context[key]);
672
          }
673
        }
674
675
        return retList.join(', ');
676
      });
677
678
      Handlebars.registerHelper('instrumentIcon', function(instrumentCode) {
679
        var icon = '<span class="instrumentTextIcon">' + instrumentCode + '</span>';
680
        if (that.displayOptions.hasOwnProperty('iconMapHtml')) {
681
          if (that.displayOptions.iconMapHtml.hasOwnProperty(instrumentCode)) {
682
            icon = that.displayOptions.iconMapHtml[instrumentCode];
683
          }
684
        }
685
        return new Handlebars.SafeString(icon);
686
      });
687
688
      Handlebars.registerHelper('durationToMS', function(duration) {
689
        var seconds = (duration % 60);
690
        if (seconds < 10) {
691
          seconds = '0' + seconds;
692
        }
693
        return Math.floor(duration / 60) + ':' + seconds;
694
      });
695
696
      Handlebars.registerHelper('gameList', function(song) {
697
        return song.platforms.join(', ');
698
      });
699
700
      Handlebars.registerHelper('ifContains', function(haystack, needle, options) {
701
        if (haystack.indexOf(needle) !== -1) {
0 ignored issues
show
Complexity Best Practice introduced by
There is no return statement if haystack.indexOf(needle) !== -1 is false. Are you sure this is correct? If so, consider adding return; explicitly.

This check looks for functions where a return statement is found in some execution paths, but not in all.

Consider this little piece of code

function isBig(a) {
    if (a > 5000) {
        return "yes";
    }
}

console.log(isBig(5001)); //returns yes
console.log(isBig(42)); //returns undefined

The function isBig will only return a specific value when its parameter is bigger than 5000. In any other case, it will implicitly return undefined.

This behaviour may not be what you had intended. In any case, you can add a return undefined to the other execution path to make the return value explicit.

Loading history...
702
          return options.fn(this);
703
        }
704
      });
705
706
      this.manageTemplate = Handlebars.compile(
707
        '<div class="ticket well well-sm {{#if ticket.used}}used{{/if}}' +
708
        ' {{#each ticket.song.platforms }}platform{{ this }} {{/each}}' +
709
        ' {{#if ticket.band.K}}withKeys{{/if}}"' +
710
        ' data-ticket-id="{{ ticket.id }}">' +
711
        '        <div class="pull-right">' +
712
        (function() {
713
          var s = '';
714
          for (var i = 0; i < that.platforms.length; i++) {
715
            var p = that.platforms[i];
716
            s += '<div class="gameMarker gameMarker' + p + '">' +
717
              '{{#ifContains ticket.song.platforms "' + p + '" }}' + p + '{{/ifContains}}</div>';
718
          }
719
          return s;
720
        })() +
721
        '        <button class="btn btn-primary performButton" data-ticket-id="{{ ticket.id }}">Performing</button>' +
722
        '        <button class="btn btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
723
        '        <button class="btn editButton" data-ticket-id="{{ ticket.id }}">' +
724
        '<span class="fa fa-edit" title="Edit"></span>' +
725
        '</button>' +
726
        '        </div>' +
727
        '<div class="ticketOrder">' +
728
        '<div class="ticketOrdinal"></div>' +
729
        '<div class="ticketTime"></div>' +
730
        '</div>' +
731
        '<div class="ticketId">' +
732
        '<span class="fa fa-ticket"></span> {{ ticket.id }}</div> ' +
733
        '<div class="ticketMeta">' +
734
        '<div class="blocking">' +
735
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
736
        '</div>' +
737
        '<div class="private">' +
738
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
739
        '</div>' +
740
        '</div>' +
741
        '<div class="pendingSong">' +
742
        '<span class="fa fa-group"></span> ' +
743
744
        // Display performers with metadata if valid, else just the band title.
745
        /*
746
         '{{#if ticket.performers}}' +
747
         '{{#each ticket.performers}}' +
748
         '<span class="performer performerDoneCount{{songsDone}}" ' +
749
         'data-performer-id="{{performerId}}"> {{performerName}} ' +
750
         ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
751
         '</span>' +
752
         '{{/each}}' +
753
         '{{else}}' +
754
         '{{ ticket.title }}' +
755
         '{{/if}}' +
756
         */
757
758
        // Display performers with metadata if valid, else just the band title.
759
        '{{#if ticket.band}}' +
760
        '{{#each ticket.band}} <span class="instrumentTextIcon">{{ @key }}</span>' +
761
        '{{#each this}}' +
762
        '<span class="performer performerDoneCount{{songsDone}}" ' +
763
        'data-performer-id="{{performerId}}" data-performer-name="{{performerName}}"> {{performerName}} ' +
764
        ' (<span class="songsDone">{{songsDone}}</span>/<span class="songsTotal">{{songsTotal}}</span>)' +
765
        '</span>' +
766
        '{{/each}}' +
767
        '{{/each}}' +
768
        '{{/if}}' +
769
770
        '{{#if ticket.title}}' +
771
        '<span class="ticketTitleIcon"><span class="instrumentTextIcon">Title</span> {{ ticket.title }}</span>' +
772
        '{{/if}}' +
773
774
        '{{#if ticket.song}}<br /><span class="fa fa-music"></span> {{ticket.song.artist}}: ' +
775
        '{{ticket.song.title}}' +
776
        ' ({{gameList ticket.song}})' +
777
        '{{/if}}' +
778
        '</div>' +
779
        '</div>'
780
      );
781
782
      this.upcomingTicketTemplate = Handlebars.compile(
783
        '<div class="ticket well ' +
784
        (this.displayOptions.songInPreview ? 'withSong' : 'noSong') +
785
        ' ' +
786
        (this.displayOptions.title ? 'withTitle' : 'noTitle') + // TODO is this used (correctly)?
787
        '" data-ticket-id="{{ ticket.id }}">' +
788
789
        (this.displayOptions.adminQueueHasControls && this.displayOptions.isAdmin ?
790
          '<div class="ticketAdminControls">' +
791
          '<button class="btn btn-sm btn-primary performingButton"' +
792
          ' data-ticket-id="{{ ticket.id }}">Performing</button>' +
793
          '<button class="btn btn-sm btn-danger removeButton" data-ticket-id="{{ ticket.id }}">Remove</button>' +
794
          '</div>'
795
          : '') +
796
797
798
        '<div class="ticketMeta">' +
799
        '<div class="blocking">' +
800
        '{{#if ticket.blocking}}<span class="fa fa-hand-stop-o" title="Blocking" />{{/if}}' +
801
        '</div>' +
802
        '<div class="private">' +
803
        '{{#if ticket.private}}<span class="fa fa-eye-slash" title="Private" />{{/if}}' +
804
        '</div>' +
805
        '</div>' +
806
807
        '  <div class="ticket-inner">' +
808
        '    <p class="text-center band auto-font">{{ticket.title}}</p>' +
809
        '    <p class="performers auto-font" data-fixed-assetwidth="200">' +
810
        '{{#each ticket.band}}' +
811
        '<span class="instrumentTag">{{instrumentIcon @key}}</span>' +
812
        '<span class="instrumentPerformers">{{#commalist this}}{{v.performerName}}{{/commalist}}</span>' +
813
        '{{/each}}' +
814
        '    </p>' +
815
        (this.displayOptions.songInPreview ?
816
          '{{#if ticket.song}}<p class="text-center song auto-font">' +
817
          '{{ticket.song.artist}}: {{ticket.song.title}}' +
818
          ' ({{gameList ticket.song}})' +
819
          '</p>{{/if}}' : '') +
820
        '        </div>' +
821
        '</div>  '
822
      );
823
824
      this.songAutocompleteItemTemplate = Handlebars.compile(
825
        '<div class="acSong" data-song-id="{{ song.id }}">' +
826
        '        <div class="acSong-inner {{#if song.queued}}queued{{/if}}">' +
827
        '        {{song.artist}}: {{song.title}} ({{gameList song}}) ' +
828
        '        </div>' +
829
        '</div>  '
830
      );
831
832
      this.editTicketTemplate = Handlebars.compile(
833
        '<div class="editTicket well">' +
834
        '<div class="pull-right editTicketButtons">' +
835
        '<button class="blockingButton btn btn-warning toggleButton">' +
836
        '<span class="fa fa-hand-stop-o" /> Blocking ' +
837
        ' <input type="checkbox" class="blockingCheckbox" ' +
838
        '  {{#if ticket}}{{# if ticket.blocking }}checked="checked"{{/if}}{{/if}} /></button>' +
839
        '<button class="privacyButton btn btn-warning toggleButton">' +
840
        '<span class="fa fa-eye-slash" /> Private ' +
841
        ' <input type="checkbox" class="privateCheckbox" ' +
842
        '  {{#if ticket}}{{# if ticket.private }}checked="checked"{{/if}}{{/if}} /></button>' +
843
        '<button class="editTicketButton btn btn-success">' +
844
        '<span class="fa fa-save" /> Save</button>' +
845
        '<button class="cancelTicketButton btn">' +
846
        '<span class="fa fa-close" /> Cancel</button>' +
847
        '</div>' +
848
849
        '{{# if ticket}}' +
850
        '<h3 class="editTicketHeader">Edit ticket <span class="fa fa-ticket"></span> {{ticket.id}}</h3>' +
851
        '{{else}}<h3 class="newTicketHeader">Add new ticket</h3>{{/if}}' +
852
853
        '<div class="editTicketInner">' +
854
        '<div class="editTicketSong">' +
855
        '<div class="ticketAspectSummary"><span class="fa fa-music fa-2x" title="Song"></span> ' +
856
        '<input type="hidden" class="selectedSongId"/> ' +
857
        '<span class="selectedSong">{{#if ticket}}{{#if ticket.song}}{{ticket.song.artist}}: ' +
858
        '{{ticket.song.title}}{{/if}}{{/if}}</span>' +
859
860
        '<button title="Remove song from ticket" ' +
861
        'class="btn removeSongButton{{#unless ticket}}{{#unless ticket.song}} hidden{{/unless}}{{/unless}}">' +
862
        ' <span class="fa fa-ban" />' +
863
        '</button>' +
864
865
        '</div>' +
866
        '<div class="input-group input-group">' +
867
        '<span class="input-group-addon" id="search-addon1"><span class="fa fa-search"></span> </span>' +
868
        '<input class="addSongTitle form-control" placeholder="Search song or use code"/>' +
869
        '</div>' +
870
871
        '<div class="songCompleteOuter">' +
872
        '<div class="songComplete"></div>' +
873
        '</div>' + // /songCompleteOuter
874
        '</div>' + // /editTicketSong
875
876
        '<div class="editTicketBandColumn">' +
877
878
        '<div class="ticketAspectSummary"><span class="fa fa-group fa-2x pull-left" title="Performers"></span>' +
879
        '<span class="selectedBand">{{#if ticket}}{{ticket.title}}{{/if}}</span>' +
880
        '</div>' + // /ticketAspectSummary
881
882
        '<div class="input-group">' +
883
        '<span class="input-group-addon" id="group-addon-band"><span class="fa fa-pencil"></span> </span>' +
884
        '<input class="editTicketTitle form-control" placeholder="Band name or message (optional)"' +
885
        ' value="{{#if ticket}}{{ticket.title}}{{/if}}"/>' +
886
        '</div>' + // /input-group
887
888
        '<div class="bandControls">' +
889
        '<div class="bandTabsOuter">' +
890
        '<div class="instruments">' +
891
        ' <div class="instrument instrumentVocals instrumentSelected" data-instrument-shortcode="V">' +
892
        '  <div class="instrumentName">Vocals</div>' +
893
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
894
        ' </div>' +
895
        ' <div class="instrument instrumentGuitar" data-instrument-shortcode="G">' +
896
        '  <div class="instrumentName">Guitar</div>' +
897
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
898
        ' </div>' +
899
        ' <div class="instrument instrumentBass" data-instrument-shortcode="B">' +
900
        '  <div class="instrumentName">Bass</div>' +
901
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
902
        ' </div>' +
903
        ' <div class="instrument instrumentDrums" data-instrument-shortcode="D">' +
904
        '  <div class="instrumentName">Drums</div>' +
905
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
906
        ' </div>' +
907
        ' <div class="instrument instrumentKeys instrumentUnused" data-instrument-shortcode="K">' +
908
        '  <div class="instrumentName">Keyboard</div>' +
909
        '  <div class="instrumentPerformer"><i>Needed</i></div>' +
910
        ' </div>' +
911
        '</div>' + // /instruments
912
        '<div class="performerSelect">' +
913
        '<div class="input-group input-group">' +
914
        '<span class="input-group-addon" id="group-addon-performer"><span class="fa fa-plus"></span> </span>' +
915
        '<input class="newPerformer form-control" placeholder="New performer (Firstname Initial)"/>' +
916
        '</div>' +
917
918
        '<div class="performers"></div>' +
919
        '</div>' + // /performerSelect
920
        '</div>' + // /bandTabsOuter
921
        '</div>' + // /bandControls
922
        '</div>' + // /editTicketBandColumn
923
        '<div class="clearfix"></div>' + // Clear after editTicketBandColumn
924
        '</div>' + // /editTicketInner
925
        '</div>' // /editTicket
926
      );
927
928
      this.songDetailsTemplate = Handlebars.compile(
929
        '<div class="songDetails"><h3>{{song.artist}}: {{song.title}}</h3>' +
930
        '<table>' +
931
        '<tr><th>Duration</th><td>{{durationToMS song.duration}}</td></tr> ' +
932
        '<tr><th>Code</th><td>{{song.codeNumber}}</td></tr> ' +
933
        '<tr><th>Instruments </th><td>{{commalist song.instruments}}</td></tr> ' +
934
        '<tr><th>Games</th><td>{{commalist song.platforms}}</td></tr> ' +
935
        '<tr><th>Source</th><td>{{song.source}}</td></tr> ' +
936
        '</table>' +
937
        '</div>'
938
      );
939
940
    },
941
942
    ticketOrderChanged: function() {
943
      var that = this;
944
      var idOrder = [];
945
      $('#target').find('.ticket').each(
946
        function() {
947
          var ticketBlock = $(this);
948
          var ticketId = ticketBlock.data('ticketId');
949
          idOrder.push(ticketId);
950
        }
951
      );
952
953
      that.showAppMessage('Updating ticket order');
954
      $.ajax({
955
        method: 'POST',
956
        data: {
957
          idOrder: idOrder
958
        },
959
        url: '/api/newOrder',
960
        success: function(data, status) {
0 ignored issues
show
Unused Code introduced by
The parameter data is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
Unused Code introduced by
The parameter status is not used and could be removed.

This check looks for parameters in functions that are not used in the function body and are not followed by other parameters which are used inside the function.

Loading history...
961
          // FIXME check return status
962
          that.showAppMessage('Saved revised order', 'success');
963
        },
964
        error: function(xhr, status, error) {
965
          var message = 'Failed to save revised order';
966
          that.reportAjaxError(message, xhr, status, error);
967
        }
968
      });
969
970
      this.updatePerformanceStats();
971
    },
972
973
    performButtonCallback: function(button) {
974
      var that = this;
975
976
      button = $(button);
977
      var ticketId = button.data('ticketId');
978
      that.showAppMessage('Mark ticket used');
979
      $.ajax({
980
          method: 'POST',
981
          data: {
982
            ticketId: ticketId
983
          },
984
          url: '/api/useTicket',
985
          success: function(data, status) {
986
            that.showAppMessage('Marked ticket used', 'success');
987
            void(data);
988
            void(status);
989
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
990
            ticketBlock.addClass('used');
991
            // TicketBlock.append(' (done)');
992
993
            // Fixme receive updated ticket info from API
994
            var ticket = ticketBlock.data('ticket');
995
            ticket.startTime = Date.now() / 1000;
996
            ticket.used = true;
997
            ticketBlock.data('ticket', ticket);
998
999
            that.updatePerformanceStats();
1000
          },
1001
          error: function(xhr, status, error) {
1002
            var message = 'Failed to mark ticket used';
1003
            that.reportAjaxError(message, xhr, status, error);
1004
          }
1005
        }
1006
      );
1007
    },
1008
1009
    removeButtonCallback: function(button) {
1010
      var that = this;
1011
      button = $(button);
1012
      var ticketId = button.data('ticketId');
1013
      that.showAppMessage('Deleting ticket');
1014
      $.ajax({
1015
          method: 'POST',
1016
          data: {
1017
            ticketId: ticketId
1018
          },
1019
          url: '/api/deleteTicket',
1020
          success: function(data, status) {
1021
            that.showAppMessage('Deleted ticket', 'success');
1022
            void(status);
1023
            var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1024
            ticketBlock.remove();
1025
            that.updatePerformanceStats();
1026
          },
1027
          error: function(xhr, status, error) {
1028
            var message = 'Failed to deleted ticket';
1029
            that.reportAjaxError(message, xhr, status, error);
1030
          }
1031
        }
1032
      );
1033
    },
1034
1035
    editButtonCallback: function(button) {
1036
      var that = this;
1037
      button = $(button);
1038
      var ticketId = button.data('ticketId');
1039
1040
      var ticketBlock = $('.ticket[data-ticket-id="' + ticketId + '"]');
1041
      var ticket = ticketBlock.data('ticket'); // TODO possibly load from ajax instead?
1042
      that.resetEditTicketBlock(ticket);
1043
    },
1044
1045
    enableAcSongSelector: function(outerElement, songClickHandler) {
1046
      var that = this;
1047
      outerElement.find('.acSong').click(
1048
        function() {
1049
          // Find & decorate clicked element
1050
          outerElement.find('.acSong').removeClass('selected');
1051
          $(this).addClass('selected');
1052
1053
          var song = $(this).data('song');
1054
          songClickHandler.call(that, song); // Run in 'that' context
1055
        }
1056
      );
1057
    },
1058
1059
1060
    searchPageSongSelectionClick: function(song) {
1061
      var target = $('#searchTarget');
1062
      target.html(this.songDetailsTemplate({song: song}));
1063
    },
1064
1065
    updatePerformanceStats: function() {
1066
      var that = this;
1067
      var performed = {};
1068
      var lastByPerformer = {};
1069
      var ticketOrdinal = 1;
1070
      var ticketTime = null;
1071
1072
      var pad = function(number) {
1073
        if (number < 10) {
1074
          return '0' + number;
1075
        }
1076
        return number;
1077
      };
1078
1079
      // First check number of songs performed before this one
1080
      var sortContainer = $('.sortContainer');
1081
      var lastSongDuration = null;
1082
      var lastTicketNoSong = true;
1083
1084
      var nthUnused = 1;
1085
1086
      sortContainer.find('.ticket').each(function() {
1087
        var realTime;
1088
        var ticketId = $(this).data('ticket-id');
1089
        var ticketData = $(this).data('ticket');
1090
1091
        if (ticketData.startTime) {
1092
          realTime = new Date(ticketData.startTime * 1000);
1093
        }
1094
1095
        $(this).removeClass('shown');
1096
1097
        if (!(ticketData.used || ticketData.private)) {
1098
          if (nthUnused <= that.displayOptions.upcomingCount) {
1099
            $(this).addClass('shown');
1100
          }
1101
          nthUnused++;
1102
        }
1103
1104
        $(this).find('.ticketOrdinal').text('# ' + ticketOrdinal);
1105
        // Fixme read ticketStart from data if present
1106
        if (realTime) {
1107
          ticketTime = realTime;
1108
        } else if (ticketTime) {
1109
          // If last song had an implicit time, add defaultSongOffsetMs to it and assume next song starts then
1110
          // If this is in the past, assume it starts now!
1111
          var songOffsetMs;
1112
          if (lastTicketNoSong) {
1113
            songOffsetMs = that.defaultSongIntervalSeconds * 1000;
1114
            // Could just be a message, could be a reset / announcement, so treat as an interval only
1115
          } else if (lastSongDuration) {
1116
            songOffsetMs = (that.defaultSongIntervalSeconds + lastSongDuration) * 1000;
1117
          } else {
1118
            songOffsetMs = (that.defaultSongIntervalSeconds + that.defaultSongLengthSeconds) * 1000;
1119
          }
1120
          ticketTime = new Date(Math.max(ticketTime.getTime() + songOffsetMs, Date.now()));
1121
        } else {
1122
          ticketTime = new Date();
1123
        }
1124
        $(this).find('.ticketTime').text(pad(ticketTime.getHours()) + ':' + pad(ticketTime.getMinutes()));
1125
1126
        // Update performer stats (done/total)
1127
        $(this).find('.performer').each(function() {
1128
          var performerId = $(this).data('performer-id');
1129
          var performerName = $(this).data('performer-name');
1130
          if (!performed.hasOwnProperty(performerId)) {
1131
            performed[performerId] = 0;
1132
          }
1133
          $(this).find('.songsDone').text(performed[performerId]);
1134
1135
          $(this).removeClass(
1136
            function(i, oldClass) {
1137
              void(i);
1138
              var classes = oldClass.split(' ');
1139
              var toRemove = [];
1140
              for (var cIdx = 0; cIdx < classes.length; cIdx++) {
1141
                if (classes[cIdx].match(/^performerDoneCount/)) {
1142
                  toRemove.push(classes[cIdx]);
1143
                }
1144
              }
1145
              return toRemove.join(' ');
1146
            }
1147
          ).addClass('performerDoneCount' + performed[performerId]);
1148
          performed[performerId]++;
1149
1150
          // Now check proximity of last song by this performer
1151
          if (lastByPerformer.hasOwnProperty(performerId)) {
1152
            var distance = ticketOrdinal - lastByPerformer[performerId].idx;
1153
            $(this).removeClass('proximityIssue');
1154
            $(this).removeClass('proximityIssue1');
1155
            if ((distance < 3) && (performerName.charAt(0) !== '?')) {
1156
              $(this).addClass('proximityIssue');
1157
              if (distance === 1) {
1158
                $(this).addClass('proximityIssue1');
1159
              }
1160
            }
1161
          } else {
1162
            // Make sure they've not got a proximity marker on a ticket that's been dragged to top
1163
            $(this).removeClass('proximityIssue');
1164
          }
1165
          lastByPerformer[performerId] = {idx: ticketOrdinal, ticketId: ticketId};
1166
        });
1167
        ticketOrdinal++;
1168
1169
        if (ticketData.song) {
1170
          lastSongDuration = ticketData.song.duration;
1171
          lastTicketNoSong = false;
1172
        } else {
1173
          lastSongDuration = 0;
1174
          lastTicketNoSong = true;
1175
        } // Set non-song ticket to minimum duration
1176
      });
1177
1178
      // Then update all totals
1179
      sortContainer.find('.performer').each(function() {
1180
        var performerId = $(this).data('performer-id');
1181
        var totalPerformed = performed[performerId];
1182
        $(this).find('.songsTotal').text(totalPerformed);
1183
      });
1184
    },
1185
1186
    /**
1187
     * Show a message in the defined appMessageTarget (f any)
1188
     *
1189
     * @param message {string} Message to show (replaces any other)
1190
     * @param [className='info'] {string} 'info','success','warning','danger'
0 ignored issues
show
Documentation Bug introduced by
The parameter className='info' does not exist. Did you maybe mean className instead?
Loading history...
1191
     */
1192
    showAppMessage: function(message, className) {
1193
      var that = this;
1194
      if (this.messageTimer) {
1195
        clearTimeout(this.messageTimer);
1196
      }
1197
1198
      this.messageTimer = setTimeout(function() {
1199
        that.appMessageTarget.html('');
1200
      }, 5000);
1201
1202
      if (!className) {
1203
        className = 'info';
1204
      }
1205
      if (this.appMessageTarget) {
1206
        var block = $('<div />').addClass('alert alert-' + className);
1207
        block.text(message);
1208
        this.appMessageTarget.html('').append(block);
1209
      }
1210
    },
1211
1212
    ucFirst: function(string) {
1213
      return string.charAt(0).toUpperCase() + string.slice(1);
1214
    },
1215
1216
    reportAjaxError: function(message, xhr, status, error) {
1217
      this.showAppMessage(
1218
        this.ucFirst(status) + ': ' + message + ': ' + error + ', ' + xhr.responseJSON.error,
1219
        'danger'
1220
      );
1221
    },
1222
1223
    checkRemoteRedirect: function() {
1224
      window.setInterval(function() {
1225
          $.get('/api/remotesRedirect', function(newPath) {
1226
            if (newPath && (newPath !== window.location.pathname)) {
1227
              window.location.pathname = newPath;
1228
            }
1229
          });
1230
        },
1231
        10000);
1232
    }
1233
  };
1234
}());