blob: f509757068c10af891fd75e3093b2b7057a8ae34 [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,
53 keyHandler = {};
Simon Hunt195cb382014-11-03 17:50:51 -080054
55 // DOM elements etc.
Simon Huntdb9eb072014-11-04 19:12:46 -080056 var $view,
57 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080058
59
Simon Hunt0df1b1d2014-11-04 22:58:29 -080060 function whatKey(code) {
61 switch (code) {
62 case 13: return 'enter';
63 case 16: return 'shift';
64 case 17: return 'ctrl';
65 case 18: return 'alt';
66 case 27: return 'esc';
67 case 32: return 'space';
68 case 37: return 'leftArrow';
69 case 38: return 'upArrow';
70 case 39: return 'rightArrow';
71 case 40: return 'downArrow';
72 case 91: return 'cmdLeft';
73 case 93: return 'cmdRight';
74 default:
75 if ((code >= 48 && code <= 57) ||
76 (code >= 65 && code <= 90)) {
77 return String.fromCharCode(code);
78 } else if (code >= 112 && code <= 123) {
79 return 'F' + (code - 111);
80 }
81 return '.';
82 }
83 }
84
85
Simon Hunt195cb382014-11-03 17:50:51 -080086 // ..........................................................
87 // Internal functions
88
89 // throw an error
90 function throwError(msg) {
91 // separate function, as we might add tracing here too, later
92 throw new Error(msg);
93 }
94
95 function doError(msg) {
96 errorCount++;
Simon Hunt25248912014-11-04 11:25:48 -080097 console.error(msg);
98 }
99
100 function trace(msg) {
101 if (settings.trace) {
102 console.log(msg);
103 }
104 }
105
106 function traceFn(fn, params) {
107 if (settings.trace) {
108 console.log('*FN* ' + fn + '(...): ' + params);
109 }
Simon Hunt195cb382014-11-03 17:50:51 -0800110 }
111
112 // hash navigation
113 function hash() {
114 var hash = window.location.hash,
115 redo = false,
116 view,
117 t;
118
Simon Hunt25248912014-11-04 11:25:48 -0800119 traceFn('hash', hash);
120
Simon Hunt195cb382014-11-03 17:50:51 -0800121 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800122 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800123 redo = true;
124 }
125
126 t = parseHash(hash);
127 if (!t || !t.vid) {
128 doError('Unable to parse target hash: ' + hash);
129 }
130
131 view = views[t.vid];
132 if (!view) {
133 doError('No view defined with id: ' + t.vid);
134 }
135
136 if (redo) {
137 window.location.hash = makeHash(t);
138 // the above will result in a hashchange event, invoking
139 // this function again
140 } else {
141 // hash was not modified... navigate to where we need to be
142 navigate(hash, view, t);
143 }
Simon Hunt195cb382014-11-03 17:50:51 -0800144 }
145
146 function parseHash(s) {
147 // extract navigation coordinates from the supplied string
148 // "vid,ctx" --> { vid:vid, ctx:ctx }
Simon Hunt25248912014-11-04 11:25:48 -0800149 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800150
151 var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
152 if (m) {
153 return { vid: m[1], ctx: m[2] };
154 }
155
156 m = /^[#]{0,1}(\S+)$/.exec(s);
157 return m ? { vid: m[1] } : null;
158 }
159
160 function makeHash(t, ctx) {
Simon Hunt25248912014-11-04 11:25:48 -0800161 traceFn('makeHash');
Simon Hunt195cb382014-11-03 17:50:51 -0800162 // make a hash string from the given navigation coordinates.
163 // if t is not an object, then it is a vid
164 var h = t,
165 c = ctx || '';
166
167 if ($.isPlainObject(t)) {
168 h = t.vid;
169 c = t.ctx || '';
170 }
171
172 if (c) {
173 h += ',' + c;
174 }
Simon Hunt25248912014-11-04 11:25:48 -0800175 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800176 return h;
177 }
178
179 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800180 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800181 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800182 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800183 createView(view);
184 setView(view, hash, t);
185 }
186
187 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800188 traceFn('reportBuildErrors');
Simon Hunt195cb382014-11-03 17:50:51 -0800189 // TODO: validate registered views / nav-item linkage etc.
190 console.log('(no build errors)');
191 }
192
Simon Hunt25248912014-11-04 11:25:48 -0800193 // returns the reference if it is a function, null otherwise
194 function isF(f) {
195 return $.isFunction(f) ? f : null;
196 }
197
Simon Hunt195cb382014-11-03 17:50:51 -0800198 // ..........................................................
199 // View life-cycle functions
200
Simon Hunt25248912014-11-04 11:25:48 -0800201 function setViewDimensions(sel) {
202 var w = window.innerWidth,
203 h = window.innerHeight - mastHeight;
204 sel.each(function () {
205 $(this)
206 .css('width', w + 'px')
207 .css('height', h + 'px')
208 });
209 }
210
Simon Hunt195cb382014-11-03 17:50:51 -0800211 function createView(view) {
212 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800213
Simon Hunt195cb382014-11-03 17:50:51 -0800214 // lazy initialization of the view
215 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800216 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800217 $d = $view.append('div')
218 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800219 id: view.vid,
220 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800221 });
Simon Hunt25248912014-11-04 11:25:48 -0800222 setViewDimensions($d);
223 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800224 }
225 }
226
227 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800228 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800229 // set the specified view as current, while invoking the
230 // appropriate life-cycle callbacks
231
232 // if there is a current view, and it is not the same as
233 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800234 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800235 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800236
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800237 // detach radio buttons, key handlers, etc.
238 $('#mastRadio').children().detach();
239 keyHandler.fn = null;
240 keyHandler.map = {};
Simon Hunt195cb382014-11-03 17:50:51 -0800241 }
242
243 // cache new view and context
244 current.view = view;
245 current.ctx = t.ctx || '';
246
Simon Hunt195cb382014-11-03 17:50:51 -0800247 // preload is called only once, after the view is in the DOM
248 if (!view.preloaded) {
Simon Hunt25248912014-11-04 11:25:48 -0800249 view.preload(current.ctx);
250 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800251 }
252
253 // clear the view of stale data
254 view.reset();
255
256 // load the view
Simon Hunt25248912014-11-04 11:25:48 -0800257 view.load(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800258 }
259
Simon Huntdb9eb072014-11-04 19:12:46 -0800260 // generate 'unique' id by prefixing view id
261 function uid(view, id) {
262 return view.vid + '-' + id;
263 }
264
265 // restore id by removing view id prefix
266 function unUid(view, uid) {
267 var re = new RegExp('^' + view.vid + '-');
268 return uid.replace(re, '');
269 }
270
271 function setRadioButtons(vid, btnSet, callback) {
272 var view = views[vid],
273 btnG;
274
275 // lazily create the buttons...
276 if (!(btnG = view.radioButtons)) {
277 btnG = d3.select(document.createElement('div'));
278
279 btnSet.forEach(function (btn, i) {
280 var bid = btn.id || 'b' + i,
281 txt = btn.text || 'Button #' + i,
282 b = btnG.append('span')
283 .attr({
284 id: uid(view, bid),
285 class: 'radio'
286 })
287 .text(txt);
288 if (i === 0) {
289 b.classed('active', true);
290 }
291 });
292
293 btnG.selectAll('span')
294 .on('click', function (d) {
295 var btn = d3.select(this),
296 bid = btn.attr('id'),
297 act = btn.classed('active');
298
299 if (!act) {
300 $mastRadio.selectAll('span')
301 .classed('active', false);
302 btn.classed('active', true);
303
304 callback(view.token(), unUid(view, bid));
305 }
306 });
307
308 view.radioButtons = btnG;
309 }
310
311 // attach the buttons to the masthead
312 $mastRadio.node().appendChild(btnG.node());
313 }
314
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800315 function setKeyBindings(keyArg) {
316 if ($.isFunction(keyArg)) {
317 // set general key handler callback
318 keyHandler.fn = keyArg;
319 } else {
320 // set specific key filter map
321 keyHandler.map = keyArg;
322 }
323 }
324
325 function keyIn() {
326 var event = d3.event,
327 keyCode = event.keyCode,
328 key = whatKey(keyCode),
329 cb = isF(keyHandler.map[key]) || isF(keyHandler.fn);
330
331 if (cb) {
332 cb(current.view.token(), key, keyCode, event);
333 }
334 }
335
Simon Hunt25248912014-11-04 11:25:48 -0800336 function resize(e) {
337 d3.selectAll('.onosView').call(setViewDimensions);
338 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800339 if (current.view) {
Simon Hunt25248912014-11-04 11:25:48 -0800340 current.view.resize(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800341 }
342 }
343
344 // ..........................................................
345 // View class
346 // Captures state information about a view.
347
348 // Constructor
349 // vid : view id
350 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800351 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800352 function View(vid) {
353 var av = 'addView(): ',
354 args = Array.prototype.slice.call(arguments),
355 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800356 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800357
358 args.shift(); // first arg is always vid
359 if (typeof args[0] === 'string') { // nid specified
360 nid = args.shift();
361 }
362 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800363
364 this.vid = vid;
365
366 if (validateViewArgs(vid)) {
367 this.nid = nid; // explicit navitem id (can be null)
368 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800369 this.$div = null; // view not yet added to DOM
370 this.radioButtons = null; // no radio buttons yet
371 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800372 }
Simon Hunt195cb382014-11-03 17:50:51 -0800373 }
374
375 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800376 var av = "ui.addView(...): ",
377 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800378 if (typeof vid !== 'string' || !vid) {
379 doError(av + 'vid required');
380 } else if (views[vid]) {
381 doError(av + 'View ID "' + vid + '" already exists');
382 } else {
383 ok = true;
384 }
385 return ok;
386 }
387
388 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800389 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800390 return {
Simon Hunt25248912014-11-04 11:25:48 -0800391 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800392 vid: this.vid,
393 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800394 $div: this.$div,
395
396 // functions
397 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800398 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800399 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800400 setRadio: this.setRadio,
401 setKeys: this.setKeys
Simon Hunt195cb382014-11-03 17:50:51 -0800402 }
Simon Hunt25248912014-11-04 11:25:48 -0800403 },
404
405 preload: function (ctx) {
406 var c = ctx || '',
407 fn = isF(this.cb.preload);
408 traceFn('View.preload', this.vid + ', ' + c);
409 if (fn) {
410 trace('PRELOAD cb for ' + this.vid);
411 fn(this.token(), c);
412 }
413 },
414
415 reset: function () {
416 var fn = isF(this.cb.reset);
417 traceFn('View.reset', this.vid);
418 if (fn) {
419 trace('RESET cb for ' + this.vid);
420 fn(this.token());
421 } else if (this.cb.reset === true) {
422 // boolean true signifies "clear view"
423 trace(' [true] cleaing view...');
424 viewApi.empty();
425 }
426 },
427
428 load: function (ctx) {
429 var c = ctx || '',
430 fn = isF(this.cb.load);
431 traceFn('View.load', this.vid + ', ' + c);
432 this.$div.classed('currentView', true);
433 // TODO: add radio button set, if needed
434 if (fn) {
435 trace('LOAD cb for ' + this.vid);
436 fn(this.token(), c);
437 }
438 },
439
440 unload: function () {
441 var fn = isF(this.cb.unload);
442 traceFn('View.unload', this.vid);
443 this.$div.classed('currentView', false);
444 // TODO: remove radio button set, if needed
445 if (fn) {
446 trace('UNLOAD cb for ' + this.vid);
447 fn(this.token());
448 }
449 },
450
451 resize: function (ctx) {
452 var c = ctx || '',
453 fn = isF(this.cb.resize),
454 w = this.width(),
455 h = this.height();
456 traceFn('View.resize', this.vid + '/' + c +
457 ' [' + w + 'x' + h + ']');
458 if (fn) {
459 trace('RESIZE cb for ' + this.vid);
460 fn(this.token(), c);
461 }
462 },
463
464 error: function (ctx) {
465 var c = ctx || '',
466 fn = isF(this.cb.error);
467 traceFn('View.error', this.vid + ', ' + c);
468 if (fn) {
469 trace('ERROR cb for ' + this.vid);
470 fn(this.token(), c);
471 }
472 },
473
474 width: function () {
475 return $(this.$div.node()).width();
476 },
477
478 height: function () {
479 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800480 },
Simon Hunt25248912014-11-04 11:25:48 -0800481
Simon Huntdb9eb072014-11-04 19:12:46 -0800482 setRadio: function (btnSet, cb) {
483 setRadioButtons(this.vid, btnSet, cb);
Simon Hunt142d0032014-11-04 20:13:09 -0800484 },
485
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800486 setKeys: function (keyArg) {
487 setKeyBindings(keyArg);
488 },
489
Simon Hunt142d0032014-11-04 20:13:09 -0800490 uid: function (id) {
491 return uid(this, id);
Simon Huntdb9eb072014-11-04 19:12:46 -0800492 }
Simon Hunt25248912014-11-04 11:25:48 -0800493
494 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800495 };
496
497 // attach instance methods to the view prototype
498 $.extend(View.prototype, viewInstanceMethods);
499
500 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800501 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800502
Simon Hunt25248912014-11-04 11:25:48 -0800503 uiApi = {
504 /** @api ui addView( vid, nid, cb )
505 * Adds a view to the UI.
506 * <p>
507 * Views are loaded/unloaded into the view content pane at
508 * appropriate times, by the navigation framework. This method
509 * adds a view to the UI and returns a token object representing
510 * the view. A view's token is always passed as the first
511 * argument to each of the view's life-cycle callback functions.
512 * <p>
513 * Note that if the view is directly referenced by a nav-item,
514 * or in a group of views with one of those views referenced by
515 * a nav-item, then the <i>nid</i> argument can be omitted as
516 * the framework can infer it.
517 * <p>
518 * <i>cb</i> is a plain object containing callback functions:
519 * "preload", "reset", "load", "unload", "resize", "error".
520 * <pre>
521 * function myLoad(view, ctx) { ... }
522 * ...
523 * // short form...
524 * onos.ui.addView('viewId', {
525 * load: myLoad
526 * });
527 * </pre>
528 *
529 * @param vid (string) [*] view ID (a unique DOM element id)
530 * @param nid (string) nav-item ID (a unique DOM element id)
531 * @param cb (object) [*] callbacks object
532 * @return the view token
533 */
534 addView: function (vid, nid, cb) {
535 traceFn('addView', vid);
536 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800537 token;
538 if (view.ok) {
539 views[vid] = view;
540 token = view.token();
541 } else {
542 token = { vid: view.vid, bad: true };
543 }
544 return token;
545 }
546 };
547
Simon Hunt25248912014-11-04 11:25:48 -0800548 // ..........................................................
549 // View API
550
551 viewApi = {
552 /** @api view empty( )
553 * Empties the current view.
554 * <p>
555 * More specifically, removes all DOM elements from the
556 * current view's display div.
557 */
558 empty: function () {
559 if (!current.view) {
560 return;
561 }
562 current.view.$div.html('');
563 }
564 };
565
566 // ..........................................................
567 // Nav API
568 navApi = {
569
570 };
571
572 // ..........................................................
573 // Exported API
574
Simon Hunt195cb382014-11-03 17:50:51 -0800575 // function to be called from index.html to build the ONOS UI
576 function buildOnosUi() {
577 tsB = new Date().getTime();
578 tsI = tsB - tsI; // initialization duration
579
580 console.log('ONOS UI initialized in ' + tsI + 'ms');
581
582 if (built) {
583 throwError("ONOS UI already built!");
584 }
585 built = true;
586
587 $view = d3.select('#view');
Simon Huntdb9eb072014-11-04 19:12:46 -0800588 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800589
590 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800591 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800592
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800593 d3.select('body').on('keydown', keyIn);
594
Simon Hunt195cb382014-11-03 17:50:51 -0800595 // Invoke hashchange callback to navigate to content
596 // indicated by the window location hash.
597 hash();
598
599 // If there were any build errors, report them
600 reportBuildErrors();
601 }
602
Simon Hunt195cb382014-11-03 17:50:51 -0800603 // export the api and build-UI function
604 return {
Simon Hunt25248912014-11-04 11:25:48 -0800605 ui: uiApi,
606 view: viewApi,
607 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800608 buildUi: buildOnosUi
609 };
610 };
611
Simon Huntdb9eb072014-11-04 19:12:46 -0800612}(jQuery));