blob: 6353a6e105729966222de8df22b63c51aec7e43c [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,
52 errorCount = 0;
53
54 // DOM elements etc.
Simon Huntdb9eb072014-11-04 19:12:46 -080055 var $view,
56 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080057
58
59 // ..........................................................
60 // Internal functions
61
62 // throw an error
63 function throwError(msg) {
64 // separate function, as we might add tracing here too, later
65 throw new Error(msg);
66 }
67
68 function doError(msg) {
69 errorCount++;
Simon Hunt25248912014-11-04 11:25:48 -080070 console.error(msg);
71 }
72
73 function trace(msg) {
74 if (settings.trace) {
75 console.log(msg);
76 }
77 }
78
79 function traceFn(fn, params) {
80 if (settings.trace) {
81 console.log('*FN* ' + fn + '(...): ' + params);
82 }
Simon Hunt195cb382014-11-03 17:50:51 -080083 }
84
85 // hash navigation
86 function hash() {
87 var hash = window.location.hash,
88 redo = false,
89 view,
90 t;
91
Simon Hunt25248912014-11-04 11:25:48 -080092 traceFn('hash', hash);
93
Simon Hunt195cb382014-11-03 17:50:51 -080094 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -080095 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -080096 redo = true;
97 }
98
99 t = parseHash(hash);
100 if (!t || !t.vid) {
101 doError('Unable to parse target hash: ' + hash);
102 }
103
104 view = views[t.vid];
105 if (!view) {
106 doError('No view defined with id: ' + t.vid);
107 }
108
109 if (redo) {
110 window.location.hash = makeHash(t);
111 // the above will result in a hashchange event, invoking
112 // this function again
113 } else {
114 // hash was not modified... navigate to where we need to be
115 navigate(hash, view, t);
116 }
Simon Hunt195cb382014-11-03 17:50:51 -0800117 }
118
119 function parseHash(s) {
120 // extract navigation coordinates from the supplied string
121 // "vid,ctx" --> { vid:vid, ctx:ctx }
Simon Hunt25248912014-11-04 11:25:48 -0800122 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800123
124 var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
125 if (m) {
126 return { vid: m[1], ctx: m[2] };
127 }
128
129 m = /^[#]{0,1}(\S+)$/.exec(s);
130 return m ? { vid: m[1] } : null;
131 }
132
133 function makeHash(t, ctx) {
Simon Hunt25248912014-11-04 11:25:48 -0800134 traceFn('makeHash');
Simon Hunt195cb382014-11-03 17:50:51 -0800135 // make a hash string from the given navigation coordinates.
136 // if t is not an object, then it is a vid
137 var h = t,
138 c = ctx || '';
139
140 if ($.isPlainObject(t)) {
141 h = t.vid;
142 c = t.ctx || '';
143 }
144
145 if (c) {
146 h += ',' + c;
147 }
Simon Hunt25248912014-11-04 11:25:48 -0800148 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800149 return h;
150 }
151
152 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800153 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800154 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800155 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800156 createView(view);
157 setView(view, hash, t);
158 }
159
160 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800161 traceFn('reportBuildErrors');
Simon Hunt195cb382014-11-03 17:50:51 -0800162 // TODO: validate registered views / nav-item linkage etc.
163 console.log('(no build errors)');
164 }
165
Simon Hunt25248912014-11-04 11:25:48 -0800166 // returns the reference if it is a function, null otherwise
167 function isF(f) {
168 return $.isFunction(f) ? f : null;
169 }
170
Simon Hunt195cb382014-11-03 17:50:51 -0800171 // ..........................................................
172 // View life-cycle functions
173
Simon Hunt25248912014-11-04 11:25:48 -0800174 function setViewDimensions(sel) {
175 var w = window.innerWidth,
176 h = window.innerHeight - mastHeight;
177 sel.each(function () {
178 $(this)
179 .css('width', w + 'px')
180 .css('height', h + 'px')
181 });
182 }
183
Simon Hunt195cb382014-11-03 17:50:51 -0800184 function createView(view) {
185 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800186
Simon Hunt195cb382014-11-03 17:50:51 -0800187 // lazy initialization of the view
188 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800189 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800190 $d = $view.append('div')
191 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800192 id: view.vid,
193 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800194 });
Simon Hunt25248912014-11-04 11:25:48 -0800195 setViewDimensions($d);
196 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800197 }
198 }
199
200 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800201 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800202 // set the specified view as current, while invoking the
203 // appropriate life-cycle callbacks
204
205 // if there is a current view, and it is not the same as
206 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800207 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800208 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800209 // detach radio buttons, if they were there..
210 $('#mastRadio').children().detach();
211
Simon Hunt195cb382014-11-03 17:50:51 -0800212 }
213
214 // cache new view and context
215 current.view = view;
216 current.ctx = t.ctx || '';
217
Simon Hunt195cb382014-11-03 17:50:51 -0800218 // preload is called only once, after the view is in the DOM
219 if (!view.preloaded) {
Simon Hunt25248912014-11-04 11:25:48 -0800220 view.preload(current.ctx);
221 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800222 }
223
224 // clear the view of stale data
225 view.reset();
226
227 // load the view
Simon Hunt25248912014-11-04 11:25:48 -0800228 view.load(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800229 }
230
Simon Huntdb9eb072014-11-04 19:12:46 -0800231 // generate 'unique' id by prefixing view id
232 function uid(view, id) {
233 return view.vid + '-' + id;
234 }
235
236 // restore id by removing view id prefix
237 function unUid(view, uid) {
238 var re = new RegExp('^' + view.vid + '-');
239 return uid.replace(re, '');
240 }
241
242 function setRadioButtons(vid, btnSet, callback) {
243 var view = views[vid],
244 btnG;
245
246 // lazily create the buttons...
247 if (!(btnG = view.radioButtons)) {
248 btnG = d3.select(document.createElement('div'));
249
250 btnSet.forEach(function (btn, i) {
251 var bid = btn.id || 'b' + i,
252 txt = btn.text || 'Button #' + i,
253 b = btnG.append('span')
254 .attr({
255 id: uid(view, bid),
256 class: 'radio'
257 })
258 .text(txt);
259 if (i === 0) {
260 b.classed('active', true);
261 }
262 });
263
264 btnG.selectAll('span')
265 .on('click', function (d) {
266 var btn = d3.select(this),
267 bid = btn.attr('id'),
268 act = btn.classed('active');
269
270 if (!act) {
271 $mastRadio.selectAll('span')
272 .classed('active', false);
273 btn.classed('active', true);
274
275 callback(view.token(), unUid(view, bid));
276 }
277 });
278
279 view.radioButtons = btnG;
280 }
281
282 // attach the buttons to the masthead
283 $mastRadio.node().appendChild(btnG.node());
284 }
285
Simon Hunt25248912014-11-04 11:25:48 -0800286 function resize(e) {
287 d3.selectAll('.onosView').call(setViewDimensions);
288 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800289 if (current.view) {
Simon Hunt25248912014-11-04 11:25:48 -0800290 current.view.resize(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800291 }
292 }
293
294 // ..........................................................
295 // View class
296 // Captures state information about a view.
297
298 // Constructor
299 // vid : view id
300 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800301 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800302 function View(vid) {
303 var av = 'addView(): ',
304 args = Array.prototype.slice.call(arguments),
305 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800306 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800307
308 args.shift(); // first arg is always vid
309 if (typeof args[0] === 'string') { // nid specified
310 nid = args.shift();
311 }
312 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800313
314 this.vid = vid;
315
316 if (validateViewArgs(vid)) {
317 this.nid = nid; // explicit navitem id (can be null)
318 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800319 this.$div = null; // view not yet added to DOM
320 this.radioButtons = null; // no radio buttons yet
321 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800322 }
323
324 }
325
326 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800327 var av = "ui.addView(...): ",
328 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800329 if (typeof vid !== 'string' || !vid) {
330 doError(av + 'vid required');
331 } else if (views[vid]) {
332 doError(av + 'View ID "' + vid + '" already exists');
333 } else {
334 ok = true;
335 }
336 return ok;
337 }
338
339 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800340 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800341 return {
Simon Hunt25248912014-11-04 11:25:48 -0800342 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800343 vid: this.vid,
344 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800345 $div: this.$div,
346
347 // functions
348 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800349 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800350 uid: this.uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800351 setRadio: this.setRadio
Simon Hunt195cb382014-11-03 17:50:51 -0800352 }
Simon Hunt25248912014-11-04 11:25:48 -0800353 },
354
355 preload: function (ctx) {
356 var c = ctx || '',
357 fn = isF(this.cb.preload);
358 traceFn('View.preload', this.vid + ', ' + c);
359 if (fn) {
360 trace('PRELOAD cb for ' + this.vid);
361 fn(this.token(), c);
362 }
363 },
364
365 reset: function () {
366 var fn = isF(this.cb.reset);
367 traceFn('View.reset', this.vid);
368 if (fn) {
369 trace('RESET cb for ' + this.vid);
370 fn(this.token());
371 } else if (this.cb.reset === true) {
372 // boolean true signifies "clear view"
373 trace(' [true] cleaing view...');
374 viewApi.empty();
375 }
376 },
377
378 load: function (ctx) {
379 var c = ctx || '',
380 fn = isF(this.cb.load);
381 traceFn('View.load', this.vid + ', ' + c);
382 this.$div.classed('currentView', true);
383 // TODO: add radio button set, if needed
384 if (fn) {
385 trace('LOAD cb for ' + this.vid);
386 fn(this.token(), c);
387 }
388 },
389
390 unload: function () {
391 var fn = isF(this.cb.unload);
392 traceFn('View.unload', this.vid);
393 this.$div.classed('currentView', false);
394 // TODO: remove radio button set, if needed
395 if (fn) {
396 trace('UNLOAD cb for ' + this.vid);
397 fn(this.token());
398 }
399 },
400
401 resize: function (ctx) {
402 var c = ctx || '',
403 fn = isF(this.cb.resize),
404 w = this.width(),
405 h = this.height();
406 traceFn('View.resize', this.vid + '/' + c +
407 ' [' + w + 'x' + h + ']');
408 if (fn) {
409 trace('RESIZE cb for ' + this.vid);
410 fn(this.token(), c);
411 }
412 },
413
414 error: function (ctx) {
415 var c = ctx || '',
416 fn = isF(this.cb.error);
417 traceFn('View.error', this.vid + ', ' + c);
418 if (fn) {
419 trace('ERROR cb for ' + this.vid);
420 fn(this.token(), c);
421 }
422 },
423
424 width: function () {
425 return $(this.$div.node()).width();
426 },
427
428 height: function () {
429 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800430 },
Simon Hunt25248912014-11-04 11:25:48 -0800431
Simon Huntdb9eb072014-11-04 19:12:46 -0800432 setRadio: function (btnSet, cb) {
433 setRadioButtons(this.vid, btnSet, cb);
Simon Hunt142d0032014-11-04 20:13:09 -0800434 },
435
436 uid: function (id) {
437 return uid(this, id);
Simon Huntdb9eb072014-11-04 19:12:46 -0800438 }
Simon Hunt25248912014-11-04 11:25:48 -0800439
440 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800441 };
442
443 // attach instance methods to the view prototype
444 $.extend(View.prototype, viewInstanceMethods);
445
446 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800447 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800448
Simon Hunt25248912014-11-04 11:25:48 -0800449 uiApi = {
450 /** @api ui addView( vid, nid, cb )
451 * Adds a view to the UI.
452 * <p>
453 * Views are loaded/unloaded into the view content pane at
454 * appropriate times, by the navigation framework. This method
455 * adds a view to the UI and returns a token object representing
456 * the view. A view's token is always passed as the first
457 * argument to each of the view's life-cycle callback functions.
458 * <p>
459 * Note that if the view is directly referenced by a nav-item,
460 * or in a group of views with one of those views referenced by
461 * a nav-item, then the <i>nid</i> argument can be omitted as
462 * the framework can infer it.
463 * <p>
464 * <i>cb</i> is a plain object containing callback functions:
465 * "preload", "reset", "load", "unload", "resize", "error".
466 * <pre>
467 * function myLoad(view, ctx) { ... }
468 * ...
469 * // short form...
470 * onos.ui.addView('viewId', {
471 * load: myLoad
472 * });
473 * </pre>
474 *
475 * @param vid (string) [*] view ID (a unique DOM element id)
476 * @param nid (string) nav-item ID (a unique DOM element id)
477 * @param cb (object) [*] callbacks object
478 * @return the view token
479 */
480 addView: function (vid, nid, cb) {
481 traceFn('addView', vid);
482 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800483 token;
484 if (view.ok) {
485 views[vid] = view;
486 token = view.token();
487 } else {
488 token = { vid: view.vid, bad: true };
489 }
490 return token;
491 }
492 };
493
Simon Hunt25248912014-11-04 11:25:48 -0800494 // ..........................................................
495 // View API
496
497 viewApi = {
498 /** @api view empty( )
499 * Empties the current view.
500 * <p>
501 * More specifically, removes all DOM elements from the
502 * current view's display div.
503 */
504 empty: function () {
505 if (!current.view) {
506 return;
507 }
508 current.view.$div.html('');
509 }
510 };
511
512 // ..........................................................
513 // Nav API
514 navApi = {
515
516 };
517
518 // ..........................................................
519 // Exported API
520
Simon Hunt195cb382014-11-03 17:50:51 -0800521 // function to be called from index.html to build the ONOS UI
522 function buildOnosUi() {
523 tsB = new Date().getTime();
524 tsI = tsB - tsI; // initialization duration
525
526 console.log('ONOS UI initialized in ' + tsI + 'ms');
527
528 if (built) {
529 throwError("ONOS UI already built!");
530 }
531 built = true;
532
533 $view = d3.select('#view');
Simon Huntdb9eb072014-11-04 19:12:46 -0800534 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800535
536 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800537 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800538
539 // Invoke hashchange callback to navigate to content
540 // indicated by the window location hash.
541 hash();
542
543 // If there were any build errors, report them
544 reportBuildErrors();
545 }
546
547
548 // export the api and build-UI function
549 return {
Simon Hunt25248912014-11-04 11:25:48 -0800550 ui: uiApi,
551 view: viewApi,
552 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800553 buildUi: buildOnosUi
554 };
555 };
556
Simon Huntdb9eb072014-11-04 19:12:46 -0800557}(jQuery));