Completed
Push — master ( 483e11...61b75c )
by Jeff
03:42
created

web/js/frontend.js   C

Complexity

Total Complexity 54
Complexity/F 2.57

Size

Lines of Code 257
Function Count 21

Duplication

Duplicated Lines 0
Ratio 0 %

Importance

Changes 13
Bugs 1 Features 0
Metric Value
wmc 54
c 13
b 1
f 0
dl 0
loc 257
rs 6.8539
cc 0
nc 1024
mnd 3
bc 50
fnc 21
bpm 2.3809
cpm 2.5714
noi 2

12 Functions

Rating   Name   Duplication   Size   Complexity  
D Field.pickNext 0 43 10
C Screen.reload 0 22 10
A frontend.js ➔ onLoad 0 18 1
A Screen.displaysData 0 5 1
A Field.setError 0 3 1
A Screen.checkUpdates 0 20 1
A frontend.js ➔ Field 0 14 1
A Field.randomizeSortContents 0 8 1
A Screen.doReload 0 7 2
A Field.getContents 0 19 2
B Field.display 0 25 5
A frontend.js ➔ Content 0 7 1

How to fix   Complexity   

Complexity

Complex classes like web/js/frontend.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
 * Screen class constructor
3
 * @param {string} updateScreenUrl global screen update checks url
4
 */
5
function Screen(updateScreenUrl) {
6
  this.fields = [];
7
  this.url = updateScreenUrl;
8
  this.lastChanges = null;
9
  this.endAt = null;
10
  this.nextUrl = null;
11
  this.stopping = false;
12
}
13
14
/**
15
 * Ajax GET on updateScreenUrl to check lastChanges timestamp and reload if necessary
16
 */
17
Screen.prototype.checkUpdates = function() {
18
  var s = this;
19
  $.get(this.url, function(j) {
20
    if (j.success) {
21
      if (s.lastChanges == null) {
22
        s.lastChanges = j.data.lastChanges;
23
      } else if (s.lastChanges != j.data.lastChanges) {
24
        s.reload();
25
        s.nextUrl = null;
26
        return;
27
      }
28
29
      if (j.data.duration > 0) {
30
        // Setup next screen
31
        s.reload(j.data.duration * 1000);
32
        s.nextUrl = j.data.nextScreenUrl;
33
      }
34
    }
35
  });
36
}
37
38
/**
39
 * Start Screen reload procedure, checking for every field timeout
40
 */
41
Screen.prototype.reload = function(minDuration) {
42
  var endAt = Date.now() + (minDuration ? minDuration : 0);
43
  if (this.stopping && this.endAt < endAt) {
44
    return;
45
  }
46
47
  this.endAt = minDuration ? Date.now() + minDuration : 0;
48
  this.stopping = true;
49
  for (var i in this.fields) {
50
    if (!this.fields.hasOwnProperty(i)) {
51
      continue;
52
    }
53
    var f = this.fields[i];
54
    if (f.timeout && f.endAt > this.endAt) {
55
      this.endAt = f.endAt;
56
    }
57
  }
58
59
  if (this.endAt === 0) {
60
    this.doReload();
61
  }
62
}
63
64
/**
65
 * Actual Screen reload action
66
 */
67
Screen.prototype.doReload = function() {
68
  if (this.nextUrl) {
69
    window.location = this.nextUrl;
70
  } else {
71
    window.location.reload();
72
  }
73
}
74
75
/**
76
 * Check every field for content
77
 * @param  {Content} data 
78
 * @return {boolean} content is displayed
79
 */
80
Screen.prototype.displaysData = function(data) {
81
  return this.fields.filter(function(field) {
82
    return field.current && field.current.data == data;
83
  }).length > 0;
84
}
85
86
/**
87
 * Content class constructor
88
 * @param {array} c content attributes
89
 */
90
function Content(c) {
91
  this.id = c.id;
92
  this.data = c.data;
93
  this.duration = c.duration * 1000;
94
  this.type = c.type;
95
  this.displayCount = 0;
96
}
97
98
/**
99
 * Field class constructor
100
 * @param {jQuery.Object} $f field object
101
 * @param {Screen} screen parent screen object
102
 */
103
function Field($f, screen) {
104
  this.$field = $f;
105
  this.id = $f.attr('data-id');
106
  this.url = $f.attr('data-url');
107
  this.types = $f.attr('data-types').split(' ');
108
  this.canUpdate = this.url != null;
109
  this.contents = [];
110
  this.previous = null;
111
  this.current = null;
112
  this.next = null;
113
  this.timeout = null;
114
  this.endAt = null;
115
  this.screen = screen;
116
}
117
118
/**
119
 * Retrieves contents from backend for this field
120
 */
