stormfront-rails 0.2.3 → 0.10.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,5 @@
1
1
  var stormfront = { };
2
- var Stormfront = { Mixin: { } };
2
+ var Stormfront = { };
3
3
 
4
4
  Stormfront.VERSION = '1.0.0';
5
5
 
@@ -8,36 +8,49 @@ Stormfront.Class = function(options) {
8
8
  this.initialize.apply(this, arguments);
9
9
  };
10
10
 
11
- Stormfront.Class.extend = function(protoProps, staticProps) {
12
- var parent = this;
13
- var child;
14
-
15
- if (protoProps && _.has(protoProps, 'constructor'))
16
- child = protoProps.constructor;
17
- else
18
- child = function(){ return parent.apply(this, arguments); };
19
-
20
- _.extend(child, parent, staticProps);
21
-
22
- var Surrogate = function(){ this.constructor = child; };
23
- Surrogate.prototype = parent.prototype;
24
- child.prototype = new Surrogate;
25
-
26
- if (protoProps) _.extend(child.prototype, protoProps);
27
- child.__super__ = parent.prototype;
11
+ Stormfront.Class.extend = function(protoProps) {
12
+ function copyConstructor(base){
13
+ return function(){ return base.apply(this, arguments); };
14
+ }
15
+ function copyBaseProperties(child, parent){
16
+ _.extend(child, parent);
17
+ }
18
+ function carryPrototypeChain(child, parent){
19
+ var Surrogate = function(){ this.constructor = child; };
20
+ Surrogate.prototype = parent.prototype;
21
+ child.prototype = new Surrogate;
22
+ }
23
+ function copyNewProperties(protoProps){
24
+ _.extend(child.prototype, protoProps);
25
+ }
26
+ function enableAccessToBaseClass(){
27
+ child.__super__ = parent.prototype;
28
+ }
28
29
 
30
+ var parent = this;
31
+ var child = copyConstructor(parent);
32
+ copyBaseProperties(child, parent);
33
+ carryPrototypeChain(child, parent);
34
+ copyNewProperties(protoProps);
35
+ enableAccessToBaseClass(child, parent);
29
36
  return child;
30
37
  };
31
38
 
32
-
33
39
  stormfront.arguments = function(args){
34
40
  return new Stormfront.Arguments(args);
35
41
  };
36
42
 
37
43
  Stormfront.Arguments = Stormfront.Class.extend({
38
44
  initialize: function(array){
45
+ function copy(array) {
46
+ var copy = new Array(array.length);
47
+ for(var i = 0; i < array.length; ++i) {
48
+ copy[i] = array[i];
49
+ }
50
+ return copy;
51
+ }
39
52
  if (array)
40
- this.array = [].slice.call(array);
53
+ this.array = copy(array);
41
54
  else
42
55
  this.array = [];
43
56
  },
@@ -57,11 +70,14 @@ Stormfront.Arguments = Stormfront.Class.extend({
57
70
  return this.array;
58
71
  }
59
72
  });
60
-
61
73
  stormfront.hash = function(hash){
62
74
  return new Stormfront.Hash(hash);
63
75
  };
64
76
 
77
+ Stormfront.Unchanged = {
78
+ unchanged: 'true'
79
+ };
80
+
65
81
  Stormfront.Hash = Stormfront.Class.extend({
66
82
  initialize: function(hash){
67
83
  this.hash = hash;
@@ -79,9 +95,68 @@ Stormfront.Hash = Stormfront.Class.extend({
79
95
  },
80
96
  subtract: function(values){
81
97
  return _.omit(this.hash, _.keys(values));
98
+ },
99
+ diff: function(newProperties){
100
+ function getProperties(oldProperties, newProperties){
101
+ var candidates = _.keys(newProperties).concat(_.keys(oldProperties));
102
+ var position = candidates.length;
103
+ var keys = [];
104
+
105
+ while (position--) {
106
+ var candidate = candidates[position];
107
+ if (keys.indexOf(candidate) === -1)
108
+ keys.unshift(candidate);
109
+ }
110
+ return keys;
111
+ }
112
+ function evaluateArray(previous, current) {
113
+ if (current.length !== previous.length)
114
+ return current;
115
+
116
+ var difference = _.any(previous, function(value, index){
117
+ return evaluateObject(value, current[index]);
118
+ });
119
+ return difference ? current : Stormfront.Unchanged;
120
+ }
121
+
122
+ function evaluateValue(previous, current){
123
+ if (_.isArray(current))
124
+ return evaluateArray(previous, current);
125
+ else if (_.isObject(current))
126
+ return evaluateObject(previous, current);
127
+ else if (previous !== current)
128
+ return current;
129
+ return Stormfront.Unchanged;
130
+ }
131
+
132
+ function evaluateObject(oldProperties, newProperties) {
133
+ var changes = { };
134
+ function addEntry(property, value) {
135
+ if (value)
136
+ changes[property] = value;
137
+ }
138
+
139
+ var properties = getProperties(oldProperties, newProperties);
140
+ _.each(properties, function(property){
141
+ var previous = oldProperties[property];
142
+ var current = newProperties[property];
143
+ if (previous && current)
144
+ addEntry(property, evaluateValue(previous, current));
145
+ else if (previous)
146
+ addEntry(property, previous);
147
+ else
148
+ addEntry(property, current);
149
+ });
150
+ return _.isEmpty(changes) ? Stormfront.Unchanged : changes;
151
+ }
152
+ if (!this.hash)
153
+ return newProperties;
154
+ if (!newProperties)
155
+ return this.hash;
156
+
157
+ return evaluateObject(this.hash, newProperties);
82
158
  }
83
159
  });
84
-
85
160
  stormfront.number = function(number){
86
161
  return new Stormfront.Number(number);
87
162
  };
@@ -97,7 +172,6 @@ Stormfront.Number = Stormfront.Class.extend({
97
172
  return this.number < value ? -1 : this.number > value ? 1 : 0;
98
173
  }
99
174
  });
100
-
101
175
  Stormfront.Response = Stormfront.Class.extend({
102
176
  initialize: function(xhr){
103
177
  this.xhr = xhr;
@@ -129,7 +203,6 @@ Stormfront.Response = Stormfront.Class.extend({
129
203
  }
130
204
  }
131
205
  });
132
-
133
206
  stormfront.string = function(text){
134
207
  return new Stormfront.String(text);
135
208
  };
@@ -138,23 +211,6 @@ Stormfront.String = Stormfront.Class.extend({
138
211
  initialize: function(text){
139
212
  this.text = text;
140
213
  },
141
- title: function(){
142
- var sentence = _.str.capitalize(_.str.camelize(this.text)).split(/(?=[A-Z\d])/);
143
- var displaySentence = "";
144
- function processCharacter(character, i){
145
- function whenPreviousWordIsNotAnAcronym() {
146
- return i > 0 && sentence[i - 1].length > 1;
147
- }
148
- function whenPreviousWordISAnAcronymAndCurrentWordIsNotAnAcronym() {
149
- return i > 0 && sentence[i - 1].length == 1 && sentence[i].length > 1;
150
- }
151
- if (whenPreviousWordIsNotAnAcronym() || whenPreviousWordISAnAcronymAndCurrentWordIsNotAnAcronym())
152
- displaySentence += " ";
153
- displaySentence += character;
154
- }
155
- _.each(sentence, processCharacter);
156
- return displaySentence;
157
- },
158
214
  decimal: function(places, placeholder){
159
215
  placeholder = placeholder || '';
160
216
  places = places || 3;
@@ -164,17 +220,22 @@ Stormfront.String = Stormfront.Class.extend({
164
220
  scientific: function(){
165
221
  return this.text === null || $.trim(this.text) === '' ? '' : parseFloat(this.text).toExponential(3);
166
222
  },
167
- decimal_format: function(){
168
- return this.decimal(this.text);
169
- },
170
- scientific_notation: function(){
171
- return this.scientific(this.text);
172
- },
173
223
  firstWord: function(){
174
224
  return this.text ? this.text.split(' ')[0] : '';
225
+ },
226
+ title: function(){
227
+ return this.camelCase(' ', ' ');
228
+ },
229
+ camelCase: function(delimiter, alimiter){
230
+ delimiter = delimiter || ' ';
231
+ alimiter = alimiter || '';
232
+ function capitalize(word){
233
+ return word.charAt(0).toUpperCase() + word.substr(1).toLowerCase();
234
+ }
235
+ var words = this.text.split(delimiter);
236
+ return _.map(words, capitalize).join(alimiter)
175
237
  }
176
238
  });
177
-
178
239
  stormfront.time = function(datetime){
179
240
  return new Stormfront.Time(datetime);
180
241
  };
@@ -205,201 +266,456 @@ Stormfront.Time = Stormfront.Class.extend({
205
266
 
206
267
 
207
268
 
269
+ stormfront.type = function(name){
270
+ return (new Stormfront.Type(name)).get();
271
+ };
272
+ Stormfront.Type = Stormfront.Class.extend({
273
+ initialize: function (name) {
274
+ this.name = name;
275
+ },
276
+ get: function () {
277
+ var name = this.name.split('.');
278
+ return stormfront.hash(window).findNestedValue(name);
279
+ }
280
+ });
208
281
 
209
-
210
- Stormfront.Chaperone = Stormfront.Class.extend({
211
- initialize: function(){
212
- this.children = {};
213
- _.extend(this, Backbone.Events);
214
- this.throttle('create', 'created', Stormfront.Errors.INITIALIZING);
215
- this.throttle('render', 'rendered', Stormfront.Errors.RENDERING);
216
- this.throttle('attach', 'attached', Stormfront.Errors.ATTACHING);
217
- this.wrap('add', 'added', Stormfront.Errors.ADDING);
218
- this.wrap('remove', 'removed', Stormfront.Errors.CLOSING);
219
- },
220
-
221
- create: function(childClass, options, context){
222
- return new window[childClass](options.call(context));
223
- },
224
- find: function(identity){
225
- return this.children[identity];
226
- },
227
- add: function(child){
228
- var identity = child.getIdentity();
229
- this.remove(this.find(identity));
230
- this.children[identity] = child;
231
- return child;
232
- },
233
- remove: function(child){
234
- if (child)
235
- this.children = _.omit(this.children, child.getIdentity());
236
- return child;
237
- },
238
- render: function(child){
239
- return child.render();
240
- },
241
- attach: function(child, location, method){
242
- if (location.size() === 0)
243
- throw Stormfront.Errors.NO_LOCATION(child);
244
- else if (location.size() > 1)
245
- throw Stormfront.Errors.MULTIPLE_LOCATIONS(child);
246
-
247
- location[method](child.$el);
248
- return child;
249
- },
250
-
251
- wrap: function(field, event, error){
252
- var implementation = this[field];
253
- this[field] = function(){
254
- try {
255
- var result = implementation.apply(this, arguments);
256
- result && this.trigger(event, result);
257
- } catch (e) {
258
- this.trigger('error', error(arguments[0].toString()), e);
259
- }
282
+ Stormfront.Patterns = {
283
+ Dispatcher: function (subject, dispatcher) {
284
+ if (dispatcher) {
285
+ subject.dispatch = function (event) {
286
+ dispatcher.dispatch(event);
287
+ };
288
+ subject.getDispatcher = function(){
289
+ return dispatcher;
290
+ };
260
291
  }
261
292
  },
262
- throttle: function(field, event, error){
263
- this.wrap(field, event, error);
264
- var implementation = this[field];
265
- this[field] = function(){
266
- var self = this;
267
- var capturedArguments = arguments;
268
- function func(){
269
- implementation.apply(self, capturedArguments);
293
+ Events: function (subject) {
294
+ if (!subject.listenTo)
295
+ $.extend(subject, Backbone.Events);
296
+ $.extend(subject, {
297
+ forward: function (other, type) {
298
+ function propagate(){
299
+ subject.trigger.apply(subject, arguments);
300
+ }
301
+ subject.listenTo(other, type || 'all', propagate);
270
302
  }
271
- setTimeout(func, 5);
272
- }
303
+ })
304
+ }
305
+ };
306
+ Stormfront.Errors = {
307
+ FEEDBACK: 'An unexpected error was encountered'
308
+ };
309
+
310
+ Stormfront.Dispatches = {
311
+ RECEIVED: 'Dispatcher:Action:Received',
312
+ QUEUED: 'Dispatcher:Action:Queued',
313
+ READY: 'Dispatcher:Action:Ready',
314
+ EXECUTING: 'Store:Action:Executing',
315
+ EXECUTED: 'Store:Action:Executed',
316
+ MISSING: 'Store:Action:Missing',
317
+ ERROR: 'Store:Action:Error',
318
+ CHANGE: 'Store:Changed',
319
+ UNCHANGED: 'Store:Unchanged',
320
+ NO_RETURN: 'Store:NoReturnValue'
321
+ };
322
+
323
+ Stormfront.NO_CHANGE = 'Store:Nothing_Changed';
324
+ Stormfront.Reducer = Stormfront.Class.extend({
325
+ type: 'NOT:IMPLEMENTED',
326
+ dependencies: [],
327
+ initialize: function(dispatcher){
328
+ Stormfront.Patterns.Dispatcher(this, dispatcher);
273
329
  },
330
+ execute: function(store, event){
331
+ throw 'The reducer does not implement a function body'
332
+ }
333
+ });
334
+ Stormfront.Ajax = {
335
+ START: 'AJAX:START',
336
+ SUCCESS: 'AJAX:SUCCESS',
337
+ ERROR: 'AJAX:ERROR',
338
+ ABORTED: 'AJAX:ABORTED',
339
+ PENDING: 'AJAX:PENDING',
340
+ Support: {
341
+ generateKey: function (event) {
342
+ return event.type;
343
+ },
344
+ createGenericRequest: function (type) {
345
+ return function (info) {
346
+ return _.extend(info, {type: type});
347
+ }
348
+ },
349
+ createGenericReducer: function (type) {
350
+ return Stormfront.Ajax.RequestReducer.extend({
351
+ type: type,
352
+ execute: function (store, event) {
353
+ try {
354
+ event.callbacks[type].call(event.callbacks.context, store, event);
355
+ } catch (e) {
356
+ console.warn('Ajax Handler encountered an error', e);
357
+ }
358
+ return this.updateEntry(store, event, type);
359
+ }
360
+ });
361
+ }
362
+ }
363
+ };
274
364
 
275
- close: function(){
276
- _.each(this.children, this.remove, this);
365
+ Stormfront.Ajax.RequestReducer = Stormfront.Reducer.extend({
366
+ getKey: function(event){
367
+ return Stormfront.Ajax.Support.generateKey(event);
368
+ },
369
+ getEntries: function(store){
370
+ return store.ajax || (store.ajax = { });
371
+ },
372
+ getEntry: function(store, event){
373
+ var key = this.getKey(event);
374
+ var entries = this.getEntries(store);
375
+ return entries[key] || (entries[key] = { });
376
+ },
377
+ updateEntry: function(store, event, state){
378
+ var ajax = this.getEntry(store, event);
379
+ ajax.state = state;
380
+ ajax.request = event.xhr;
381
+ ajax.exception = event.exception;
382
+ return store;
383
+ },
384
+ dropEntry: function(store, event){
385
+ var key = this.getKey(event);
386
+ var entries = this.getEntries(store);
387
+ delete entries[key];
277
388
  }
278
389
  });
279
390
 
280
- Stormfront.Template = Stormfront.Class.extend({
281
- initialize: function(view){
282
- this.view = view;
283
- },
284
- render: function(){
285
- function renderTemplate(view){
286
- function getTemplate(view){
287
- var selector = view.getIdentity();
288
- return $('#' + selector + '_template');
289
- }
290
- function compileTemplate(selection){
291
- return Handlebars.compile(selection.html());
292
- }
293
- function expandTemplate(template){
294
- return template(view.getViewModel());
295
- }
296
- return expandTemplate(compileTemplate(getTemplate(view)))
297
- }
298
- function findError(view){
299
- function getTemplate(view){
300
- if (!view.className)
301
- throw Stormfront.Errors.NO_SELECTOR();
302
- var selector = view.getIdentity();
303
- return $('#' + selector + '_template');
304
- }
305
- function compileTemplate(template){
306
- if (template.size() === 1)
307
- return template;
308
- if (template.size() === 0)
309
- throw Stormfront.Errors.NO_TEMPLATE(view.getIdentity());
391
+ Stormfront.Ajax.Request = Stormfront.Ajax.RequestReducer.extend({
392
+ type: 'NOT:IMPLEMENTED',
393
+ execute: function(store, event){
394
+ event.callbacks = { };
395
+ event.callbacks.context = this;
396
+ event.callbacks[Stormfront.Ajax.START] = this.onStart;
397
+ event.callbacks[Stormfront.Ajax.SUCCESS] = this.onSuccess;
398
+ event.callbacks[Stormfront.Ajax.ERROR] = this.onError;
310
399
 
311
- return Handlebars.compile(template.html());
312
- }
313
- function expandTemplate(template, vm){
314
- try {
315
- return template(vm);
316
- }
317
- catch(e) {
318
- throw Stormfront.Errors.INCOMPATIBLE_TEMPLATE(view.getIdentity());
319
- }
320
- }
321
- function getViewModel(view){
322
- try {
323
- return view.getViewModel();
324
- }
325
- catch (e){
326
- throw Stormfront.Errors.VIEW_MODEL(view.getIdentity());
327
- }
328
- }
329
- try {
330
- return expandTemplate(compileTemplate(getTemplate(view)), getViewModel(view));
331
- }
332
- catch(e) {
333
- return e;
334
- }
400
+ var self = this;
401
+ function onStart(xhr, settings){
402
+ _.extend(event, { xhr: xhr });
403
+ self.dispatch(new Stormfront.Ajax.StartRequest(event));
335
404
  }
336
- try {
337
- return renderTemplate(this.view);
405
+ function onSuccess(data, status, xhr){
406
+ _.extend(event, {
407
+ data: data,
408
+ status: status,
409
+ xhr: xhr
410
+ });
411
+ self.dispatch(new Stormfront.Ajax.SuccessRequest(event));
338
412
  }
339
- catch(e) {
340
- throw new TemplateError(findError(this.view), e.message);
413
+ function onError(xhr, status, exception){
414
+ _.extend(event, {
415
+ status: status,
416
+ xhr: xhr,
417
+ exception: exception
418
+ });
419
+ self.dispatch(new Stormfront.Ajax.ErrorRequest(event));
341
420
  }
342
- }
421
+ var request = this.getParameters(store, event);
422
+ var settings = _.extend(request, {
423
+ beforeSend: onStart,
424
+ error: onError,
425
+ success: onSuccess
426
+ });
427
+ this.updateEntry(store, event, Stormfront.Ajax.PENDING);
428
+ this.send(settings);
429
+ return store;
430
+ },
431
+ send: function(settings){
432
+ // TODO: Remaining Concerns AJAX:
433
+ // TODO: Aborting similar request
434
+ // TODO: Avoiding equal request
435
+ $.ajax(settings);
436
+ },
437
+ getParameters: function(store, event){
438
+ return { url: '', method: 'get', data: {} }
439
+ },
440
+ onStart: function(store, event) { },
441
+ onSuccess: function(store, event) { },
442
+ onError: function(store, event) { }
343
443
  });
344
444
 
345
- function TemplateError(message, innerException) {
346
- var error = Error.call(this, message);
347
- this.name = 'TemplateError';
348
- this.message = error.message;
349
- this.stack = error.stack;
350
- this.innerException = innerException;
351
- }
445
+ Stormfront.Ajax.StartRequest = Stormfront.Ajax.Support.createGenericRequest(Stormfront.Ajax.START);
446
+ Stormfront.Ajax.Start = Stormfront.Ajax.Support.createGenericReducer(Stormfront.Ajax.START);
352
447
 
353
- TemplateError.prototype = Object.create(Error.prototype);
354
- TemplateError.prototype.constructor = TemplateError;
448
+ Stormfront.Ajax.SuccessRequest = Stormfront.Ajax.Support.createGenericRequest(Stormfront.Ajax.SUCCESS);
449
+ Stormfront.Ajax.Success = Stormfront.Ajax.Support.createGenericReducer(Stormfront.Ajax.SUCCESS);
355
450
 
451
+ Stormfront.Ajax.ErrorRequest = Stormfront.Ajax.Support.createGenericRequest(Stormfront.Ajax.ERROR);
452
+ Stormfront.Ajax.Error = Stormfront.Ajax.Support.createGenericReducer(Stormfront.Ajax.ERROR);
356
453
 
357
- Stormfront.Mixin.Dispatch = {
358
- hasEvents: function(){
359
- return this.trigger;
360
- },
361
- addEvents: function(){
454
+ Stormfront.Ajax.Reducers = [
455
+ Stormfront.Ajax.Start,
456
+ Stormfront.Ajax.Success,
457
+ Stormfront.Ajax.Error
458
+ ];
459
+ Stormfront.Store = Stormfront.Class.extend({
460
+ initialize: function (options) {
362
461
  $.extend(this, Backbone.Events);
462
+
463
+ this._state = new Stormfront.Store.State();
464
+ this._reducers = new Stormfront.Store.Reducers(options);
363
465
  },
364
- streamlineDispatch: function(event){
365
- this.dispatch = function(){
366
- this.trigger('dispatch', event)
367
- };
368
- this.dispatch(event);
466
+ executeAction: function (action) {
467
+ var reducer = this._reducers.find(action);
468
+ if (reducer)
469
+ this.executeDispatch(action, reducer);
470
+ else
471
+ this.trigger(Stormfront.Dispatches.MISSING, action, reducer);
472
+ },
473
+ executeDispatch: function (action, reducer) {
474
+ try {
475
+ this.executeExternalCode(action, reducer);
476
+ } catch (e) {
477
+ this.trigger(Stormfront.Dispatches.ERROR, action, reducer);
478
+ }
369
479
  },
370
- dispatch: function(event){
371
- if (!this.hasEvents())
372
- this.addEvents();
373
- this.streamlineDispatch(event);
480
+ executeExternalCode: function(action, reducer){
481
+ var current = this._state.getState();
482
+ this.trigger(Stormfront.Dispatches.EXECUTING, action, reducer, current);
483
+
484
+ var updated = reducer.execute(current, action);
485
+ this.trigger(Stormfront.Dispatches.EXECUTED, action, reducer, updated);
486
+
487
+ if (updated === Stormfront.NO_CHANGE)
488
+ this.trigger(Stormfront.Dispatches.UNCHANGED, action, reducer);
489
+ if (this._state.setState(updated))
490
+ this.trigger(Stormfront.Dispatches.CHANGE, action, reducer, updated, current);
491
+ else
492
+ this.trigger(Stormfront.Dispatches.NO_RETURN, action, reducer);
374
493
  }
375
- };
494
+ });
376
495
 
377
- Stormfront.Mixin.Other = {
378
- unauthorized: function(entity){
379
- function propagateUnauthorized(source, response, options){
380
- if (response.status === 403)
381
- this.trigger('unauthorized', source, response, options);
382
- }
383
- entity.on('error', propagateUnauthorized);
496
+ Stormfront.Store.State = Stormfront.Class.extend({
497
+ setState: function (state) {
498
+ return state ? this.updateState(state) : false;
499
+ },
500
+ updateState: function (state) {
501
+ this._state = this.copyState(state);
502
+ return true;
503
+ },
504
+ getState: function () {
505
+ return this.copyState(this._state);
384
506
  },
385
- invalid: function(entity){
386
- function propagateInvalid(source, response, options){
387
- if (response.status === 400)
388
- this.trigger('invalid', source, response, options);
507
+ copyState: function (state) {
508
+ return $.extend(true, {}, state);
509
+ }
510
+ });
511
+
512
+ Stormfront.Store.Reducers = Stormfront.Class.extend({
513
+ reducers: { },
514
+ initialize: function (options) {
515
+ function createReducer(type) {
516
+ var reducer = new type(options.dispatcher);
517
+ this.reducers[reducer.type] = reducer;
518
+ _.each(reducer.dependencies, createReducer, this);
389
519
  }
390
- entity.on('error', propagateInvalid);
520
+
521
+ _.each(options.reducers, createReducer, this);
522
+ _.each(Stormfront.Ajax.Reducers, createReducer, this);
523
+ _.each(Stormfront.Store.Operations, createReducer, this);
391
524
  },
392
- empty: function(collection){
393
- function determineEmpty(collection, response, options){
394
- if (collection.isEmpty())
395
- this.trigger('empty', collection, response, options);
525
+ find: function(event){
526
+ return this.reducers[event.type];
527
+ }
528
+ });
529
+
530
+ SetStateAction = function(state){
531
+ return {
532
+ type: 'Stormfront:Store:State:Set',
533
+ state: state
534
+ }
535
+ };
536
+
537
+ SetState = Stormfront.Reducer.extend({
538
+ type: 'Stormfront:Store:State:Set',
539
+ execute: function (store, action) {
540
+ return action.state;
541
+ }
542
+ });
543
+
544
+ Stormfront.Store.Operations = [SetState];
545
+ Stormfront.Dispatcher = Stormfront.Class.extend({
546
+ initialize: function () {
547
+ $.extend(this, Backbone.Events);
548
+ var queue = [];
549
+ var root = this;
550
+
551
+ this.dispatch = function (event) {
552
+ this.trigger(Stormfront.Dispatches.RECEIVED, event);
553
+ if (_.isEmpty(queue))
554
+ process(event);
396
555
  else
397
- this.trigger('populated', collection, response, options);
556
+ enqueue(event);
557
+ };
558
+
559
+ function process(event) {
560
+ enqueue(event);
561
+ processQueue();
562
+ }
563
+
564
+ function enqueue(event) {
565
+ queue.push(event);
566
+ root.trigger(Stormfront.Dispatches.QUEUED, event);
567
+ }
568
+
569
+
570
+ function processQueue() {
571
+ if (_.isEmpty(queue))
572
+ return;
573
+ processNext();
574
+ processQueue();
575
+ }
576
+
577
+ function processNext() {
578
+ root.trigger(Stormfront.Dispatches.READY, queue[0]);
579
+ queue.shift();
580
+ }
581
+ }
582
+ });
583
+ Stormfront.Container = Stormfront.Class.extend({
584
+ page: null,
585
+ initial: null,
586
+ reducers: [],
587
+ initialize: function(location) {
588
+ Stormfront.Patterns.Events(this);
589
+ var dispatcher = new Stormfront.Dispatcher();
590
+ var page = new this.page(dispatcher);
591
+ var store = new Stormfront.Store({
592
+ reducers: this.reducers,
593
+ dispatcher: dispatcher
594
+ });
595
+
596
+ function executeAction(action){
597
+ store.executeAction(action);
598
+ }
599
+ function updateView(action, reducer, newProperties, oldProperties){
600
+ page.updateProperties(newProperties, oldProperties, action, reducer);
398
601
  }
399
- collection.on('sync', determineEmpty);
602
+ this.listenTo(dispatcher, Stormfront.Dispatches.READY, executeAction);
603
+ this.listenTo(store, Stormfront.Dispatches.CHANGE, updateView);
604
+
605
+ Stormfront.Patterns.Dispatcher(this, dispatcher);
606
+ Stormfront.Patterns.Events(this);
607
+
608
+ this.forward(dispatcher);
609
+ this.forward(store);
610
+
611
+ dispatcher.dispatch(SetStateAction(this.initial));
612
+
613
+ location.html(page.render().$el);
614
+
615
+ page.transition('attached');
400
616
  }
617
+ });
618
+
619
+ var homefront = { };
620
+ var Homefront = {
621
+ Patterns: {}
401
622
  };
402
623
 
624
+ Homefront.Template = Stormfront.Class.extend({
625
+ initialize: function (selector) {
626
+ this.selector = selector;
627
+ },
628
+ render: function (vm) {
629
+ function getTemplate(selector) {
630
+ return $('#' + selector + '_template');
631
+ }
632
+ function compileTemplate(selection) {
633
+ return Handlebars.compile(selection.html());
634
+ }
635
+ function expandTemplate(template, vm) {
636
+ return template(vm);
637
+ }
638
+
639
+ var template = getTemplate(this.selector);
640
+ var compiled = compileTemplate(template);
641
+ return expandTemplate(compiled, vm)
642
+ }
643
+ });
644
+
645
+ Homefront.Entity = Stormfront.Class.extend({
646
+ defaults: null,
647
+ initialize: function (properties) {
648
+ this.properties = _.extend(this, this.defaults, properties);
649
+ },
650
+ state: function(){
651
+ return this.getEntity().state;
652
+ },
653
+ getEntity: function(){
654
+ return this.properties;
655
+ },
656
+ isBusy: function(){
657
+ var state = this.state();
658
+ return state === Stormfront.Ajax.START ||
659
+ state === Stormfront.Ajax.PENDING;
660
+ }
661
+ });
662
+ Homefront.Collection = Homefront.Entity.extend({
663
+ entity: null,
664
+ state: '',
665
+ initialize: function (items, state) {
666
+ state = state || this.state;
667
+ var entity = this.entity;
668
+ this.properties = {
669
+ state: state,
670
+ data: _.map(items, function (option) {
671
+ return new entity(option);
672
+ })
673
+ };
674
+ this.state = state;
675
+ },
676
+ list: function () {
677
+ return this.getEntity().data;
678
+ },
679
+ isEmpty: function () {
680
+ return this.list().length === 0;
681
+ },
682
+ find: function (parameters) {
683
+ return _.findWhere(this.list(), parameters);
684
+ },
685
+ groupBy: function (field) {
686
+ return _.groupBy(this.list(), field);
687
+ },
688
+ pluck: function (field) {
689
+ return _.pluck(this.list(), field);
690
+ },
691
+ first: function () {
692
+ return this.list()[0];
693
+ },
694
+ onlyOne: function () {
695
+ return this.list().length === 1;
696
+ }
697
+ });
698
+ Homefront.Request = Stormfront.Ajax.Request.extend({
699
+ getEntity: function (store, action) {
700
+ return {};
701
+ },
702
+ onStart: function (store, action) {
703
+ this.getEntity(store, action).state = Stormfront.Ajax.START;
704
+ return store;
705
+ },
706
+ onSuccess: function (store, action) {
707
+ var entity = this.getEntity(store, action);
708
+ entity.state = Stormfront.Ajax.SUCCESS;
709
+ entity.data = action.data;
710
+ return store;
711
+ },
712
+ onError: function (store, action) {
713
+ var entity = this.getEntity(store);
714
+ entity.state = Stormfront.Ajax.ERROR;
715
+ entity.error = action.error;
716
+ return store;
717
+ }
718
+ });
403
719
 
404
720
  Keys = {
405
721
  Enter: 13,
@@ -453,7 +769,6 @@ KeyboardEvents = Stormfront.Class.extend({
453
769
  this.off('all');
454
770
  }
455
771
  });
456
-
457
772
  MouseEvents = Stormfront.Class.extend({
458
773
  initialize: function(options){
459
774
  options = _.extend({delay: 175}, options);
@@ -600,123 +915,7 @@ MouseEvents = Stormfront.Class.extend({
600
915
  this.$el.global && this.$el.global.off('.me');
601
916
  }
602
917
  });
603
-
604
-
605
- Stormfront.ViewBase = Backbone.View.extend({
606
- initialize: function(){
607
- this.errors = [];
608
- },
609
- render: function(){
610
- try {
611
- this.$el.html(new Stormfront.Template(this).render());
612
- }
613
- catch(e) {
614
- this.handleException(e);
615
- }
616
- return this;
617
- },
618
- getViewModel: function(){
619
- return this.model ? this.model.toJSON ? this.model.toJSON() : this.model : {};
620
- },
621
- handleException: function(exception){
622
- if (exception.innerException)
623
- console.warn(exception.message, exception.innerException);
624
- else
625
- console.warn(exception.message);
626
-
627
- this.handleError(exception.message);
628
- },
629
- handleError: function(message){
630
- if (_.contains(this.errors, message))
631
- return;
632
- this.notifyUser(message);
633
- },
634
- notifyUser: function(message){
635
- this.alert && this.alert.close();
636
-
637
- this.errors.push(message);
638
- this.alert = new Stormfront.Alert({
639
- model: {
640
- summary: Stormfront.Errors.FEEDBACK,
641
- details: this.errors.join(', ')
642
- }
643
- });
644
-
645
- this.listenTo(this.alert, 'closing', this.removeNotification);
646
- this.$el.append(this.alert.render().$el);
647
- },
648
- removeNotification: function(){
649
- this.errors = [];
650
- this.stopListening(this.alert);
651
- },
652
- getIdentity: function(){
653
- return stormfront.string(this.className).firstWord();
654
- },
655
- toString: function(){
656
- return this.getIdentity();
657
- },
658
- close: function(){
659
- this.remove();
660
- }
661
- });
662
-
663
- Stormfront.View = Stormfront.ViewBase.extend({
664
- initialize: function(options){
665
- Stormfront.ViewBase.prototype.initialize.call(this, options);
666
- this.when = this.when || { };
667
- this.addPublisher(this);
668
- this.addSubscriber(this.when);
669
- this.transition.apply(this, stormfront.arguments(arguments).prepend('initializing').get());
670
- },
671
- render: function(){
672
- Stormfront.ViewBase.prototype.render.call(this);
673
- this.transition.apply(this, stormfront.arguments(arguments).prepend('rendering').get());
674
- return this;
675
- },
676
- addPublisher: function(publisher, identity){
677
- function notify(event){
678
- var args = stormfront.arguments(arguments).skip(1).get();
679
- function executeHandler(subscriber){
680
- var handler = stormfront.hash(subscriber).findNestedValue(event.split(':'));
681
- if (_.isFunction(handler))
682
- handler.apply(this, args);
683
- }
684
- _.each(this._subscribers, executeHandler, this);
685
- }
686
- function prepend(identity){
687
- return function(event){
688
- var newEvent = identity + ':' + event;
689
- var args = stormfront.arguments(arguments).skip(1).prepend(newEvent).get();
690
- notify.apply(this, args);
691
- }
692
- }
693
- if (publisher === this)
694
- this.listenTo(publisher, 'transition', notify);
695
- else if (this.model && publisher === this.model)
696
- this.listenTo(publisher, 'all', prepend('model'));
697
- else if (this.collection && publisher === this.collection)
698
- this.listenTo(publisher, 'all', prepend('collection'));
699
- else if (publisher.className)
700
- this.listenTo(publisher, 'transition', prepend(publisher.getIdentity()));
701
- else
702
- this.listenTo(publisher, 'all', prepend(identity));
703
- },
704
- addSubscriber: function(whenBlock){
705
- this._subscribers = this._subscribers || [];
706
- if (whenBlock)
707
- this._subscribers.push(whenBlock);
708
- },
709
- transition: function(){
710
- var args = stormfront.arguments(arguments).prepend('transition').get();
711
- this.trigger.apply(this, args);
712
- },
713
- close: function(){
714
- this.transition.apply(this, stormfront.arguments(arguments).prepend('closing').get());
715
- Stormfront.ViewBase.prototype.close.call(this);
716
- }
717
- });
718
-
719
- Stormfront.Errors = {
918
+ Homefront.Errors = {
720
919
  FEEDBACK: 'An unexpected error was encountered',
721
920
  /** @return {string} */
722
921
  NO_SELECTOR: function() { return 'A template selector was not provided for your view' },
@@ -725,7 +924,7 @@ Stormfront.Errors = {
725
924
  /** @return {string} */
726
925
  INCOMPATIBLE_TEMPLATE: function(name) { return name +'\'s template and view model for your view are not compatible'; },
727
926
  /** @return {string} */
728
- VIEW_MODEL: function(name) { return name + '\'s view model for the child generated an error'; },
927
+ VIEW_MODEL: function(name) { return name + '\'s context generated an error'; },
729
928
  /** @return {string} */
730
929
  NO_LOCATION: function(name) { return name + ' has no locations available to render'; },
731
930
  /** @return {string} */
@@ -742,181 +941,167 @@ Stormfront.Errors = {
742
941
  ADDING: function(name) { return name + ' failed to add'; }
743
942
  };
744
943
 
745
- //TODO: This whole class depends on Bootstrap
746
- Stormfront.Alert = Stormfront.View.extend({
747
- className: 'alert',
748
- when: {
749
- rendering: function(){
750
- this.$el.attr('role', 'alert').
751
- addClass('alert-warning').
752
- addClass('alert-dismissible');
753
-
754
- var self = this;
755
- this.$el.on('closed.bs.alert', function(){
756
- self.trigger('closing');
757
- self.close();
758
- });
759
- }
760
- }
761
- });
762
-
763
- Stormfront.Container = Stormfront.View.extend({
764
- reducers: [],
765
- store: null,
766
- root: null,
767
- when: {
768
- initializing: function(){
769
- this._reducers = _.map(this.reducers, this.createReducer, this);
770
- },
771
- rendering: function(){
772
- this.store = new this.store();
773
- this.root = new this.root({model: this.store});
774
- this.listenToDispatches(this.root);
775
- this.$el.append(this.root.render().$el);
944
+ Homefront.ViewBase = Backbone.View.extend({
945
+ location: null,
946
+ template: null,
947
+ render: function () {
948
+ try {
949
+ var context = this.getContext();
950
+ if (this.template)
951
+ this.$el.html(new Homefront.Template(this.template).render(context));
952
+ } catch (e) {
953
+ this.$el.html('An error was encountered');
954
+ console.error('Failed to render view');
955
+ console.error(e);
776
956
  }
957
+ return this;
777
958
  },
778
- createReducer: function(interaction){
779
- var useCase = new interaction();
780
- this.listenToDispatches(useCase);
781
- return this.addReducer(useCase);
782
- },
783
- addReducer: function(useCase){
784
- this._reducers[useCase.type] = useCase;
785
- return useCase;
786
- },
787
- executeUseCase: function(event){
788
- this._reducers[event.type].execute(this.store, event);
959
+ getContext: function(){
960
+ return this.properties ? this.properties : { };
789
961
  },
790
- listenToDispatches: function(publisher){
791
- this.listenTo(publisher, 'dispatch', this.executeUseCase);
962
+ close: function () {
963
+ this.remove();
792
964
  }
793
965
  });
794
-
795
- Stormfront.Entity = Stormfront.View.extend({
796
- exceptions: {
797
- model: {
798
- error: function(collection, xhr){
799
- var response = new Stormfront.Response(xhr);
800
- if (response.isActionable()) {
801
- this.handleException({error: response.getError()});
802
- response.markAsHandled();
966
+ Homefront.View = Homefront.ViewBase.extend({
967
+ when: {},
968
+ initialize: function (dispatcher) {
969
+ _.each(this.when, this.addSubscriber, this);
970
+ this.addPublisher(this);
971
+ this.transition('before:initializing');
972
+ Stormfront.Patterns.Dispatcher(this, dispatcher);
973
+ this.transition('initializing');
974
+ },
975
+ render: function () {
976
+ this.transition('before:rendering');
977
+ Homefront.ViewBase.prototype.render.call(this);
978
+ this.transition('rendering');
979
+ return this;
980
+ },
981
+ renderAgain: function(){
982
+ this.render();
983
+ this.transition('attached');
984
+ },
985
+ addPublisher: function (publisher, identity) {
986
+ function notify(event) {
987
+ var args = stormfront.arguments(arguments).skip(1).get();
988
+ function executeHandler(subscriber) {
989
+ var handler = stormfront.hash(subscriber).findNestedValue(event.split(':'));
990
+ try {
991
+ if (_.isFunction(handler))
992
+ handler.apply(this, args);
993
+ } catch (e) {
994
+ console.error('Error encountered within the When block: ('+event+')');
995
+ console.error(e);
803
996
  }
804
- },
805
- invalid: function(model, error){
806
- this.handleException({error: error});
807
997
  }
998
+
999
+ _.each(this._subscribers, executeHandler, this);
808
1000
  }
809
- },
810
- overlays: {
811
- model: {
812
- request: function(){
813
- this.addOverlay();
814
- },
815
- sync: function(){
816
- this.removeOverlay();
817
- },
818
- error: function(){
819
- this.removeOverlay();
1001
+
1002
+ function prepend(identity) {
1003
+ return function (event) {
1004
+ var newEvent = identity + ':' + event;
1005
+ var args = stormfront.arguments(arguments).skip(1).prepend(newEvent).get();
1006
+ notify.apply(this, args);
820
1007
  }
821
1008
  }
1009
+
1010
+ if (publisher === this)
1011
+ this.listenTo(publisher, 'transition', notify);
1012
+ else
1013
+ this.listenTo(publisher, 'transition', prepend(identity));
822
1014
  },
823
- change: {
824
- model: {
825
- change: function(){
826
- this.render();
827
- }
828
- }
1015
+ addSubscriber: function (whenBlock) {
1016
+ this._subscribers = this._subscribers || [];
1017
+ if (whenBlock && !_.isEmpty(whenBlock))
1018
+ this._subscribers.push(whenBlock);
829
1019
  },
830
- initialize: function(){
831
- this.addSubscriber(this.overlays);
832
- this.addSubscriber(this.exceptions);
833
- this.addSubscriber(this.change);
834
- this.addPublisher(this.model);
835
- Stormfront.View.prototype.initialize.apply(this, arguments);
1020
+ transition: function () {
1021
+ this.trigger.apply(this, stormfront.arguments(arguments).prepend('transition').get());
836
1022
  },
837
- addOverlay: function(){
838
- this.removeOverlay();
839
- this.overlay = new Stormfront.Overlay({selector: this.$el});
1023
+ updateProperties: function (properties) {
1024
+ var previous = this.properties;
1025
+ this.properties = this.selectProperties(properties);
1026
+ this.transition('updating', this.properties, previous);
840
1027
  },
841
- removeOverlay: function(){
842
- if (this.overlay) {
843
- this.overlay.close();
844
- this.overlay = null;
845
- }
1028
+ selectProperties: function (properties) {
1029
+ return properties;
1030
+ },
1031
+ close: function () {
1032
+ this.transition('before:closing');
1033
+ this.transition('closing');
1034
+ Homefront.ViewBase.prototype.close.call(this);
846
1035
  }
847
1036
  });
848
1037
 
849
- Stormfront.Layout = Stormfront.View.extend({
850
- base: {
851
- initializing: function(){
852
- this._children = new Stormfront.Chaperone();
853
- this.addPublisher(this._children, 'child');
854
- },
855
- rendering: function(){
856
- _.each(this.children, this.composeChild, this);
857
- },
858
- closing: function(){
859
- this._children.close();
860
- },
861
- child: {
862
- created: function(child){
863
- this._children.add(child);
864
- },
865
- added: function(child){
866
- this.addPublisher(child);
867
- this._children.render(child);
868
- },
869
- rendered: function(child){
870
- var location = this.$('.' + child.getIdentity());
871
- this._children.attach(child, location, 'replaceWith');
872
- },
873
- attached: function(child){
874
- child.transition('rendered', child);
875
- },
876
- removed: function(child){
877
- child.close();
878
- this.stopListening(child);
879
- },
880
- error: function(error, innerException){
881
- this.handleException(
882
- new LayoutError(error, innerException)
883
- );
884
- }
885
- }
886
- },
887
- initialize: function(){
888
- this.addSubscriber(this.base);
889
- Stormfront.View.prototype.initialize.apply(this, arguments);
890
- },
891
- composeChild: function(childOptions, childClass){
892
- this._children.create(childClass, childOptions, this);
893
- },
894
- findChild: function(identity){
895
- return this._children.find(identity);
1038
+ Homefront.View.extend = function(protoProps){
1039
+ function copyConstructor(base){
1040
+ return function(){ return base.apply(this, arguments); };
896
1041
  }
897
- });
1042
+ function copyBaseProperties(child, parent){
1043
+ _.extend(child, parent);
1044
+ }
1045
+ function carryPrototypeChain(child, parent){
1046
+ var Surrogate = function(){ this.constructor = child; };
1047
+ Surrogate.prototype = parent.prototype;
1048
+ child.prototype = new Surrogate;
1049
+ }
1050
+ function copyNewProperties(child, protoProps){
1051
+ var newWhen = protoProps['when'];
1052
+ var oldWhen = child.prototype['when'];
898
1053
 
899
- function LayoutError(message, innerException) {
900
- var error = Error(message);
901
- this.name = 'LayoutError';
902
- this.message = error.message;
903
- this.stack = error.stack;
904
- this.innerException = innerException;
905
- }
1054
+ _.extend(child.prototype, protoProps);
906
1055
 
907
- LayoutError.prototype = Object.create(Error.prototype);
908
- LayoutError.prototype.constructor = LayoutError;
1056
+ var when = [];
1057
+ oldWhen && when.push(oldWhen);
1058
+ newWhen && when.push(newWhen);
1059
+ child.prototype['when'] = _.flatten(when);
1060
+ }
1061
+ function enableAccessToBaseClass(){
1062
+ child.__super__ = parent.prototype;
1063
+ }
1064
+
1065
+ var parent = this;
1066
+ var child = copyConstructor(parent);
1067
+ copyBaseProperties(child, parent);
1068
+ carryPrototypeChain(child, parent);
1069
+ copyNewProperties(child, protoProps);
1070
+ enableAccessToBaseClass(child, parent);
1071
+ return child;
1072
+ };
909
1073
 
910
- Stormfront.Overlay = Stormfront.Class.extend({
1074
+ Homefront.Alert = Homefront.View.extend({
1075
+ className: 'alert',
1076
+ events: {
1077
+ 'click .close': 'dismiss'
1078
+ },
1079
+ render: function(){
1080
+ var errors = this.properties.errors || [];
1081
+ var content = $('<div>');
1082
+ var summary = $('<p>').addClass('summary').text(Stormfront.Errors.FEEDBACK);
1083
+ var details = $('<p>').addClass('errors').text(errors.join(', '));
1084
+ var close = $('<div>').addClass('close').attr('title', 'click to close');
1085
+ content.append(summary).append(details).append(close);
1086
+ this.$el.html(content);
1087
+ this.transitionAs('rendering', arguments);
1088
+ return this;
1089
+ },
1090
+ dismiss: function(){
1091
+ this.trigger('dismissed');
1092
+ this.close();
1093
+ }
1094
+ });
1095
+ Homefront.Overlay = Stormfront.Class.extend({
911
1096
  initialize: function(options){
912
1097
  var selector = options.selector;
913
1098
 
914
1099
  if (!selector)
915
- this.overlay = new Stormfront.FullScreenOverlay({model: options});
1100
+ this.overlay = new Homefront.FullScreenOverlay({model: options});
916
1101
  else if (options.saveRequest)
917
- this.overlay = new Stormfront.InlineOverlay();
1102
+ this.overlay = new Homefront.InlineOverlay();
918
1103
  else
919
- this.overlay = new Stormfront.AnonymousOverlay();
1104
+ this.overlay = new Homefront.AnonymousOverlay();
920
1105
 
921
1106
  selector = selector ? selector : document.body;
922
1107
  this.location = selector.size ? selector : $(selector);
@@ -936,7 +1121,7 @@ Stormfront.Overlay = Stormfront.Class.extend({
936
1121
  }
937
1122
  });
938
1123
 
939
- Stormfront.OverlayBase = Stormfront.View.extend({
1124
+ Homefront.OverlayBase = Homefront.View.extend({
940
1125
  className: 'overlay',
941
1126
  activated: false,
942
1127
  PAUSE_DURATION: 0,
@@ -1010,7 +1195,7 @@ Stormfront.OverlayBase = Stormfront.View.extend({
1010
1195
  }
1011
1196
  });
1012
1197
 
1013
- Stormfront.FullScreenOverlay = Stormfront.OverlayBase.extend({
1198
+ Homefront.FullScreenOverlay = Homefront.OverlayBase.extend({
1014
1199
  MINIMUM_DURATION: 900,
1015
1200
  css: {
1016
1201
  overlay_background: {
@@ -1028,7 +1213,7 @@ Stormfront.FullScreenOverlay = Stormfront.OverlayBase.extend({
1028
1213
  }
1029
1214
  });
1030
1215
 
1031
- Stormfront.AnonymousOverlay = Stormfront.OverlayBase.extend({
1216
+ Homefront.AnonymousOverlay = Homefront.OverlayBase.extend({
1032
1217
  PAUSE_DURATION: 200,
1033
1218
  MINIMUM_DURATION: 450,
1034
1219
  css: {
@@ -1047,7 +1232,7 @@ Stormfront.AnonymousOverlay = Stormfront.OverlayBase.extend({
1047
1232
  }
1048
1233
  });
1049
1234
 
1050
- Stormfront.InlineOverlay = Stormfront.OverlayBase.extend({
1235
+ Homefront.InlineOverlay = Homefront.OverlayBase.extend({
1051
1236
  className: 'input_overlay',
1052
1237
  MINIMUM_DURATION: 1500,
1053
1238
  MAXIMUM_WIDTH: 1200,
@@ -1085,53 +1270,546 @@ Stormfront.InlineOverlay = Stormfront.OverlayBase.extend({
1085
1270
  }
1086
1271
  }
1087
1272
  });
1273
+ Homefront.Guidance = {
1274
+ Generic: function (type, text) {
1275
+ return Homefront.Text.extend({
1276
+ tagName: type,
1277
+ className: 'guidance',
1278
+ text: text,
1279
+ selectProperties: function () {
1280
+ return {value: this.text}
1281
+ }
1282
+ });
1283
+ },
1284
+ Paragraph: function (text) {
1285
+ return Homefront.Guidance.Generic('p', text);
1286
+ },
1287
+ Table: function (text) {
1288
+ text = '<td colspan="99">' + text + '</td>';
1289
+ return Homefront.Guidance.Generic('tr', text);
1290
+ },
1291
+ List: function (text) {
1292
+ return Homefront.Guidance.Generic('li', text);
1293
+ }
1294
+ };
1088
1295
 
1089
- Stormfront.Reducer = Stormfront.Class.extend({
1090
- type: Stormfront.Errors.NOT_IMPLEMENTED,
1091
- initialize: function(){
1092
- _.extend(this, Stormfront.Mixin.Dispatch);
1296
+ Homefront.Chaperone = Homefront.View.extend({
1297
+ method: 'append',
1298
+ when: {
1299
+ initializing: function () {
1300
+ this._children = [];
1301
+ },
1302
+ updating: function (properties) {
1303
+ this.updateChildren(this._children, properties);
1304
+ },
1305
+ attached: function(){
1306
+ var previous = this._children;
1307
+ this._children = [];
1308
+ this.createChildren();
1309
+ this.renderChildren(this._children);
1310
+ this.attachChildren(this._children);
1311
+ this.closeChildren(previous);
1312
+ },
1313
+ closing: function () {
1314
+ this.closeChildren(this._children);
1315
+ this._children = [];
1316
+ },
1317
+ chaperone: {
1318
+ create: function (type, properties) {
1319
+ type = _.isString(type) ? stormfront.type(type) : type;
1320
+ properties = _.isObject(properties) ? properties : this.properties;
1321
+ if (type) {
1322
+ var child = new type(this.getDispatcher());
1323
+ this.transition('chaperone:add', child, properties);
1324
+ } else {
1325
+ console.error('Child type does not exist: ', type);
1326
+ }
1327
+ },
1328
+ add: function (child, properties) {
1329
+ this._children.push(child);
1330
+ child.transition && this.addPublisher(child, 'child');
1331
+ child.updateProperties(properties);
1332
+ },
1333
+ attach: function (child, location, method) {
1334
+ child.transition('attaching', child);
1335
+ location[method](child.$el);
1336
+ child.transition('attached', child);
1337
+ }
1338
+ },
1339
+ child: {
1340
+ closing: function (child) {
1341
+ this._children.splice(this._children.indexOf(child), 1);
1342
+ this.stopListening(child);
1343
+ }
1344
+ }
1345
+ },
1346
+ createChild: function (type, properties) {
1347
+ this.transition('chaperone:create', type, properties);
1348
+ },
1349
+ attachChild: function (child, location, method) {
1350
+ this.transition('chaperone:attach', child, location, method || this.method);
1351
+ },
1352
+ createChildren: function () {
1353
+
1354
+ },
1355
+ renderChildren: function (children) {
1356
+ _.invoke(children, 'render');
1357
+ },
1358
+ attachChildren: function (children) {
1359
+ function attachChild(child) {
1360
+ if (child.location)
1361
+ this.attachChild(child, this.$('.' + child.location));
1362
+ else
1363
+ this.attachChild(child, this.$el, 'append');
1364
+ }
1365
+ _.each(children, attachChild, this);
1366
+ },
1367
+ updateChildren: function (children, properties) {
1368
+ _.invoke(children, 'updateProperties', properties);
1369
+ },
1370
+ closeChildren: function (children) {
1371
+ function stop(child) {
1372
+ this.stopListening(child);
1373
+ }
1374
+ //TODO: This bit is odd...
1375
+ _.each(children, stop, this);
1376
+ _.invoke(children, 'close');
1377
+ },
1378
+ at: function (index) {
1379
+ return this._children[index];
1380
+ }
1381
+ });
1382
+ Homefront.Layout = Homefront.Chaperone.extend({
1383
+ method: 'replaceWith',
1384
+ createChildren: function () {
1385
+ _.each(this.children, this.createChild, this);
1386
+ }
1387
+ });
1388
+ Homefront.Switch = Homefront.Chaperone.extend({
1389
+ when: {
1390
+ updating: function () {
1391
+ if (this._state !== undefined && this.stateChanged())
1392
+ this.renderAgain();
1393
+ }
1394
+ },
1395
+ getCases: function () {
1396
+ var feedback = '"getState" and "getCases" are not implemented';
1397
+ var guidance = Homefront.Guidance.Paragraph(feedback);
1398
+ return {
1399
+ not_implemented: guidance
1400
+ };
1401
+ },
1402
+ getState: function () {
1403
+ return 'not_implemented'
1404
+ },
1405
+ stateChanged: function(){
1406
+ return this._state !== this.getState(this.properties);
1407
+ },
1408
+ updateState: function () {
1409
+ var oldState = this._state;
1410
+ var newState = this.getState(this.properties);
1411
+ this._state = newState;
1412
+
1413
+ if (oldState !== newState) {
1414
+ oldState && this.transition('exiting:' + oldState, oldState);
1415
+ this.transition('entering:' + newState, newState);
1416
+ } else {
1417
+ oldState && this.transition('staying:' + oldState, oldState);
1418
+ }
1419
+ return newState;
1420
+ },
1421
+ createChildren: function () {
1422
+ var cases = this.getCases();
1423
+ var state = this.updateState();
1424
+ var candidate = cases[state];
1425
+ candidate && this.createChild(candidate);
1426
+ },
1427
+ attachChildren: function (children) {
1428
+ if (children.length === 1) {
1429
+ var candidate = children[0];
1430
+ candidate.$el.addClass(this.className);
1431
+ candidate.$el.addClass(this._state);
1432
+ this.attachChild(candidate, this.$el, 'replaceWith');
1433
+ this.setElement(candidate.el);
1434
+ }
1435
+ }
1436
+ });
1437
+
1438
+ Homefront.Expandable = Homefront.Switch.extend({
1439
+ summary: null,
1440
+ details: null,
1441
+ expanded: false,
1442
+ getCases: function () {
1443
+ return {
1444
+ summary: this.summary,
1445
+ details: this.details
1446
+ };
1447
+ },
1448
+ getState: function () {
1449
+ return this.expanded ? 'details' : 'summary';
1450
+ },
1451
+ expand: function () {
1452
+ this.expanded = true;
1453
+ this.renderAgain();
1454
+ },
1455
+ collapse: function () {
1456
+ this.expanded = false;
1457
+ this.renderAgain();
1458
+ }
1459
+ });
1460
+ Homefront.Enumerator = Homefront.Chaperone.extend({
1461
+ item: null,
1462
+ when: {
1463
+ chaperone: {
1464
+ add: function(child){
1465
+ child.location = this.item.location;
1466
+ }
1467
+ }
1468
+ },
1469
+ createChildren: function () {
1470
+ var items = this.selectItems(this.properties);
1471
+ function createChild(item) {
1472
+ this.createChild(this.item.view, item);
1473
+ }
1474
+ _.each(items, createChild, this);
1475
+ },
1476
+ updateChildren: function(children, properties){
1477
+ var items = this.selectItems(properties);
1478
+ function updateChild(child, index) {
1479
+ var item = items[index];
1480
+ child.updateProperties(item);
1481
+ }
1482
+ _.each(children, updateChild);
1093
1483
  },
1094
- execute: function(model, event){
1095
- throw Stormfront.Errors.UseCases.NOT_IMPLEMENTED;
1484
+ composeChild: function(item){
1485
+ return this.createChild(this.item, item);
1486
+ },
1487
+ selectItems: function (properties) {
1488
+ return properties;
1489
+ },
1490
+ at: function (index) {
1491
+ return this._children[index];
1492
+ }
1493
+ });
1494
+ Homefront.List = Homefront.Switch.extend({
1495
+ item: null,
1496
+ when: {
1497
+ initializing: function () {
1498
+ this.enumerator = Homefront.Enumerator.extend({
1499
+ tagName: this.tagName,
1500
+ template: this.template,
1501
+ className: this.className + ' stormfront.enumerator',
1502
+ item: this.item,
1503
+ getContext: this.getContext,
1504
+ selectItems: this.selectItems
1505
+ });
1506
+ }
1507
+ },
1508
+ getCases: function () {
1509
+ return {
1510
+ empty: this.empty,
1511
+ populated: this.enumerator
1512
+ }
1513
+ },
1514
+ getState: function (properties) {
1515
+ var items = this.selectItems(properties);
1516
+ return items && items.length > 0 ? 'populated' : 'empty';
1517
+ },
1518
+ selectItems: function (properties) {
1519
+ return properties.list ? properties.list() : properties;
1520
+ }
1521
+ });
1522
+ //TODO: This should have a better name I suppose
1523
+ Homefront.ActiveList = Homefront.List.extend({
1524
+ getCases: function () {
1525
+ //lazy shortcut
1526
+ this.populated = this.enumerator;
1527
+ return this;
1528
+ },
1529
+ getState: function (properties) {
1530
+ var state = properties.state;
1531
+ if (state === Stormfront.Ajax.SUCCESS)
1532
+ //TODO: Fetching inventory can return an error in the data
1533
+ return this.selectItems(properties).length === 0 ? 'empty' : properties.data.error ? 'error' : 'populated';
1534
+ else if (state === Stormfront.Ajax.START)
1535
+ return 'requesting';
1536
+ else if (state === Stormfront.Ajax.ERROR)
1537
+ return 'error';
1538
+ return state;
1539
+ },
1540
+ selectItems: function(properties) {
1541
+ return properties.data
1096
1542
  }
1097
1543
  });
1098
1544
 
1099
- Stormfront.Request = function(model, request, options){
1100
- var startTime = stormfront.time.now();
1101
- var overlay = new Stormfront.Overlay(options);
1102
- function tryRequest(){
1103
- return shouldWait() ? delayRequest() : sendRequest();
1545
+ //TODO: Got two of them now?
1546
+ Homefront.Series = Homefront.List.extend({
1547
+ getState: function (properties) {
1548
+ var state = properties.state();
1549
+ if (state === Stormfront.Ajax.SUCCESS) {
1550
+ var items = this.selectItems(properties);
1551
+ return items && items.length > 0 ? 'populated' : 'empty';
1552
+ }
1553
+ return state;
1554
+ },
1555
+ selectItems: function (properties) {
1556
+ return properties.list();
1104
1557
  }
1105
- function shouldWait(){
1106
- //TODO: This is a dependency upon the Indigo behavior
1107
- return options.wait && $.xhrPool.length > 0;
1558
+ });
1559
+ Homefront.Patterns.Selectable = function(method, action){
1560
+ var events = {};
1561
+ events[method] = 'select';
1562
+ return {
1563
+ events: events,
1564
+ action: action,
1565
+ select: function(){
1566
+ var action = stormfront.type(this.action);
1567
+ this.dispatch(new action(this.properties));
1568
+ }
1569
+ };
1570
+ };
1571
+
1572
+ Homefront.SelectableList = Homefront.Enumerator.extend({
1573
+ className: 'options',
1574
+ item: null,
1575
+ select: null,
1576
+ method: 'click',
1577
+ when: {
1578
+ initializing: function(){
1579
+ var item = stormfront.type(this.item);
1580
+ this.item = {
1581
+ view: item.extend(Homefront.Patterns.Selectable(this.method, this.select))
1582
+ };
1583
+ }
1584
+ },
1585
+ selectItems: function (properties) {
1586
+ return properties.list();
1108
1587
  }
1109
- function delayRequest(){
1110
- if (stormfront.time.now() - startTime < 5000)
1111
- setTimeout(tryRequest, 1000);
1112
- else
1113
- overlay.error('Request timed out. Please try again soon.');
1114
- return null;
1588
+ });
1589
+
1590
+ Homefront.SelectableDisplay = Homefront.Switch.extend({
1591
+ className: 'selected',
1592
+ select: null,
1593
+ guidance: '',
1594
+ display: null,
1595
+ getCases: function() {
1596
+ return {
1597
+ none: Homefront.Guidance.Paragraph(this.guidance),
1598
+ selected: this.display
1599
+ };
1600
+ },
1601
+ when: {
1602
+ entering: {
1603
+ none: function(){
1604
+ var entity = this.properties;
1605
+ if (entity.onlyOne()) {
1606
+ var action = stormfront.type(this.select);
1607
+ this.dispatch(new action(entity.first()));
1608
+ }
1609
+ }
1610
+ },
1611
+ updating: function(){
1612
+ this.render();
1613
+ }
1614
+ },
1615
+ getState: function(properties){
1616
+ return properties.selected() ? 'selected' : 'none';
1115
1617
  }
1116
- function sendRequest(){
1117
- function onComplete(){
1118
- removeListeners();
1119
- overlay.close();
1618
+ });
1619
+
1620
+ //TODO: Could be a list or a series.
1621
+ Homefront.Selection = Homefront.Switch.extend({
1622
+ className: 'selection',
1623
+ item: null,
1624
+ select: null,
1625
+ method: 'click',
1626
+ display: null,
1627
+ none: '',
1628
+ initial: '',
1629
+ empty: '',
1630
+ list: null,
1631
+ adapter: null,
1632
+ when: {
1633
+ initializing: function(){
1634
+ this.selectList = Homefront.SelectableList.extend({
1635
+ item: this.item,
1636
+ select: this.select,
1637
+ method: this.method || 'click'
1638
+ });
1639
+ this.selectDisplay = Homefront.SelectableDisplay.extend({
1640
+ select: this.select,
1641
+ display: this.display,
1642
+ guidance: this.none
1643
+ });
1120
1644
  }
1121
- function onError(model, xhr){
1122
- removeListeners();
1123
- overlay.error(xhr);
1645
+ },
1646
+ getCases: function(){
1647
+ return {
1648
+ initial: Homefront.Guidance.Paragraph(this.initial),
1649
+ empty: Homefront.Guidance.Paragraph(this.empty),
1650
+ populated: Homefront.Layout.extend({
1651
+ children: [this.selectList, this.selectDisplay]
1652
+ })
1124
1653
  }
1125
- function removeListeners(){
1126
- model.off('error', onError);
1127
- model.off('sync', onComplete);
1128
- model.off('invalid', onComplete);
1654
+ },
1655
+ getState: function (properties) {
1656
+ var state = properties.state();
1657
+ if (state === Stormfront.Ajax.SUCCESS)
1658
+ return properties.isEmpty() ? 'empty' : 'populated';
1659
+ return state;
1660
+ },
1661
+ selectProperties: function(properties){
1662
+ var adapter = stormfront.type(this.adapter);
1663
+ return new adapter(properties);
1664
+ }
1665
+ });
1666
+ Homefront.Search = Homefront.Layout.extend({
1667
+ className: 'search',
1668
+ adapter: null,
1669
+ when: {
1670
+ initializing: function () {
1671
+ var results = Homefront.Selection.extend({
1672
+ select: this.select,
1673
+ display: this.details,
1674
+ none: this.none,
1675
+ empty: this.empty,
1676
+ initial: this.initial,
1677
+ item: this.item,
1678
+ method: this.method || 'click'
1679
+ });
1680
+ this.children = [this.search, results]
1681
+ }
1682
+ },
1683
+ selectProperties: function (properties) {
1684
+ var adapter = stormfront.type(this.adapter);
1685
+ return new adapter(properties);
1686
+ }
1687
+ });
1688
+
1689
+ Homefront.Text = Homefront.View.extend({
1690
+ when: {
1691
+ updating: function(){
1692
+ this.updateValue();
1693
+ },
1694
+ rendering: function(){
1695
+ this.updateValue();
1696
+ }
1697
+ },
1698
+ updateValue: function(){
1699
+ this.$el.text(this.getValue(this.properties));
1700
+ },
1701
+ getValue: function(properties){
1702
+ return this.value || properties.value;
1703
+ }
1704
+ });
1705
+
1706
+ Homefront.Label = Homefront.Text.extend({
1707
+ tagName: 'label',
1708
+ when: {
1709
+ rendering: function() {
1710
+ this.$el.attr('for', this.properties.for);
1711
+ }
1712
+ }
1713
+ });
1714
+
1715
+ Homefront.Paragraph = Homefront.Text.extend({
1716
+ tagName: 'p'
1717
+ });
1718
+
1719
+ Homefront.Input = Homefront.Text.extend({
1720
+ tagName: 'input',
1721
+ enable: function(){
1722
+ this.$el.prop('disabled', false);
1723
+ },
1724
+ disable: function(){
1725
+ this.$el.prop('disabled', true);
1726
+ },
1727
+ updateValue: function(){
1728
+ this.$el.val(this.getValue(this.properties));
1729
+ },
1730
+ readValue: function(){
1731
+ return this.$el.val();
1732
+ }
1733
+ });
1734
+
1735
+ Homefront.Select = Homefront.Input.extend({
1736
+ tagName: 'select',
1737
+ events: {
1738
+ 'change': 'select'
1739
+ },
1740
+ when: {
1741
+ rendering: function(){
1742
+ this.updateSelect();
1743
+ },
1744
+ updating: function(){
1745
+ this.updateSelect();
1746
+ }
1747
+ },
1748
+ updateSelect: function(){
1749
+ this.updateOptions();
1750
+ this.updateValue();
1751
+ },
1752
+ updateOptions: function(){
1753
+ var options = this.getOptions(this.properties);
1754
+ function createOption(value){
1755
+ return $('<option>').val(value).text(value);
1129
1756
  }
1130
- model.once('invalid', onError);
1131
- model.once('error', onError);
1132
- model.once('sync', onComplete);
1757
+ this.$el.append(_.map(options, createOption));
1758
+ },
1759
+ getOptions: function(properties){
1760
+ return properties.options;
1761
+ },
1762
+ getValue: function(properties) {
1763
+ return properties.selected;
1764
+ },
1765
+ select: function(){
1766
+ this.transition('change', this.$el.val());
1767
+ }
1768
+ });
1133
1769
 
1134
- return request.call(model, options.args, options.options);
1770
+ Homefront.TextBox = Homefront.Input.extend({
1771
+ events: {
1772
+ 'change': 'change',
1773
+ 'blur': 'blur',
1774
+ 'keyup': 'special'
1775
+ },
1776
+ when: {
1777
+ rendering: function(){
1778
+ this.$el.attr('type', 'text');
1779
+ }
1780
+ },
1781
+ change: function(){
1782
+ this.transition('change', this.readValue());
1783
+ },
1784
+ blur: function(){
1785
+ this.transition('blur', this.readValue());
1786
+ },
1787
+ special: function(event){
1788
+ if (event.which === Keys.Enter)
1789
+ this.transition('enter', this.readValue());
1790
+ }
1791
+ });
1792
+
1793
+ Homefront.Clicker = Homefront.Input.extend({
1794
+ events: {
1795
+ 'click': 'click'
1796
+ },
1797
+ click: function(){
1798
+ this.transition('click');
1799
+ }
1800
+ });
1801
+
1802
+ Homefront.Button = Homefront.Clicker.extend({
1803
+ when: {
1804
+ rendering: function(){
1805
+ this.$el.attr('type', 'button');
1806
+ }
1807
+ }
1808
+ });
1809
+ Homefront.Leaf = Homefront.View.extend({
1810
+ when: {
1811
+ initializing: function() {
1812
+ this.template = stormfront.string(this.className).firstWord() || null;
1813
+ }
1135
1814
  }
1136
- return tryRequest();
1137
- };
1815
+ });