blob: 375fe6b00abb72e472f39171c2ca376d7cc7cb9e [file] [log] [blame]
Simon Hunt195cb382014-11-03 17:50:51 -08001/*
2 * Copyright 2014 Open Networking Laboratory
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17/*
18 ONOS GUI -- Base Framework
19
20 @author Simon Hunt
21 */
22
23(function ($) {
24 'use strict';
25 var tsI = new Date().getTime(), // initialize time stamp
26 tsB, // build time stamp
Simon Hunt25248912014-11-04 11:25:48 -080027 mastHeight = 36, // see mast2.css
Simon Hunt142d0032014-11-04 20:13:09 -080028 defaultVid = 'sample';
Simon Hunt195cb382014-11-03 17:50:51 -080029
30
31 // attach our main function to the jQuery object
32 $.onos = function (options) {
Simon Hunt25248912014-11-04 11:25:48 -080033 var uiApi,
34 viewApi,
35 navApi;
36
37 var defaultOptions = {
Simon Hunt142d0032014-11-04 20:13:09 -080038 trace: false,
39 startVid: defaultVid
Simon Hunt25248912014-11-04 11:25:48 -080040 };
41
42 // compute runtime settings
43 var settings = $.extend({}, defaultOptions, options);
Simon Hunt195cb382014-11-03 17:50:51 -080044
45 // internal state
46 var views = {},
47 current = {
48 view: null,
49 ctx: ''
50 },
51 built = false,
Simon Hunt0df1b1d2014-11-04 22:58:29 -080052 errorCount = 0,
Simon Hunt934c3ce2014-11-05 11:45:07 -080053 keyHandler = {
54 fn: null,
55 map: {}
56 };
Simon Hunt195cb382014-11-03 17:50:51 -080057
58 // DOM elements etc.
Simon Huntdb9eb072014-11-04 19:12:46 -080059 var $view,
60 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080061
62
Simon Hunt0df1b1d2014-11-04 22:58:29 -080063 function whatKey(code) {
64 switch (code) {
65 case 13: return 'enter';
66 case 16: return 'shift';
67 case 17: return 'ctrl';
68 case 18: return 'alt';
69 case 27: return 'esc';
70 case 32: return 'space';
71 case 37: return 'leftArrow';
72 case 38: return 'upArrow';
73 case 39: return 'rightArrow';
74 case 40: return 'downArrow';
75 case 91: return 'cmdLeft';
76 case 93: return 'cmdRight';
77 default:
78 if ((code >= 48 && code <= 57) ||
79 (code >= 65 && code <= 90)) {
80 return String.fromCharCode(code);
81 } else if (code >= 112 && code <= 123) {
82 return 'F' + (code - 111);
83 }
84 return '.';
85 }
86 }
87
88
Simon Hunt195cb382014-11-03 17:50:51 -080089 // ..........................................................
90 // Internal functions
91
92 // throw an error
93 function throwError(msg) {
94 // separate function, as we might add tracing here too, later
95 throw new Error(msg);
96 }
97
98 function doError(msg) {
99 errorCount++;
Simon Hunt25248912014-11-04 11:25:48 -0800100 console.error(msg);
101 }
102
103 function trace(msg) {
104 if (settings.trace) {
105 console.log(msg);
106 }
107 }
108
109 function traceFn(fn, params) {
110 if (settings.trace) {
111 console.log('*FN* ' + fn + '(...): ' + params);
112 }
Simon Hunt195cb382014-11-03 17:50:51 -0800113 }
114
115 // hash navigation
116 function hash() {
117 var hash = window.location.hash,
118 redo = false,
119 view,
120 t;
121
Simon Hunt25248912014-11-04 11:25:48 -0800122 traceFn('hash', hash);
123
Simon Hunt195cb382014-11-03 17:50:51 -0800124 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800125 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800126 redo = true;
127 }
128
129 t = parseHash(hash);
130 if (!t || !t.vid) {
131 doError('Unable to parse target hash: ' + hash);
132 }
133
134 view = views[t.vid];
135 if (!view) {
136 doError('No view defined with id: ' + t.vid);
137 }
138
139 if (redo) {
140 window.location.hash = makeHash(t);
141 // the above will result in a hashchange event, invoking
142 // this function again
143 } else {
144 // hash was not modified... navigate to where we need to be
145 navigate(hash, view, t);
146 }
Simon Hunt195cb382014-11-03 17:50:51 -0800147 }
148
149 function parseHash(s) {
150 // extract navigation coordinates from the supplied string
151 // "vid,ctx" --> { vid:vid, ctx:ctx }
Simon Hunt25248912014-11-04 11:25:48 -0800152 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800153
154 var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
155 if (m) {
156 return { vid: m[1], ctx: m[2] };
157 }
158
159 m = /^[#]{0,1}(\S+)$/.exec(s);
160 return m ? { vid: m[1] } : null;
161 }
162
163 function makeHash(t, ctx) {
Simon Hunt25248912014-11-04 11:25:48 -0800164 traceFn('makeHash');
Simon Hunt195cb382014-11-03 17:50:51 -0800165 // make a hash string from the given navigation coordinates.
166 // if t is not an object, then it is a vid
167 var h = t,
168 c = ctx || '';
169
170 if ($.isPlainObject(t)) {
171 h = t.vid;
172 c = t.ctx || '';
173 }
174
175 if (c) {
176 h += ',' + c;
177 }
Simon Hunt25248912014-11-04 11:25:48 -0800178 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800179 return h;
180 }
181
182 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800183 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800184 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800185 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800186 createView(view);
187 setView(view, hash, t);
188 }
189
190 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800191 traceFn('reportBuildErrors');
Simon Hunt195cb382014-11-03 17:50:51 -0800192 // TODO: validate registered views / nav-item linkage etc.
193 console.log('(no build errors)');
194 }
195
Simon Hunt25248912014-11-04 11:25:48 -0800196 // returns the reference if it is a function, null otherwise
197 function isF(f) {
198 return $.isFunction(f) ? f : null;
199 }
200
Simon Hunt195cb382014-11-03 17:50:51 -0800201 // ..........................................................
202 // View life-cycle functions
203
Simon Hunt25248912014-11-04 11:25:48 -0800204 function setViewDimensions(sel) {
205 var w = window.innerWidth,
206 h = window.innerHeight - mastHeight;
207 sel.each(function () {
208 $(this)
209 .css('width', w + 'px')
210 .css('height', h + 'px')
211 });
212 }
213
Simon Hunt195cb382014-11-03 17:50:51 -0800214 function createView(view) {
215 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800216
Simon Hunt195cb382014-11-03 17:50:51 -0800217 // lazy initialization of the view
218 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800219 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800220 $d = $view.append('div')
221 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800222 id: view.vid,
223 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800224 });
Simon Hunt25248912014-11-04 11:25:48 -0800225 setViewDimensions($d);
226 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800227 }
228 }
229
230 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800231 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800232 // set the specified view as current, while invoking the
233 // appropriate life-cycle callbacks
234
235 // if there is a current view, and it is not the same as
236 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800237 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800238 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800239
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800240 // detach radio buttons, key handlers, etc.
241 $('#mastRadio').children().detach();
242 keyHandler.fn = null;
243 keyHandler.map = {};
Simon Hunt195cb382014-11-03 17:50:51 -0800244 }
245
246 // cache new view and context
247 current.view = view;
248 current.ctx = t.ctx || '';
249
Simon Hunt195cb382014-11-03 17:50:51 -0800250 // preload is called only once, after the view is in the DOM
251 if (!view.preloaded) {
Simon Hunt25248912014-11-04 11:25:48 -0800252 view.preload(current.ctx);
253 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800254 }
255
256 // clear the view of stale data
257 view.reset();
258
259 // load the view
Simon Hunt25248912014-11-04 11:25:48 -0800260 view.load(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800261 }
262
Simon Huntdb9eb072014-11-04 19:12:46 -0800263 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800264 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800265 return view.vid + '-' + id;
266 }
267
268 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800269 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800270 var re = new RegExp('^' + view.vid + '-');
271 return uid.replace(re, '');
272 }
273
Simon Hunt934c3ce2014-11-05 11:45:07 -0800274 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800275 var view = views[vid],
276 btnG;
277
278 // lazily create the buttons...
279 if (!(btnG = view.radioButtons)) {
280 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800281 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800282
283 btnSet.forEach(function (btn, i) {
284 var bid = btn.id || 'b' + i,
285 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800286 uid = makeUid(view, bid),
287 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800288 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800289 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800290 class: 'radio'
291 })
292 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800293
294 btnG.buttonDef[uid] = btn;
295
Simon Huntdb9eb072014-11-04 19:12:46 -0800296 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800297 button.classed('active', true);
Simon Huntdb9eb072014-11-04 19:12:46 -0800298 }
299 });
300
301 btnG.selectAll('span')
302 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800303 var button = d3.select(this),
304 uid = button.attr('id'),
305 btn = btnG.buttonDef[uid],
306 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800307
308 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800309 btnG.selectAll('span').classed('active', false);
310 button.classed('active', true);
311 if (isF(btn.cb)) {
312 btn.cb(view.token(), btn);
313 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800314 }
315 });
316
317 view.radioButtons = btnG;
318 }
319
320 // attach the buttons to the masthead
321 $mastRadio.node().appendChild(btnG.node());
322 }
323
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800324 function setKeyBindings(keyArg) {
325 if ($.isFunction(keyArg)) {
326 // set general key handler callback
327 keyHandler.fn = keyArg;
328 } else {
329 // set specific key filter map
330 keyHandler.map = keyArg;
331 }
332 }
333
334 function keyIn() {
335 var event = d3.event,
336 keyCode = event.keyCode,
337 key = whatKey(keyCode),
338 cb = isF(keyHandler.map[key]) || isF(keyHandler.fn);
339
340 if (cb) {
341 cb(current.view.token(), key, keyCode, event);
342 }
343 }
344
Simon Hunt25248912014-11-04 11:25:48 -0800345 function resize(e) {
346 d3.selectAll('.onosView').call(setViewDimensions);
347 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800348 if (current.view) {
Simon Hunt25248912014-11-04 11:25:48 -0800349 current.view.resize(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800350 }
351 }
352
353 // ..........................................................
354 // View class
355 // Captures state information about a view.
356
357 // Constructor
358 // vid : view id
359 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800360 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800361 function View(vid) {
362 var av = 'addView(): ',
363 args = Array.prototype.slice.call(arguments),
364 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800365 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800366
367 args.shift(); // first arg is always vid
368 if (typeof args[0] === 'string') { // nid specified
369 nid = args.shift();
370 }
371 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800372
373 this.vid = vid;
374
375 if (validateViewArgs(vid)) {
376 this.nid = nid; // explicit navitem id (can be null)
377 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800378 this.$div = null; // view not yet added to DOM
379 this.radioButtons = null; // no radio buttons yet
380 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800381 }
Simon Hunt195cb382014-11-03 17:50:51 -0800382 }
383
384 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800385 var av = "ui.addView(...): ",
386 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800387 if (typeof vid !== 'string' || !vid) {
388 doError(av + 'vid required');
389 } else if (views[vid]) {
390 doError(av + 'View ID "' + vid + '" already exists');
391 } else {
392 ok = true;
393 }
394 return ok;
395 }
396
397 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800398 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800399 return {
Simon Hunt25248912014-11-04 11:25:48 -0800400 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800401 vid: this.vid,
402 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800403 $div: this.$div,
404
405 // functions
406 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800407 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800408 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800409 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800410 setKeys: this.setKeys,
411 dataLoadError: this.dataLoadError
Simon Hunt195cb382014-11-03 17:50:51 -0800412 }
Simon Hunt25248912014-11-04 11:25:48 -0800413 },
414
415 preload: function (ctx) {
416 var c = ctx || '',
417 fn = isF(this.cb.preload);
418 traceFn('View.preload', this.vid + ', ' + c);
419 if (fn) {
420 trace('PRELOAD cb for ' + this.vid);
421 fn(this.token(), c);
422 }
423 },
424
425 reset: function () {
426 var fn = isF(this.cb.reset);
427 traceFn('View.reset', this.vid);
428 if (fn) {
429 trace('RESET cb for ' + this.vid);
430 fn(this.token());
431 } else if (this.cb.reset === true) {
432 // boolean true signifies "clear view"
433 trace(' [true] cleaing view...');
434 viewApi.empty();
435 }
436 },
437
438 load: function (ctx) {
439 var c = ctx || '',
440 fn = isF(this.cb.load);
441 traceFn('View.load', this.vid + ', ' + c);
442 this.$div.classed('currentView', true);
443 // TODO: add radio button set, if needed
444 if (fn) {
445 trace('LOAD cb for ' + this.vid);
446 fn(this.token(), c);
447 }
448 },
449
450 unload: function () {
451 var fn = isF(this.cb.unload);
452 traceFn('View.unload', this.vid);
453 this.$div.classed('currentView', false);
454 // TODO: remove radio button set, if needed
455 if (fn) {
456 trace('UNLOAD cb for ' + this.vid);
457 fn(this.token());
458 }
459 },
460
461 resize: function (ctx) {
462 var c = ctx || '',
463 fn = isF(this.cb.resize),
464 w = this.width(),
465 h = this.height();
466 traceFn('View.resize', this.vid + '/' + c +
467 ' [' + w + 'x' + h + ']');
468 if (fn) {
469 trace('RESIZE cb for ' + this.vid);
470 fn(this.token(), c);
471 }
472 },
473
474 error: function (ctx) {
475 var c = ctx || '',
476 fn = isF(this.cb.error);
477 traceFn('View.error', this.vid + ', ' + c);
478 if (fn) {
479 trace('ERROR cb for ' + this.vid);
480 fn(this.token(), c);
481 }
482 },
483
484 width: function () {
485 return $(this.$div.node()).width();
486 },
487
488 height: function () {
489 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800490 },
Simon Hunt25248912014-11-04 11:25:48 -0800491
Simon Hunt934c3ce2014-11-05 11:45:07 -0800492 setRadio: function (btnSet) {
493 setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800494 },
495
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800496 setKeys: function (keyArg) {
497 setKeyBindings(keyArg);
498 },
499
Simon Hunt142d0032014-11-04 20:13:09 -0800500 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800501 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800502 },
503
504 // TODO : implement custom dialogs (don't use alerts)
505
506 dataLoadError: function (err, url) {
507 var msg = 'Data Load Error\n\n' +
508 err.status + ' -- ' + err.statusText + '\n\n' +
509 'relative-url: "' + url + '"\n\n' +
510 'complete-url: "' + err.responseURL + '"';
511 alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800512 }
Simon Hunt25248912014-11-04 11:25:48 -0800513
514 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800515 };
516
517 // attach instance methods to the view prototype
518 $.extend(View.prototype, viewInstanceMethods);
519
520 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800521 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800522
Simon Hunt25248912014-11-04 11:25:48 -0800523 uiApi = {
524 /** @api ui addView( vid, nid, cb )
525 * Adds a view to the UI.
526 * <p>
527 * Views are loaded/unloaded into the view content pane at
528 * appropriate times, by the navigation framework. This method
529 * adds a view to the UI and returns a token object representing
530 * the view. A view's token is always passed as the first
531 * argument to each of the view's life-cycle callback functions.
532 * <p>
533 * Note that if the view is directly referenced by a nav-item,
534 * or in a group of views with one of those views referenced by
535 * a nav-item, then the <i>nid</i> argument can be omitted as
536 * the framework can infer it.
537 * <p>
538 * <i>cb</i> is a plain object containing callback functions:
539 * "preload", "reset", "load", "unload", "resize", "error".
540 * <pre>
541 * function myLoad(view, ctx) { ... }
542 * ...
543 * // short form...
544 * onos.ui.addView('viewId', {
545 * load: myLoad
546 * });
547 * </pre>
548 *
549 * @param vid (string) [*] view ID (a unique DOM element id)
550 * @param nid (string) nav-item ID (a unique DOM element id)
551 * @param cb (object) [*] callbacks object
552 * @return the view token
553 */
554 addView: function (vid, nid, cb) {
555 traceFn('addView', vid);
556 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800557 token;
558 if (view.ok) {
559 views[vid] = view;
560 token = view.token();
561 } else {
562 token = { vid: view.vid, bad: true };
563 }
564 return token;
565 }
566 };
567
Simon Hunt25248912014-11-04 11:25:48 -0800568 // ..........................................................
569 // View API
570
571 viewApi = {
572 /** @api view empty( )
573 * Empties the current view.
574 * <p>
575 * More specifically, removes all DOM elements from the
576 * current view's display div.
577 */
578 empty: function () {
579 if (!current.view) {
580 return;
581 }
582 current.view.$div.html('');
583 }
584 };
585
586 // ..........................................................
587 // Nav API
588 navApi = {
589
590 };
591
592 // ..........................................................
593 // Exported API
594
Simon Hunt195cb382014-11-03 17:50:51 -0800595 // function to be called from index.html to build the ONOS UI
596 function buildOnosUi() {
597 tsB = new Date().getTime();
598 tsI = tsB - tsI; // initialization duration
599
600 console.log('ONOS UI initialized in ' + tsI + 'ms');
601
602 if (built) {
603 throwError("ONOS UI already built!");
604 }
605 built = true;
606
607 $view = d3.select('#view');
Simon Huntdb9eb072014-11-04 19:12:46 -0800608 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800609
610 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800611 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800612
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800613 d3.select('body').on('keydown', keyIn);
614
Simon Hunt195cb382014-11-03 17:50:51 -0800615 // Invoke hashchange callback to navigate to content
616 // indicated by the window location hash.
617 hash();
618
619 // If there were any build errors, report them
620 reportBuildErrors();
621 }
622
Simon Hunt195cb382014-11-03 17:50:51 -0800623 // export the api and build-UI function
624 return {
Simon Hunt25248912014-11-04 11:25:48 -0800625 ui: uiApi,
626 view: viewApi,
627 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800628 buildUi: buildOnosUi
629 };
630 };
631
Simon Huntdb9eb072014-11-04 19:12:46 -0800632}(jQuery));