blob: 85aa617c8d379ea6c2ac0332a743544df995e05c [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
28 defaultHash = '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 = {
38 trace: false
39 };
40
41 // compute runtime settings
42 var settings = $.extend({}, defaultOptions, options);
Simon Hunt195cb382014-11-03 17:50:51 -080043
44 // internal state
45 var views = {},
46 current = {
47 view: null,
48 ctx: ''
49 },
50 built = false,
51 errorCount = 0;
52
53 // DOM elements etc.
Simon Huntdb9eb072014-11-04 19:12:46 -080054 var $view,
55 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080056
57
58 // ..........................................................
59 // Internal functions
60
61 // throw an error
62 function throwError(msg) {
63 // separate function, as we might add tracing here too, later
64 throw new Error(msg);
65 }
66
67 function doError(msg) {
68 errorCount++;
Simon Hunt25248912014-11-04 11:25:48 -080069 console.error(msg);
70 }
71
72 function trace(msg) {
73 if (settings.trace) {
74 console.log(msg);
75 }
76 }
77
78 function traceFn(fn, params) {
79 if (settings.trace) {
80 console.log('*FN* ' + fn + '(...): ' + params);
81 }
Simon Hunt195cb382014-11-03 17:50:51 -080082 }
83
84 // hash navigation
85 function hash() {
86 var hash = window.location.hash,
87 redo = false,
88 view,
89 t;
90
Simon Hunt25248912014-11-04 11:25:48 -080091 traceFn('hash', hash);
92
Simon Hunt195cb382014-11-03 17:50:51 -080093 if (!hash) {
94 hash = defaultHash;
95 redo = true;
96 }
97
98 t = parseHash(hash);
99 if (!t || !t.vid) {
100 doError('Unable to parse target hash: ' + hash);
101 }
102
103 view = views[t.vid];
104 if (!view) {
105 doError('No view defined with id: ' + t.vid);
106 }
107
108 if (redo) {
109 window.location.hash = makeHash(t);
110 // the above will result in a hashchange event, invoking
111 // this function again
112 } else {
113 // hash was not modified... navigate to where we need to be
114 navigate(hash, view, t);
115 }
Simon Hunt195cb382014-11-03 17:50:51 -0800116 }
117
118 function parseHash(s) {
119 // extract navigation coordinates from the supplied string
120 // "vid,ctx" --> { vid:vid, ctx:ctx }
Simon Hunt25248912014-11-04 11:25:48 -0800121 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800122
123 var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
124 if (m) {
125 return { vid: m[1], ctx: m[2] };
126 }
127
128 m = /^[#]{0,1}(\S+)$/.exec(s);
129 return m ? { vid: m[1] } : null;
130 }
131
132 function makeHash(t, ctx) {
Simon Hunt25248912014-11-04 11:25:48 -0800133 traceFn('makeHash');
Simon Hunt195cb382014-11-03 17:50:51 -0800134 // make a hash string from the given navigation coordinates.
135 // if t is not an object, then it is a vid
136 var h = t,
137 c = ctx || '';
138
139 if ($.isPlainObject(t)) {
140 h = t.vid;
141 c = t.ctx || '';
142 }
143
144 if (c) {
145 h += ',' + c;
146 }
Simon Hunt25248912014-11-04 11:25:48 -0800147 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800148 return h;
149 }
150
151 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800152 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800153 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800154 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800155 createView(view);
156 setView(view, hash, t);
157 }
158
159 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800160 traceFn('reportBuildErrors');
Simon Hunt195cb382014-11-03 17:50:51 -0800161 // TODO: validate registered views / nav-item linkage etc.
162 console.log('(no build errors)');
163 }
164
Simon Hunt25248912014-11-04 11:25:48 -0800165 // returns the reference if it is a function, null otherwise
166 function isF(f) {
167 return $.isFunction(f) ? f : null;
168 }
169
Simon Hunt195cb382014-11-03 17:50:51 -0800170 // ..........................................................
171 // View life-cycle functions
172
Simon Hunt25248912014-11-04 11:25:48 -0800173 function setViewDimensions(sel) {
174 var w = window.innerWidth,
175 h = window.innerHeight - mastHeight;
176 sel.each(function () {
177 $(this)
178 .css('width', w + 'px')
179 .css('height', h + 'px')
180 });
181 }
182
Simon Hunt195cb382014-11-03 17:50:51 -0800183 function createView(view) {
184 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800185
Simon Hunt195cb382014-11-03 17:50:51 -0800186 // lazy initialization of the view
187 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800188 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800189 $d = $view.append('div')
190 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800191 id: view.vid,
192 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800193 });
Simon Hunt25248912014-11-04 11:25:48 -0800194 setViewDimensions($d);
195 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800196 }
197 }
198
199 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800200 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800201 // set the specified view as current, while invoking the
202 // appropriate life-cycle callbacks
203
204 // if there is a current view, and it is not the same as
205 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800206 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800207 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800208 // detach radio buttons, if they were there..
209 $('#mastRadio').children().detach();
210
Simon Hunt195cb382014-11-03 17:50:51 -0800211 }
212
213 // cache new view and context
214 current.view = view;
215 current.ctx = t.ctx || '';
216
Simon Hunt195cb382014-11-03 17:50:51 -0800217 // preload is called only once, after the view is in the DOM
218 if (!view.preloaded) {
Simon Hunt25248912014-11-04 11:25:48 -0800219 view.preload(current.ctx);
220 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800221 }
222
223 // clear the view of stale data
224 view.reset();
225
226 // load the view
Simon Hunt25248912014-11-04 11:25:48 -0800227 view.load(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800228 }
229
Simon Huntdb9eb072014-11-04 19:12:46 -0800230 // generate 'unique' id by prefixing view id
231 function uid(view, id) {
232 return view.vid + '-' + id;
233 }
234
235 // restore id by removing view id prefix
236 function unUid(view, uid) {
237 var re = new RegExp('^' + view.vid + '-');
238 return uid.replace(re, '');
239 }
240
241 function setRadioButtons(vid, btnSet, callback) {
242 var view = views[vid],
243 btnG;
244
245 // lazily create the buttons...
246 if (!(btnG = view.radioButtons)) {
247 btnG = d3.select(document.createElement('div'));
248
249 btnSet.forEach(function (btn, i) {
250 var bid = btn.id || 'b' + i,
251 txt = btn.text || 'Button #' + i,
252 b = btnG.append('span')
253 .attr({
254 id: uid(view, bid),
255 class: 'radio'
256 })
257 .text(txt);
258 if (i === 0) {
259 b.classed('active', true);
260 }
261 });
262
263 btnG.selectAll('span')
264 .on('click', function (d) {
265 var btn = d3.select(this),
266 bid = btn.attr('id'),
267 act = btn.classed('active');
268
269 if (!act) {
270 $mastRadio.selectAll('span')
271 .classed('active', false);
272 btn.classed('active', true);
273
274 callback(view.token(), unUid(view, bid));
275 }
276 });
277
278 view.radioButtons = btnG;
279 }
280
281 // attach the buttons to the masthead
282 $mastRadio.node().appendChild(btnG.node());
283 }
284
Simon Hunt25248912014-11-04 11:25:48 -0800285 function resize(e) {
286 d3.selectAll('.onosView').call(setViewDimensions);
287 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800288 if (current.view) {
Simon Hunt25248912014-11-04 11:25:48 -0800289 current.view.resize(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800290 }
291 }
292
293 // ..........................................................
294 // View class
295 // Captures state information about a view.
296
297 // Constructor
298 // vid : view id
299 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800300 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800301 function View(vid) {
302 var av = 'addView(): ',
303 args = Array.prototype.slice.call(arguments),
304 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800305 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800306
307 args.shift(); // first arg is always vid
308 if (typeof args[0] === 'string') { // nid specified
309 nid = args.shift();
310 }
311 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800312
313 this.vid = vid;
314
315 if (validateViewArgs(vid)) {
316 this.nid = nid; // explicit navitem id (can be null)
317 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800318 this.$div = null; // view not yet added to DOM
319 this.radioButtons = null; // no radio buttons yet
320 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800321 }
322
323 }
324
325 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800326 var av = "ui.addView(...): ",
327 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800328 if (typeof vid !== 'string' || !vid) {
329 doError(av + 'vid required');
330 } else if (views[vid]) {
331 doError(av + 'View ID "' + vid + '" already exists');
332 } else {
333 ok = true;
334 }
335 return ok;
336 }
337
338 var viewInstanceMethods = {
339 toString: function () {
340 return '[View: id="' + this.vid + '"]';
341 },
342
Simon Hunt25248912014-11-04 11:25:48 -0800343 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800344 return {
Simon Hunt25248912014-11-04 11:25:48 -0800345 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800346 vid: this.vid,
347 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800348 $div: this.$div,
349
350 // functions
351 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800352 height: this.height,
353 setRadio: this.setRadio
Simon Hunt195cb382014-11-03 17:50:51 -0800354 }
Simon Hunt25248912014-11-04 11:25:48 -0800355 },
356
357 preload: function (ctx) {
358 var c = ctx || '',
359 fn = isF(this.cb.preload);
360 traceFn('View.preload', this.vid + ', ' + c);
361 if (fn) {
362 trace('PRELOAD cb for ' + this.vid);
363 fn(this.token(), c);
364 }
365 },
366
367 reset: function () {
368 var fn = isF(this.cb.reset);
369 traceFn('View.reset', this.vid);
370 if (fn) {
371 trace('RESET cb for ' + this.vid);
372 fn(this.token());
373 } else if (this.cb.reset === true) {
374 // boolean true signifies "clear view"
375 trace(' [true] cleaing view...');
376 viewApi.empty();
377 }
378 },
379
380 load: function (ctx) {
381 var c = ctx || '',
382 fn = isF(this.cb.load);
383 traceFn('View.load', this.vid + ', ' + c);
384 this.$div.classed('currentView', true);
385 // TODO: add radio button set, if needed
386 if (fn) {
387 trace('LOAD cb for ' + this.vid);
388 fn(this.token(), c);
389 }
390 },
391
392 unload: function () {
393 var fn = isF(this.cb.unload);
394 traceFn('View.unload', this.vid);
395 this.$div.classed('currentView', false);
396 // TODO: remove radio button set, if needed
397 if (fn) {
398 trace('UNLOAD cb for ' + this.vid);
399 fn(this.token());
400 }
401 },
402
403 resize: function (ctx) {
404 var c = ctx || '',
405 fn = isF(this.cb.resize),
406 w = this.width(),
407 h = this.height();
408 traceFn('View.resize', this.vid + '/' + c +
409 ' [' + w + 'x' + h + ']');
410 if (fn) {
411 trace('RESIZE cb for ' + this.vid);
412 fn(this.token(), c);
413 }
414 },
415
416 error: function (ctx) {
417 var c = ctx || '',
418 fn = isF(this.cb.error);
419 traceFn('View.error', this.vid + ', ' + c);
420 if (fn) {
421 trace('ERROR cb for ' + this.vid);
422 fn(this.token(), c);
423 }
424 },
425
426 width: function () {
427 return $(this.$div.node()).width();
428 },
429
430 height: function () {
431 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800432 },
Simon Hunt25248912014-11-04 11:25:48 -0800433
Simon Huntdb9eb072014-11-04 19:12:46 -0800434 setRadio: function (btnSet, cb) {
435 setRadioButtons(this.vid, btnSet, cb);
436 }
Simon Hunt25248912014-11-04 11:25:48 -0800437
438 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800439 };
440
441 // attach instance methods to the view prototype
442 $.extend(View.prototype, viewInstanceMethods);
443
444 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800445 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800446
Simon Hunt25248912014-11-04 11:25:48 -0800447 uiApi = {
448 /** @api ui addView( vid, nid, cb )
449 * Adds a view to the UI.
450 * <p>
451 * Views are loaded/unloaded into the view content pane at
452 * appropriate times, by the navigation framework. This method
453 * adds a view to the UI and returns a token object representing
454 * the view. A view's token is always passed as the first
455 * argument to each of the view's life-cycle callback functions.
456 * <p>
457 * Note that if the view is directly referenced by a nav-item,
458 * or in a group of views with one of those views referenced by
459 * a nav-item, then the <i>nid</i> argument can be omitted as
460 * the framework can infer it.
461 * <p>
462 * <i>cb</i> is a plain object containing callback functions:
463 * "preload", "reset", "load", "unload", "resize", "error".
464 * <pre>
465 * function myLoad(view, ctx) { ... }
466 * ...
467 * // short form...
468 * onos.ui.addView('viewId', {
469 * load: myLoad
470 * });
471 * </pre>
472 *
473 * @param vid (string) [*] view ID (a unique DOM element id)
474 * @param nid (string) nav-item ID (a unique DOM element id)
475 * @param cb (object) [*] callbacks object
476 * @return the view token
477 */
478 addView: function (vid, nid, cb) {
479 traceFn('addView', vid);
480 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800481 token;
482 if (view.ok) {
483 views[vid] = view;
484 token = view.token();
485 } else {
486 token = { vid: view.vid, bad: true };
487 }
488 return token;
489 }
490 };
491
Simon Hunt25248912014-11-04 11:25:48 -0800492 // ..........................................................
493 // View API
494
495 viewApi = {
496 /** @api view empty( )
497 * Empties the current view.
498 * <p>
499 * More specifically, removes all DOM elements from the
500 * current view's display div.
501 */
502 empty: function () {
503 if (!current.view) {
504 return;
505 }
506 current.view.$div.html('');
507 }
508 };
509
510 // ..........................................................
511 // Nav API
512 navApi = {
513
514 };
515
516 // ..........................................................
517 // Exported API
518
Simon Hunt195cb382014-11-03 17:50:51 -0800519 // function to be called from index.html to build the ONOS UI
520 function buildOnosUi() {
521 tsB = new Date().getTime();
522 tsI = tsB - tsI; // initialization duration
523
524 console.log('ONOS UI initialized in ' + tsI + 'ms');
525
526 if (built) {
527 throwError("ONOS UI already built!");
528 }
529 built = true;
530
531 $view = d3.select('#view');
Simon Huntdb9eb072014-11-04 19:12:46 -0800532 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800533
534 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800535 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800536
537 // Invoke hashchange callback to navigate to content
538 // indicated by the window location hash.
539 hash();
540
541 // If there were any build errors, report them
542 reportBuildErrors();
543 }
544
545
546 // export the api and build-UI function
547 return {
Simon Hunt25248912014-11-04 11:25:48 -0800548 ui: uiApi,
549 view: viewApi,
550 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800551 buildUi: buildOnosUi
552 };
553 };
554
Simon Huntdb9eb072014-11-04 19:12:46 -0800555}(jQuery));