blob: c35b56d3b19ee2bf78423fb5e93ec76106b25718 [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.
54 var $view;
55
56
57 // ..........................................................
58 // Internal functions
59
60 // throw an error
61 function throwError(msg) {
62 // separate function, as we might add tracing here too, later
63 throw new Error(msg);
64 }
65
66 function doError(msg) {
67 errorCount++;
Simon Hunt25248912014-11-04 11:25:48 -080068 console.error(msg);
69 }
70
71 function trace(msg) {
72 if (settings.trace) {
73 console.log(msg);
74 }
75 }
76
77 function traceFn(fn, params) {
78 if (settings.trace) {
79 console.log('*FN* ' + fn + '(...): ' + params);
80 }
Simon Hunt195cb382014-11-03 17:50:51 -080081 }
82
83 // hash navigation
84 function hash() {
85 var hash = window.location.hash,
86 redo = false,
87 view,
88 t;
89
Simon Hunt25248912014-11-04 11:25:48 -080090 traceFn('hash', hash);
91
Simon Hunt195cb382014-11-03 17:50:51 -080092 if (!hash) {
93 hash = defaultHash;
94 redo = true;
95 }
96
97 t = parseHash(hash);
98 if (!t || !t.vid) {
99 doError('Unable to parse target hash: ' + hash);
100 }
101
102 view = views[t.vid];
103 if (!view) {
104 doError('No view defined with id: ' + t.vid);
105 }
106
107 if (redo) {
108 window.location.hash = makeHash(t);
109 // the above will result in a hashchange event, invoking
110 // this function again
111 } else {
112 // hash was not modified... navigate to where we need to be
113 navigate(hash, view, t);
114 }
Simon Hunt195cb382014-11-03 17:50:51 -0800115 }
116
117 function parseHash(s) {
118 // extract navigation coordinates from the supplied string
119 // "vid,ctx" --> { vid:vid, ctx:ctx }
Simon Hunt25248912014-11-04 11:25:48 -0800120 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800121
122 var m = /^[#]{0,1}(\S+),(\S*)$/.exec(s);
123 if (m) {
124 return { vid: m[1], ctx: m[2] };
125 }
126
127 m = /^[#]{0,1}(\S+)$/.exec(s);
128 return m ? { vid: m[1] } : null;
129 }
130
131 function makeHash(t, ctx) {
Simon Hunt25248912014-11-04 11:25:48 -0800132 traceFn('makeHash');
Simon Hunt195cb382014-11-03 17:50:51 -0800133 // make a hash string from the given navigation coordinates.
134 // if t is not an object, then it is a vid
135 var h = t,
136 c = ctx || '';
137
138 if ($.isPlainObject(t)) {
139 h = t.vid;
140 c = t.ctx || '';
141 }
142
143 if (c) {
144 h += ',' + c;
145 }
Simon Hunt25248912014-11-04 11:25:48 -0800146 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800147 return h;
148 }
149
150 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800151 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800152 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800153 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800154 createView(view);
155 setView(view, hash, t);
156 }
157
158 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800159 traceFn('reportBuildErrors');
Simon Hunt195cb382014-11-03 17:50:51 -0800160 // TODO: validate registered views / nav-item linkage etc.
161 console.log('(no build errors)');
162 }
163
Simon Hunt25248912014-11-04 11:25:48 -0800164 // returns the reference if it is a function, null otherwise
165 function isF(f) {
166 return $.isFunction(f) ? f : null;
167 }
168
Simon Hunt195cb382014-11-03 17:50:51 -0800169 // ..........................................................
170 // View life-cycle functions
171
Simon Hunt25248912014-11-04 11:25:48 -0800172 function setViewDimensions(sel) {
173 var w = window.innerWidth,
174 h = window.innerHeight - mastHeight;
175 sel.each(function () {
176 $(this)
177 .css('width', w + 'px')
178 .css('height', h + 'px')
179 });
180 }
181
Simon Hunt195cb382014-11-03 17:50:51 -0800182 function createView(view) {
183 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800184
Simon Hunt195cb382014-11-03 17:50:51 -0800185 // lazy initialization of the view
186 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800187 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800188 $d = $view.append('div')
189 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800190 id: view.vid,
191 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800192 });
Simon Hunt25248912014-11-04 11:25:48 -0800193 setViewDimensions($d);
194 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800195 }
196 }
197
198 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800199 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800200 // set the specified view as current, while invoking the
201 // appropriate life-cycle callbacks
202
203 // if there is a current view, and it is not the same as
204 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800205 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800206 current.view.unload();
207 }
208
209 // cache new view and context
210 current.view = view;
211 current.ctx = t.ctx || '';
212
Simon Hunt195cb382014-11-03 17:50:51 -0800213 // preload is called only once, after the view is in the DOM
214 if (!view.preloaded) {
Simon Hunt25248912014-11-04 11:25:48 -0800215 view.preload(current.ctx);
216 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800217 }
218
219 // clear the view of stale data
220 view.reset();
221
222 // load the view
Simon Hunt25248912014-11-04 11:25:48 -0800223 view.load(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800224 }
225
Simon Hunt25248912014-11-04 11:25:48 -0800226 function resize(e) {
227 d3.selectAll('.onosView').call(setViewDimensions);
228 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800229 if (current.view) {
Simon Hunt25248912014-11-04 11:25:48 -0800230 current.view.resize(current.ctx);
Simon Hunt195cb382014-11-03 17:50:51 -0800231 }
232 }
233
234 // ..........................................................
235 // View class
236 // Captures state information about a view.
237
238 // Constructor
239 // vid : view id
240 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800241 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800242 function View(vid) {
243 var av = 'addView(): ',
244 args = Array.prototype.slice.call(arguments),
245 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800246 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800247
248 args.shift(); // first arg is always vid
249 if (typeof args[0] === 'string') { // nid specified
250 nid = args.shift();
251 }
252 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800253
254 this.vid = vid;
255
256 if (validateViewArgs(vid)) {
257 this.nid = nid; // explicit navitem id (can be null)
258 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Hunt195cb382014-11-03 17:50:51 -0800259 this.$div = null; // view not yet added to DOM
260 this.ok = true; // valid view
261 }
262
263 }
264
265 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800266 var av = "ui.addView(...): ",
267 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800268 if (typeof vid !== 'string' || !vid) {
269 doError(av + 'vid required');
270 } else if (views[vid]) {
271 doError(av + 'View ID "' + vid + '" already exists');
272 } else {
273 ok = true;
274 }
275 return ok;
276 }
277
278 var viewInstanceMethods = {
279 toString: function () {
280 return '[View: id="' + this.vid + '"]';
281 },
282
Simon Hunt25248912014-11-04 11:25:48 -0800283 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800284 return {
Simon Hunt25248912014-11-04 11:25:48 -0800285 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800286 vid: this.vid,
287 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800288 $div: this.$div,
289
290 // functions
291 width: this.width,
292 height: this.height
Simon Hunt195cb382014-11-03 17:50:51 -0800293 }
Simon Hunt25248912014-11-04 11:25:48 -0800294 },
295
296 preload: function (ctx) {
297 var c = ctx || '',
298 fn = isF(this.cb.preload);
299 traceFn('View.preload', this.vid + ', ' + c);
300 if (fn) {
301 trace('PRELOAD cb for ' + this.vid);
302 fn(this.token(), c);
303 }
304 },
305
306 reset: function () {
307 var fn = isF(this.cb.reset);
308 traceFn('View.reset', this.vid);
309 if (fn) {
310 trace('RESET cb for ' + this.vid);
311 fn(this.token());
312 } else if (this.cb.reset === true) {
313 // boolean true signifies "clear view"
314 trace(' [true] cleaing view...');
315 viewApi.empty();
316 }
317 },
318
319 load: function (ctx) {
320 var c = ctx || '',
321 fn = isF(this.cb.load);
322 traceFn('View.load', this.vid + ', ' + c);
323 this.$div.classed('currentView', true);
324 // TODO: add radio button set, if needed
325 if (fn) {
326 trace('LOAD cb for ' + this.vid);
327 fn(this.token(), c);
328 }
329 },
330
331 unload: function () {
332 var fn = isF(this.cb.unload);
333 traceFn('View.unload', this.vid);
334 this.$div.classed('currentView', false);
335 // TODO: remove radio button set, if needed
336 if (fn) {
337 trace('UNLOAD cb for ' + this.vid);
338 fn(this.token());
339 }
340 },
341
342 resize: function (ctx) {
343 var c = ctx || '',
344 fn = isF(this.cb.resize),
345 w = this.width(),
346 h = this.height();
347 traceFn('View.resize', this.vid + '/' + c +
348 ' [' + w + 'x' + h + ']');
349 if (fn) {
350 trace('RESIZE cb for ' + this.vid);
351 fn(this.token(), c);
352 }
353 },
354
355 error: function (ctx) {
356 var c = ctx || '',
357 fn = isF(this.cb.error);
358 traceFn('View.error', this.vid + ', ' + c);
359 if (fn) {
360 trace('ERROR cb for ' + this.vid);
361 fn(this.token(), c);
362 }
363 },
364
365 width: function () {
366 return $(this.$div.node()).width();
367 },
368
369 height: function () {
370 return $(this.$div.node()).height();
Simon Hunt195cb382014-11-03 17:50:51 -0800371 }
Simon Hunt25248912014-11-04 11:25:48 -0800372
373
374 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800375 };
376
377 // attach instance methods to the view prototype
378 $.extend(View.prototype, viewInstanceMethods);
379
380 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800381 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800382
Simon Hunt25248912014-11-04 11:25:48 -0800383 uiApi = {
384 /** @api ui addView( vid, nid, cb )
385 * Adds a view to the UI.
386 * <p>
387 * Views are loaded/unloaded into the view content pane at
388 * appropriate times, by the navigation framework. This method
389 * adds a view to the UI and returns a token object representing
390 * the view. A view's token is always passed as the first
391 * argument to each of the view's life-cycle callback functions.
392 * <p>
393 * Note that if the view is directly referenced by a nav-item,
394 * or in a group of views with one of those views referenced by
395 * a nav-item, then the <i>nid</i> argument can be omitted as
396 * the framework can infer it.
397 * <p>
398 * <i>cb</i> is a plain object containing callback functions:
399 * "preload", "reset", "load", "unload", "resize", "error".
400 * <pre>
401 * function myLoad(view, ctx) { ... }
402 * ...
403 * // short form...
404 * onos.ui.addView('viewId', {
405 * load: myLoad
406 * });
407 * </pre>
408 *
409 * @param vid (string) [*] view ID (a unique DOM element id)
410 * @param nid (string) nav-item ID (a unique DOM element id)
411 * @param cb (object) [*] callbacks object
412 * @return the view token
413 */
414 addView: function (vid, nid, cb) {
415 traceFn('addView', vid);
416 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800417 token;
418 if (view.ok) {
419 views[vid] = view;
420 token = view.token();
421 } else {
422 token = { vid: view.vid, bad: true };
423 }
424 return token;
425 }
426 };
427
Simon Hunt25248912014-11-04 11:25:48 -0800428 // ..........................................................
429 // View API
430
431 viewApi = {
432 /** @api view empty( )
433 * Empties the current view.
434 * <p>
435 * More specifically, removes all DOM elements from the
436 * current view's display div.
437 */
438 empty: function () {
439 if (!current.view) {
440 return;
441 }
442 current.view.$div.html('');
443 }
444 };
445
446 // ..........................................................
447 // Nav API
448 navApi = {
449
450 };
451
452 // ..........................................................
453 // Exported API
454
Simon Hunt195cb382014-11-03 17:50:51 -0800455 // function to be called from index.html to build the ONOS UI
456 function buildOnosUi() {
457 tsB = new Date().getTime();
458 tsI = tsB - tsI; // initialization duration
459
460 console.log('ONOS UI initialized in ' + tsI + 'ms');
461
462 if (built) {
463 throwError("ONOS UI already built!");
464 }
465 built = true;
466
467 $view = d3.select('#view');
468
469 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800470 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800471
472 // Invoke hashchange callback to navigate to content
473 // indicated by the window location hash.
474 hash();
475
476 // If there were any build errors, report them
477 reportBuildErrors();
478 }
479
480
481 // export the api and build-UI function
482 return {
Simon Hunt25248912014-11-04 11:25:48 -0800483 ui: uiApi,
484 view: viewApi,
485 nav: navApi,
Simon Hunt195cb382014-11-03 17:50:51 -0800486 buildUi: buildOnosUi
487 };
488 };
489
490}(jQuery));