121
Field.prototype.getContents = function() {
122
  if (!this.canUpdate) {
123
    return;
124
  }
125
126
  var f = this;
127
  $.get(this.url, function(j) {
128
    if (j.success) {
129
      f.contents = j.next.map(function(c) {
130
        return new Content(c);
131
      });
132
      if (!f.timeout && f.contents.length) {
133
        f.pickNext();
134
      }
135
    } else {
136
      f.setError(j.message || 'Error');
137
    }
138
  });
139
}
140
141
/**
142
 * Display error in field text
143
 */
144
Field.prototype.setError = function(err) {
145
  this.$field.text(err);
146
}
147
148
/**
149
 * Sort by displayCount and randomize order when equal displayCount
150
 */
151
Field.prototype.randomizeSortContents = function() {
152
  this.contents = this.contents.sort(function(a, b) {
153
    if (a.displayCount === b.displayCount) {
154
      return Math.random() - 0.5;
155
    }
156
    return a.displayCount - b.displayCount;
157
  });
158
}
159
160
/**
161
 * Loop through field contents to pick next displayable content
162
 */
163
Field.prototype.pickNext = function() {
164
  if (this.screen.stopping) { // Stoping screen
165
    if (this.screen.endAt < Date.now()) {
166
      this.screen.doReload();
167
      return;
168
    }
169
  }
170
171
  this.previous = this.current;
172
  this.current = null;
173
  var pData = this.previous && this.previous.data;
174
  // Avoid repeat & other field same content
175
  this.randomizeSortContents();
176
  for (var i = 0; i < this.contents.length; i++) {
177
    var c = this.contents[i];
178
    // Skip too long content
179
    if (this.screen.endAt != null && c.duration + Date.now() > this.screen.endAt) {
180
      continue;
181
    }
182
183
    if (c.data == pData) {
184
      // Will repeat, avoid if enough content
185
      if (this.contents.length < 2) {
186
        this.next = c;
187
        break;
188
      }
189
      continue;
190
    }
191
192
    if (this.screen.displaysData(c.data)) {
193
      // Same content already displayed on other field, avoid if enough content
194
      if (this.contents.length < 3) {
195
        this.next = c;
196
        break;
197
      }
198
      continue;
199
    }
200
201
    this.next = c;
202
  }
203
204
  this.display();
205
}
206
207
/**
208
 * Display next content in field html
209
 */
210
Field.prototype.display = function() {
211
  if (this.next && this.next.duration > 0) {
212
    this.current = this.next
213
    this.current.displayCount++;
214
    this.next = null;
215
    this.$field.html(this.current.data);
216
    this.$field.show();
217
    if (this.$field.text() != '') {
218
      this.$field.textfill({
219
        maxFontPixels: 0,
220
      });
221
    }
222
    if (this.timeout) {
223
      clearTimeout(this.timeout);
224
    }
225
    var f = this;
226
    this.timeout = setTimeout(function() {
227
      f.pickNext();
228
    }, this.current.duration);
229
    this.endAt = this.current.duration + Date.now()
230
  } else {
231
    this.timeout = null;
232
    console.error('No content to display for', this);
233
  }
234
}
235
236
/**
237
 * jQuery.load event
238
 * Initialize Screen and Fields
239
 * Setup updates interval timeouts
240
 */
241
function onLoad() {
242
  var screen = new Screen(updateScreenUrl);
0 ignored issues
show
Bug introduced by
The variable updateScreenUrl seems to be never declared. If this is a global, consider adding a /** global: updateScreenUrl */ comment.

This checks looks for references to variables that have not been declared. This is most likey a typographical error or a variable has been renamed.

To learn more about declaring variables in Javascript, see the MDN.

Loading history...
243
  // Init
244
  $('.field').each(function() {
245
    var f = new Field($(this), screen);
246
    f.getContents();
247
    screen.fields.push(f);
248
  });
249
250
  // Setup content updates loop
251
  setInterval(function() {
252
    for (var f in screen.fields) {
0 ignored issues
show
Complexity introduced by
A for in loop automatically includes the property of any prototype object, consider checking the key using hasOwnProperty.

When iterating over the keys of an object, this includes not only the keys of the object, but also keys contained in the prototype of that object. It is generally a best practice to check for these keys specifically:

var someObject;
for (var key in someObject) {
    if ( ! someObject.hasOwnProperty(key)) {
        continue; // Skip keys from the prototype.
    }

    doSomethingWith(key);
}
Loading history...
253
      screen.fields[f].getContents();
254
    }
255
    screen.checkUpdates();
256
  }, 60000);
257
  screen.checkUpdates();
258
}
259
260
// Run
261
$(onLoad);
262