blob: 20c679a79e8b250df64921f6aaa186f56fb27ff2 [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,
Simon Hunt1a9eff92014-11-07 11:06:34 -080035 navApi,
Simon Huntbb282f52014-11-10 11:08:19 -080036 libApi,
37 exported = {};
Simon Hunt25248912014-11-04 11:25:48 -080038
39 var defaultOptions = {
Simon Hunt142d0032014-11-04 20:13:09 -080040 trace: false,
Thomas Vachuska65368e32014-11-08 16:10:20 -080041 theme: 'light',
Simon Hunt142d0032014-11-04 20:13:09 -080042 startVid: defaultVid
Simon Hunt25248912014-11-04 11:25:48 -080043 };
44
45 // compute runtime settings
46 var settings = $.extend({}, defaultOptions, options);
Simon Hunt195cb382014-11-03 17:50:51 -080047
Thomas Vachuska65368e32014-11-08 16:10:20 -080048 // set the selected theme
49 d3.select('body').classed(settings.theme, true);
50
Simon Hunt195cb382014-11-03 17:50:51 -080051 // internal state
52 var views = {},
Simon Hunt61d04042014-11-11 17:27:16 -080053 fpanels = {},
Simon Hunt195cb382014-11-03 17:50:51 -080054 current = {
55 view: null,
Thomas Vachuska65368e32014-11-08 16:10:20 -080056 ctx: '',
Simon Hunt56d51852014-11-09 13:03:35 -080057 flags: {},
Thomas Vachuska65368e32014-11-08 16:10:20 -080058 theme: settings.theme
Simon Hunt195cb382014-11-03 17:50:51 -080059 },
60 built = false,
Simon Hunt61d04042014-11-11 17:27:16 -080061 buildErrors = [],
Simon Hunt934c3ce2014-11-05 11:45:07 -080062 keyHandler = {
Thomas Vachuska65368e32014-11-08 16:10:20 -080063 globalKeys: {},
64 maskedKeys: {},
65 viewKeys: {},
66 viewFn: null
67 },
68 alerts = {
69 open: false,
70 count: 0
Simon Hunt934c3ce2014-11-05 11:45:07 -080071 };
Simon Hunt195cb382014-11-03 17:50:51 -080072
73 // DOM elements etc.
Simon Hunt61d04042014-11-11 17:27:16 -080074 // TODO: verify existence of following elements...
75 var $view = d3.select('#view'),
76 $floatPanels = d3.select('#floatPanels'),
77 $alerts = d3.select('#alerts'),
78 // note, following elements added programmatically...
Simon Huntdb9eb072014-11-04 19:12:46 -080079 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080080
81
Simon Hunt0df1b1d2014-11-04 22:58:29 -080082 function whatKey(code) {
83 switch (code) {
84 case 13: return 'enter';
85 case 16: return 'shift';
86 case 17: return 'ctrl';
87 case 18: return 'alt';
88 case 27: return 'esc';
89 case 32: return 'space';
90 case 37: return 'leftArrow';
91 case 38: return 'upArrow';
92 case 39: return 'rightArrow';
93 case 40: return 'downArrow';
94 case 91: return 'cmdLeft';
95 case 93: return 'cmdRight';
96 default:
97 if ((code >= 48 && code <= 57) ||
98 (code >= 65 && code <= 90)) {
99 return String.fromCharCode(code);
100 } else if (code >= 112 && code <= 123) {
101 return 'F' + (code - 111);
102 }
103 return '.';
104 }
105 }
106
107
Simon Hunt195cb382014-11-03 17:50:51 -0800108 // ..........................................................
109 // Internal functions
110
111 // throw an error
112 function throwError(msg) {
113 // separate function, as we might add tracing here too, later
114 throw new Error(msg);
115 }
116
117 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800118 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800119 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800120 }
121
122 function trace(msg) {
123 if (settings.trace) {
124 console.log(msg);
125 }
126 }
127
128 function traceFn(fn, params) {
129 if (settings.trace) {
130 console.log('*FN* ' + fn + '(...): ' + params);
131 }
Simon Hunt195cb382014-11-03 17:50:51 -0800132 }
133
134 // hash navigation
135 function hash() {
136 var hash = window.location.hash,
137 redo = false,
138 view,
139 t;
140
Simon Hunt25248912014-11-04 11:25:48 -0800141 traceFn('hash', hash);
142
Simon Hunt195cb382014-11-03 17:50:51 -0800143 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800144 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800145 redo = true;
146 }
147
148 t = parseHash(hash);
149 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800150 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800151 }
152
153 view = views[t.vid];
154 if (!view) {
155 doError('No view defined with id: ' + t.vid);
156 }
157
158 if (redo) {
159 window.location.hash = makeHash(t);
160 // the above will result in a hashchange event, invoking
161 // this function again
162 } else {
163 // hash was not modified... navigate to where we need to be
164 navigate(hash, view, t);
165 }
Simon Hunt195cb382014-11-03 17:50:51 -0800166 }
167
168 function parseHash(s) {
169 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800170 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800171 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800172
Simon Hunt56d51852014-11-09 13:03:35 -0800173 // look for use of flags, first
174 var vidctx,
175 vid,
176 ctx,
177 flags,
178 flagMap,
179 m;
180
181 // RE that includes flags ('?flag1,flag2')
182 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800183 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800184 vidctx = m[1];
185 flags = m[2];
186 flagMap = {};
187 } else {
188 // no flags
189 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
190 if (m) {
191 vidctx = m[1];
192 } else {
193 // bad hash
194 return null;
195 }
Simon Hunt195cb382014-11-03 17:50:51 -0800196 }
197
Simon Hunt56d51852014-11-09 13:03:35 -0800198 vidctx = vidctx.split(',');
199 vid = vidctx[0];
200 ctx = vidctx[1];
201 if (flags) {
202 flags.split(',').forEach(function (f) {
203 flagMap[f.trim()] = true;
204 });
205 }
206
207 return {
208 vid: vid.trim(),
209 ctx: ctx ? ctx.trim() : '',
210 flags: flagMap
211 };
212
Simon Hunt195cb382014-11-03 17:50:51 -0800213 }
214
Simon Hunt56d51852014-11-09 13:03:35 -0800215 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800216 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800217 // make a hash string from the given navigation coordinates,
218 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800219 // if t is not an object, then it is a vid
220 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800221 c = ctx || '',
222 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800223
224 if ($.isPlainObject(t)) {
225 h = t.vid;
226 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800227 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800228 }
229
230 if (c) {
231 h += ',' + c;
232 }
Simon Hunt56d51852014-11-09 13:03:35 -0800233 if (f) {
234 h += '?' + d3.map(f).keys().join(',');
235 }
Simon Hunt25248912014-11-04 11:25:48 -0800236 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800237 return h;
238 }
239
240 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800241 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800242 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800243 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800244 createView(view);
245 setView(view, hash, t);
246 }
247
Simon Hunt61d04042014-11-11 17:27:16 -0800248 function buildError(msg) {
249 buildErrors.push(msg);
250 }
251
Simon Hunt195cb382014-11-03 17:50:51 -0800252 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800253 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800254 var nerr = buildErrors.length,
255 errmsg;
256 if (!nerr) {
257 console.log('(no build errors)');
258 } else {
259 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
260 buildErrors.join('\n');
261 doAlert(errmsg);
262 console.error(errmsg);
263 }
Simon Hunt195cb382014-11-03 17:50:51 -0800264 }
265
Simon Hunt25248912014-11-04 11:25:48 -0800266 // returns the reference if it is a function, null otherwise
267 function isF(f) {
268 return $.isFunction(f) ? f : null;
269 }
270
Simon Hunt195cb382014-11-03 17:50:51 -0800271 // ..........................................................
272 // View life-cycle functions
273
Simon Hunt25248912014-11-04 11:25:48 -0800274 function setViewDimensions(sel) {
275 var w = window.innerWidth,
276 h = window.innerHeight - mastHeight;
277 sel.each(function () {
278 $(this)
279 .css('width', w + 'px')
280 .css('height', h + 'px')
281 });
282 }
283
Simon Hunt195cb382014-11-03 17:50:51 -0800284 function createView(view) {
285 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800286
Simon Hunt195cb382014-11-03 17:50:51 -0800287 // lazy initialization of the view
288 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800289 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800290 $d = $view.append('div')
291 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800292 id: view.vid,
293 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800294 });
Simon Hunt25248912014-11-04 11:25:48 -0800295 setViewDimensions($d);
296 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800297 }
298 }
299
300 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800301 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800302 // set the specified view as current, while invoking the
303 // appropriate life-cycle callbacks
304
Simon Hunt56d51852014-11-09 13:03:35 -0800305 // first, we'll start by closing the alerts pane, if open
306 closeAlerts();
307
Simon Hunt195cb382014-11-03 17:50:51 -0800308 // if there is a current view, and it is not the same as
309 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800310 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800311 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800312
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800313 // detach radio buttons, key handlers, etc.
314 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800315 keyHandler.viewKeys = {};
316 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800317 }
318
319 // cache new view and context
320 current.view = view;
321 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800322 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800323
Simon Hunt195cb382014-11-03 17:50:51 -0800324 // preload is called only once, after the view is in the DOM
325 if (!view.preloaded) {
Simon Hunt56d51852014-11-09 13:03:35 -0800326 view.preload(current.ctx, current.flags);
Simon Hunt25248912014-11-04 11:25:48 -0800327 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800328 }
329
330 // clear the view of stale data
331 view.reset();
332
333 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800334 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800335 }
336
Simon Huntdb9eb072014-11-04 19:12:46 -0800337 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800338 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800339 return view.vid + '-' + id;
340 }
341
342 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800343 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800344 var re = new RegExp('^' + view.vid + '-');
345 return uid.replace(re, '');
346 }
347
Simon Hunt934c3ce2014-11-05 11:45:07 -0800348 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800349 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800350 btnG,
351 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800352
353 // lazily create the buttons...
354 if (!(btnG = view.radioButtons)) {
355 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800356 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800357
358 btnSet.forEach(function (btn, i) {
359 var bid = btn.id || 'b' + i,
360 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800361 uid = makeUid(view, bid),
362 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800363 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800364 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800365 class: 'radio'
366 })
367 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800368
Simon Hunt9462e8c2014-11-14 17:28:09 -0800369 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800370 btnG.buttonDef[uid] = btn;
371
Simon Huntdb9eb072014-11-04 19:12:46 -0800372 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800373 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800374 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800375 }
376 });
377
378 btnG.selectAll('span')
379 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800380 var button = d3.select(this),
381 uid = button.attr('id'),
382 btn = btnG.buttonDef[uid],
383 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800384
385 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800386 btnG.selectAll('span').classed('active', false);
387 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800388 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800389 if (isF(btn.cb)) {
390 btn.cb(view.token(), btn);
391 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800392 }
393 });
394
395 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800396
397 api.selected = function () {
398 return btnG.selected;
399 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800400 }
401
402 // attach the buttons to the masthead
403 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800404 // return an api for interacting with the button set
405 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800406 }
407
Thomas Vachuska65368e32014-11-08 16:10:20 -0800408 function setupGlobalKeys() {
409 keyHandler.globalKeys = {
410 esc: escapeKey,
411 T: toggleTheme
412 };
413 // Masked keys are global key handlers that always return true.
414 // That is, the view will never see the event for that key.
415 keyHandler.maskedKeys = {
416 T: true
417 };
418 }
419
420 function escapeKey(view, key, code, ev) {
421 if (alerts.open) {
422 closeAlerts();
423 return true;
424 }
425 return false;
426 }
427
428 function toggleTheme(view, key, code, ev) {
429 var body = d3.select('body');
430 current.theme = (current.theme === 'light') ? 'dark' : 'light';
431 body.classed('light dark', false);
432 body.classed(current.theme, true);
433 return true;
434 }
435
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800436 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800437 var viewKeys,
438 masked = [];
439
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800440 if ($.isFunction(keyArg)) {
441 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800442 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800443 } else {
444 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800445 viewKeys = d3.map(keyArg).keys();
446 viewKeys.forEach(function (key) {
447 if (keyHandler.maskedKeys[key]) {
448 masked.push(' Key "' + key + '" is reserved');
449 }
450 });
451
452 if (masked.length) {
453 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
454 }
455 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800456 }
457 }
458
Thomas Vachuska65368e32014-11-08 16:10:20 -0800459 function keyIn() {
460 var event = d3.event,
461 keyCode = event.keyCode,
462 key = whatKey(keyCode),
463 gcb = isF(keyHandler.globalKeys[key]),
464 vcb = isF(keyHandler.viewKeys[key]) || isF(keyHandler.viewFn);
465
466 // global callback?
467 if (gcb && gcb(current.view.token(), key, keyCode, event)) {
468 // if the event was 'handled', we are done
469 return;
470 }
471 // otherwise, let the view callback have a shot
472 if (vcb) {
473 vcb(current.view.token(), key, keyCode, event);
474 }
475 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800476
477 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800478 $alerts.style('display', 'block');
479 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800480 .attr('class', 'close')
481 .text('X')
482 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800483 $alerts.append('pre');
484 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800485 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800486 alerts.open = true;
487 alerts.count = 0;
488 }
489
490 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800491 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800492 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800493 alerts.open = false;
494 }
495
496 function addAlert(msg) {
497 var lines,
498 oldContent;
499
500 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800501 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800502 }
503
504 lines = msg.split('\n');
505 lines[0] += ' '; // spacing so we don't crowd 'X'
506 lines = lines.join('\n');
507
508 if (oldContent) {
509 lines += '\n----\n' + oldContent;
510 }
511
Simon Hunt61d04042014-11-11 17:27:16 -0800512 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800513 alerts.count++;
514 }
515
516 function doAlert(msg) {
517 if (!alerts.open) {
518 createAlerts();
519 }
520 addAlert(msg);
521 }
522
Simon Hunt25248912014-11-04 11:25:48 -0800523 function resize(e) {
524 d3.selectAll('.onosView').call(setViewDimensions);
525 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800526 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800527 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800528 }
529 }
530
531 // ..........................................................
532 // View class
533 // Captures state information about a view.
534
535 // Constructor
536 // vid : view id
537 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800538 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800539 function View(vid) {
540 var av = 'addView(): ',
541 args = Array.prototype.slice.call(arguments),
542 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800543 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800544
545 args.shift(); // first arg is always vid
546 if (typeof args[0] === 'string') { // nid specified
547 nid = args.shift();
548 }
549 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800550
551 this.vid = vid;
552
553 if (validateViewArgs(vid)) {
554 this.nid = nid; // explicit navitem id (can be null)
555 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800556 this.$div = null; // view not yet added to DOM
557 this.radioButtons = null; // no radio buttons yet
558 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800559 }
Simon Hunt195cb382014-11-03 17:50:51 -0800560 }
561
562 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800563 var av = "ui.addView(...): ",
564 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800565 if (typeof vid !== 'string' || !vid) {
566 doError(av + 'vid required');
567 } else if (views[vid]) {
568 doError(av + 'View ID "' + vid + '" already exists');
569 } else {
570 ok = true;
571 }
572 return ok;
573 }
574
575 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800576 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800577 return {
Simon Hunt25248912014-11-04 11:25:48 -0800578 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800579 vid: this.vid,
580 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800581 $div: this.$div,
582
583 // functions
584 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800585 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800586 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800587 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800588 setKeys: this.setKeys,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800589 dataLoadError: this.dataLoadError,
590 alert: this.alert
Simon Hunt195cb382014-11-03 17:50:51 -0800591 }
Simon Hunt25248912014-11-04 11:25:48 -0800592 },
593
Simon Huntf67722a2014-11-10 09:32:06 -0800594 // == Life-cycle functions
595 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800596 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800597 var c = ctx || '',
598 fn = isF(this.cb.preload);
599 traceFn('View.preload', this.vid + ', ' + c);
600 if (fn) {
601 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800602 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800603 }
604 },
605
Simon Huntf67722a2014-11-10 09:32:06 -0800606 reset: function (ctx, flags) {
607 var c = ctx || '',
608 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800609 traceFn('View.reset', this.vid);
610 if (fn) {
611 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800612 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800613 } else if (this.cb.reset === true) {
614 // boolean true signifies "clear view"
615 trace(' [true] cleaing view...');
616 viewApi.empty();
617 }
618 },
619
Simon Hunt56d51852014-11-09 13:03:35 -0800620 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800621 var c = ctx || '',
622 fn = isF(this.cb.load);
623 traceFn('View.load', this.vid + ', ' + c);
624 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800625 if (fn) {
626 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800627 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800628 }
629 },
630
Simon Huntf67722a2014-11-10 09:32:06 -0800631 unload: function (ctx, flags) {
632 var c = ctx | '',
633 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800634 traceFn('View.unload', this.vid);
635 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800636 if (fn) {
637 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800638 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800639 }
640 },
641
Simon Hunt56d51852014-11-09 13:03:35 -0800642 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800643 var c = ctx || '',
644 fn = isF(this.cb.resize),
645 w = this.width(),
646 h = this.height();
647 traceFn('View.resize', this.vid + '/' + c +
648 ' [' + w + 'x' + h + ']');
649 if (fn) {
650 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800651 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800652 }
653 },
654
Simon Huntf67722a2014-11-10 09:32:06 -0800655 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800656 var c = ctx || '',
657 fn = isF(this.cb.error);
658 traceFn('View.error', this.vid + ', ' + c);
659 if (fn) {
660 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800661 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800662 }
663 },
664
Simon Huntf67722a2014-11-10 09:32:06 -0800665 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800666 width: function () {
667 return $(this.$div.node()).width();
668 },
669
670 height: function () {
671 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800672 },
Simon Hunt25248912014-11-04 11:25:48 -0800673
Simon Hunt934c3ce2014-11-05 11:45:07 -0800674 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800675 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800676 },
677
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800678 setKeys: function (keyArg) {
679 setKeyBindings(keyArg);
680 },
681
Simon Hunt142d0032014-11-04 20:13:09 -0800682 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800683 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800684 },
685
Simon Huntbb282f52014-11-10 11:08:19 -0800686 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800687 // TODO : implement custom dialogs
688
689 // Consider enhancing alert mechanism to handle multiples
690 // as individually closable.
691 alert: function (msg) {
692 doAlert(msg);
693 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800694
695 dataLoadError: function (err, url) {
696 var msg = 'Data Load Error\n\n' +
697 err.status + ' -- ' + err.statusText + '\n\n' +
698 'relative-url: "' + url + '"\n\n' +
699 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800700 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800701 }
Simon Hunt25248912014-11-04 11:25:48 -0800702
703 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800704 };
705
706 // attach instance methods to the view prototype
707 $.extend(View.prototype, viewInstanceMethods);
708
709 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800710 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800711
Simon Hunta5e89142014-11-14 07:00:33 -0800712 var fpConfig = {
713 TR: {
714 side: 'right'
715
716 },
717 TL: {
718 side: 'left'
719 }
720 };
721
Simon Hunt25248912014-11-04 11:25:48 -0800722 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800723 addLib: function (libName, api) {
724 // TODO: validation of args
725 libApi[libName] = api;
726 },
727
Simon Hunt61d04042014-11-11 17:27:16 -0800728 // TODO: implement floating panel as a class
729 // TODO: parameterize position (currently hard-coded to TopRight)
730 /*
731 * Creates div in floating panels block, with the given id.
732 * Returns panel token used to interact with the panel
733 */
734 addFloatingPanel: function (id, position) {
735 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800736 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800737 el,
738 fp;
739
740 if (fpanels[id]) {
741 buildError('Float panel with id "' + id + '" already exists.');
742 return null;
743 }
744
745 el = $floatPanels.append('div')
746 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800747 .attr('class', 'fpanel')
748 .style('opacity', 0);
749
750 // has to be called after el is set.
751 el.style(cfg.side, pxHide());
752
753 function pxShow() {
754 return '20px';
755 }
756 function pxHide() {
757 return (-20 - widthVal()) + 'px';
758 }
759 function widthVal() {
760 return el.style('width').replace(/px$/, '');
761 }
Simon Hunt61d04042014-11-11 17:27:16 -0800762
763 fp = {
764 id: id,
765 el: el,
766 pos: pos,
Simon Hunta5e89142014-11-14 07:00:33 -0800767
Simon Hunt61d04042014-11-11 17:27:16 -0800768 show: function () {
769 console.log('show pane: ' + id);
770 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800771 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800772 .style('opacity', 1);
773 },
774 hide: function () {
775 console.log('hide pane: ' + id);
776 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800777 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800778 .style('opacity', 0);
779 },
780 empty: function () {
781 return el.html('');
782 },
783 append: function (what) {
784 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800785 },
786 width: function (w) {
787 if (w === undefined) {
788 return widthVal();
789 }
790 el.style('width', w);
Simon Hunt61d04042014-11-11 17:27:16 -0800791 }
792 };
793 fpanels[id] = fp;
794 return fp;
795 },
796
Simon Hunt1a9eff92014-11-07 11:06:34 -0800797 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800798 /** @api ui addView( vid, nid, cb )
799 * Adds a view to the UI.
800 * <p>
801 * Views are loaded/unloaded into the view content pane at
802 * appropriate times, by the navigation framework. This method
803 * adds a view to the UI and returns a token object representing
804 * the view. A view's token is always passed as the first
805 * argument to each of the view's life-cycle callback functions.
806 * <p>
807 * Note that if the view is directly referenced by a nav-item,
808 * or in a group of views with one of those views referenced by
809 * a nav-item, then the <i>nid</i> argument can be omitted as
810 * the framework can infer it.
811 * <p>
812 * <i>cb</i> is a plain object containing callback functions:
813 * "preload", "reset", "load", "unload", "resize", "error".
814 * <pre>
815 * function myLoad(view, ctx) { ... }
816 * ...
817 * // short form...
818 * onos.ui.addView('viewId', {
819 * load: myLoad
820 * });
821 * </pre>
822 *
823 * @param vid (string) [*] view ID (a unique DOM element id)
824 * @param nid (string) nav-item ID (a unique DOM element id)
825 * @param cb (object) [*] callbacks object
826 * @return the view token
827 */
828 addView: function (vid, nid, cb) {
829 traceFn('addView', vid);
830 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800831 token;
832 if (view.ok) {
833 views[vid] = view;
834 token = view.token();
835 } else {
836 token = { vid: view.vid, bad: true };
837 }
838 return token;
839 }
840 };
841
Simon Hunt25248912014-11-04 11:25:48 -0800842 // ..........................................................
843 // View API
844
Simon Huntbb282f52014-11-10 11:08:19 -0800845 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800846 viewApi = {
847 /** @api view empty( )
848 * Empties the current view.
849 * <p>
850 * More specifically, removes all DOM elements from the
851 * current view's display div.
852 */
853 empty: function () {
854 if (!current.view) {
855 return;
856 }
857 current.view.$div.html('');
858 }
859 };
860
861 // ..........................................................
862 // Nav API
863 navApi = {
864
865 };
866
867 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800868 // Library API
869 libApi = {
870
871 };
872
873 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800874 // Exported API
875
Simon Hunt195cb382014-11-03 17:50:51 -0800876 // function to be called from index.html to build the ONOS UI
877 function buildOnosUi() {
878 tsB = new Date().getTime();
879 tsI = tsB - tsI; // initialization duration
880
881 console.log('ONOS UI initialized in ' + tsI + 'ms');
882
883 if (built) {
884 throwError("ONOS UI already built!");
885 }
886 built = true;
887
Simon Huntdb9eb072014-11-04 19:12:46 -0800888 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800889
890 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800891 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800892
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800893 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800894 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800895
Simon Hunt195cb382014-11-03 17:50:51 -0800896 // Invoke hashchange callback to navigate to content
897 // indicated by the window location hash.
898 hash();
899
900 // If there were any build errors, report them
901 reportBuildErrors();
902 }
903
Simon Hunt195cb382014-11-03 17:50:51 -0800904 // export the api and build-UI function
905 return {
Simon Hunt25248912014-11-04 11:25:48 -0800906 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800907 lib: libApi,
908 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800909 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800910 buildUi: buildOnosUi,
911 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800912 };
913 };
914
Simon Huntdb9eb072014-11-04 19:12:46 -0800915}(jQuery));