blob: 427a23ff8962a70f1f7af2f860a881a164252a1b [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,
410 setKeys: this.setKeys
Simon Hunt195cb382014-11-03 17:50:51 -0800411 }
Simon Hunt25248912014-11-04 11:25:48 -0800412 },
413
414 preload: function (ctx) {
415 var c = ctx || '',
416 fn = isF(this.cb.preload);
417 traceFn('View.preload', this.vid + ', ' + c);
418 if (fn) {
419 trace('PRELOAD cb for ' + this.vid);
420 fn(this.token(), c);
421 }
422 },
423
424 reset: function () {
425 var fn = isF(this.cb.reset);
426 traceFn('View.reset', this.vid);
427 if (fn) {
428 trace('RESET cb for ' + this.vid);
429 fn(this.token());
430 } else if (this.cb.reset === true) {
431 // boolean true signifies "clear view"
432 trace(' [true] cleaing view...');
433 viewApi.empty();
434 }
435 },
436
437 load: function (ctx) {
438 var c = ctx || '',
439 fn = isF(this.cb.load);
440 traceFn('View.load', this.vid + ', ' + c);
441 this.$div.classed('currentView', true);
442 // TODO: add radio button set, if needed
443 if (fn) {
444 trace('LOAD cb for ' + this.vid);
445 fn(this.token(), c);
446 }
447 },
448
449 unload: function () {
450 var fn = isF(this.cb.unload);
451 traceFn('View.unload', this.vid);
452 this.$div.classed('currentView', false);
453 // TODO: remove radio button set, if needed
454 if (fn) {
455 trace('UNLOAD cb for ' + this.vid);
456 fn(this.token());
457 }
458 },
459
460 resize: function (ctx) {
461 var c = ctx || '',
462 fn = isF(this.cb.resize),
463 w = this.width(),
464 h = this.height();
465 traceFn('View.resize', this.vid + '/' + c +
466 ' [' + w + 'x' + h + ']');
467 if (fn) {
468 trace('RESIZE cb for ' + this.vid);
469 fn(this.token(), c);
470 }
471 },
472
473 error: function (ctx) {
474 var c = ctx || '',
475 fn = isF(this.cb.error);
476 traceFn('View.error', this.vid + ', ' + c);
477 if (fn) {
478 trace('ERROR cb for ' + this.vid);
479 fn(this.token(), c);
480 }
481 },
482
483 width: function () {
484 return $(this.$div.node()).width();
485 },
486
487 height: function () {
488 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800489 },
Simon Hunt25248912014-11-04 11:25:48 -0800490
Simon Hunt934c3ce2014-11-05 11:45:07 -0800491 setRadio: function (btnSet) {
492 setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800493 },
494
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800495 setKeys: function (keyArg) {
496 setKeyBindings(keyArg);
497 },
498
Simon Hunt142d0032014-11-04 20:13:09 -0800499 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800500 return makeUid(this, id);
Simon Huntdb9eb072014-11-04 19:12:46 -0800501 }
Simon Hunt25248912014-11-04 11:25:48 -0800502
503 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800504 };
505
506 // attach instance methods to the view prototype
507 $.extend(View.prototype, viewInstanceMethods);
508
509 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800510 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800511
Simon Hunt25248912014-11-04 11:25:48 -0800512 uiApi = {
513 /** @api ui addView( vid, nid, cb )
514 * Adds a view to the UI.
515 * <p>
516 * Views are loaded/unloaded into the view content pane at
517 * appropriate times, by the navigation framework. This method
518 * adds a view to the UI and returns a token object representing
519 * the view. A view's token is always passed as the first
520 * argument to each of the view's life-cycle callback functions.
521 * <p>
522 * Note that if the view is directly referenced by a nav-item,
523 * or in a group of views with one of those views referenced by
524 * a nav-item, then the <i>nid</i> argument can be omitted as
525 * the framework can infer it.
526 * <p>
527 * <i>cb</i> is a plain object containing callback functions:
528 * "preload", "reset", "load", "unload", "resize", "error".
529 * <pre>
530 * function myLoad(view, ctx) { ... }
531 * ...
532 * // short form...
533 * onos.ui.addView('viewId', {
534 * load: myLoad
535 * });
536 * </pre>
537 *
538 * @param vid (string) [*] view ID (a unique DOM element id)
539 * @param nid (string) nav-item ID (a unique DOM element id)
540 * @param cb (object) [*] callbacks object
541 * @return the view token
542 */
543 addView: function (vid, nid, cb) {
544 traceFn('addView', vid);
545 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800546 token;
547 if (view.ok) {
548 views[vid] = view;
549 token = view.token();
550 } else {
551 token = { vid: view.vid, bad: true };
552 }
553 return token;
554 }
555 };
556
Simon Hunt25248912014-11-04 11:25:48 -0800557 // ..........................................................
558 // View API
559
560 viewApi = {
561 /** @api view empty( )
562 * Empties the current view.
563 * <p>
564 * More specifically, removes all DOM elements from the
565 * current view's display div.
566 */
567 empty: function () {
568 if (!current.view) {
569 return;
570 }
571 current.view.$div.html('');
572 }
573 };
574
575 // ..........................................................
576 // Nav API
577 navApi = {
578
579 };
580
581 // ..........................................................
582 // Exported API
583
Simon Hunt195cb382014-11-03 17:50:51 -0800584 // function to be called from index.html to build the ONOS UI
585 function buildOnosUi() {
586 tsB = new Date().getTime();
587 tsI = tsB - tsI; // initialization duration
588
589 console.log('ONOS UI initialized in ' + tsI + 'ms');
590
591 if (built) {
592 throwError("ONOS UI already built!");
593 }
594 built = true;
595
596 $view = d3.select('#view');
Simon Huntdb9eb072014-11-04 19:12:46 -0800597 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800598
599 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800600 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800601
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800602 d3.select('body').on('keydown', keyIn);
603
Simon Hunt195cb382014-11-03 17:50:51 -0800604 // Invoke hashchange callback to navigate to content
605 // indicated by the window location hash.
606 hash();
607
608 // If there were any build errors, report them
609 reportBuildErrors();
610 }
611
Simon Hunt195cb382014-11-03 17:50:51 -0800612 // export the api and build-UI function
613 return {
Simon Hunt25248912014-11-04 11:25:48 -0800614 ui: uiApi,
615 view: viewApi,
616 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800617 buildUi: buildOnosUi
618 };
619 };
620
Simon Huntdb9eb072014-11-04 19:12:46 -0800621}(jQuery));