blob: 87aa63d231dcd06318cfdf0f44e3cef640371c05 [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],
350 btnG;
351
352 // lazily create the buttons...
353 if (!(btnG = view.radioButtons)) {
354 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800355 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800356
357 btnSet.forEach(function (btn, i) {
358 var bid = btn.id || 'b' + i,
359 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800360 uid = makeUid(view, bid),
361 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800362 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800363 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800364 class: 'radio'
365 })
366 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800367
368 btnG.buttonDef[uid] = btn;
369
Simon Huntdb9eb072014-11-04 19:12:46 -0800370 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800371 button.classed('active', true);
Simon Huntdb9eb072014-11-04 19:12:46 -0800372 }
373 });
374
375 btnG.selectAll('span')
376 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800377 var button = d3.select(this),
378 uid = button.attr('id'),
379 btn = btnG.buttonDef[uid],
380 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800381
382 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800383 btnG.selectAll('span').classed('active', false);
384 button.classed('active', true);
385 if (isF(btn.cb)) {
386 btn.cb(view.token(), btn);
387 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800388 }
389 });
390
391 view.radioButtons = btnG;
392 }
393
394 // attach the buttons to the masthead
395 $mastRadio.node().appendChild(btnG.node());
396 }
397
Thomas Vachuska65368e32014-11-08 16:10:20 -0800398 function setupGlobalKeys() {
399 keyHandler.globalKeys = {
400 esc: escapeKey,
401 T: toggleTheme
402 };
403 // Masked keys are global key handlers that always return true.
404 // That is, the view will never see the event for that key.
405 keyHandler.maskedKeys = {
406 T: true
407 };
408 }
409
410 function escapeKey(view, key, code, ev) {
411 if (alerts.open) {
412 closeAlerts();
413 return true;
414 }
415 return false;
416 }
417
418 function toggleTheme(view, key, code, ev) {
419 var body = d3.select('body');
420 current.theme = (current.theme === 'light') ? 'dark' : 'light';
421 body.classed('light dark', false);
422 body.classed(current.theme, true);
423 return true;
424 }
425
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800426 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800427 var viewKeys,
428 masked = [];
429
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800430 if ($.isFunction(keyArg)) {
431 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800432 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800433 } else {
434 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800435 viewKeys = d3.map(keyArg).keys();
436 viewKeys.forEach(function (key) {
437 if (keyHandler.maskedKeys[key]) {
438 masked.push(' Key "' + key + '" is reserved');
439 }
440 });
441
442 if (masked.length) {
443 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
444 }
445 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800446 }
447 }
448
Thomas Vachuska65368e32014-11-08 16:10:20 -0800449 function keyIn() {
450 var event = d3.event,
451 keyCode = event.keyCode,
452 key = whatKey(keyCode),
453 gcb = isF(keyHandler.globalKeys[key]),
454 vcb = isF(keyHandler.viewKeys[key]) || isF(keyHandler.viewFn);
455
456 // global callback?
457 if (gcb && gcb(current.view.token(), key, keyCode, event)) {
458 // if the event was 'handled', we are done
459 return;
460 }
461 // otherwise, let the view callback have a shot
462 if (vcb) {
463 vcb(current.view.token(), key, keyCode, event);
464 }
465 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800466
467 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800468 $alerts.style('display', 'block');
469 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800470 .attr('class', 'close')
471 .text('X')
472 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800473 $alerts.append('pre');
474 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800475 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800476 alerts.open = true;
477 alerts.count = 0;
478 }
479
480 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800481 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800482 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800483 alerts.open = false;
484 }
485
486 function addAlert(msg) {
487 var lines,
488 oldContent;
489
490 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800491 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800492 }
493
494 lines = msg.split('\n');
495 lines[0] += ' '; // spacing so we don't crowd 'X'
496 lines = lines.join('\n');
497
498 if (oldContent) {
499 lines += '\n----\n' + oldContent;
500 }
501
Simon Hunt61d04042014-11-11 17:27:16 -0800502 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800503 alerts.count++;
504 }
505
506 function doAlert(msg) {
507 if (!alerts.open) {
508 createAlerts();
509 }
510 addAlert(msg);
511 }
512
Simon Hunt25248912014-11-04 11:25:48 -0800513 function resize(e) {
514 d3.selectAll('.onosView').call(setViewDimensions);
515 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800516 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800517 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800518 }
519 }
520
521 // ..........................................................
522 // View class
523 // Captures state information about a view.
524
525 // Constructor
526 // vid : view id
527 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800528 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800529 function View(vid) {
530 var av = 'addView(): ',
531 args = Array.prototype.slice.call(arguments),
532 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800533 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800534
535 args.shift(); // first arg is always vid
536 if (typeof args[0] === 'string') { // nid specified
537 nid = args.shift();
538 }
539 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800540
541 this.vid = vid;
542
543 if (validateViewArgs(vid)) {
544 this.nid = nid; // explicit navitem id (can be null)
545 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800546 this.$div = null; // view not yet added to DOM
547 this.radioButtons = null; // no radio buttons yet
548 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800549 }
Simon Hunt195cb382014-11-03 17:50:51 -0800550 }
551
552 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800553 var av = "ui.addView(...): ",
554 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800555 if (typeof vid !== 'string' || !vid) {
556 doError(av + 'vid required');
557 } else if (views[vid]) {
558 doError(av + 'View ID "' + vid + '" already exists');
559 } else {
560 ok = true;
561 }
562 return ok;
563 }
564
565 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800566 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800567 return {
Simon Hunt25248912014-11-04 11:25:48 -0800568 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800569 vid: this.vid,
570 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800571 $div: this.$div,
572
573 // functions
574 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800575 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800576 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800577 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800578 setKeys: this.setKeys,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800579 dataLoadError: this.dataLoadError,
580 alert: this.alert
Simon Hunt195cb382014-11-03 17:50:51 -0800581 }
Simon Hunt25248912014-11-04 11:25:48 -0800582 },
583
Simon Huntf67722a2014-11-10 09:32:06 -0800584 // == Life-cycle functions
585 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800586 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800587 var c = ctx || '',
588 fn = isF(this.cb.preload);
589 traceFn('View.preload', this.vid + ', ' + c);
590 if (fn) {
591 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800592 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800593 }
594 },
595
Simon Huntf67722a2014-11-10 09:32:06 -0800596 reset: function (ctx, flags) {
597 var c = ctx || '',
598 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800599 traceFn('View.reset', this.vid);
600 if (fn) {
601 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800602 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800603 } else if (this.cb.reset === true) {
604 // boolean true signifies "clear view"
605 trace(' [true] cleaing view...');
606 viewApi.empty();
607 }
608 },
609
Simon Hunt56d51852014-11-09 13:03:35 -0800610 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800611 var c = ctx || '',
612 fn = isF(this.cb.load);
613 traceFn('View.load', this.vid + ', ' + c);
614 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800615 if (fn) {
616 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800617 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800618 }
619 },
620
Simon Huntf67722a2014-11-10 09:32:06 -0800621 unload: function (ctx, flags) {
622 var c = ctx | '',
623 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800624 traceFn('View.unload', this.vid);
625 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800626 if (fn) {
627 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800628 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800629 }
630 },
631
Simon Hunt56d51852014-11-09 13:03:35 -0800632 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800633 var c = ctx || '',
634 fn = isF(this.cb.resize),
635 w = this.width(),
636 h = this.height();
637 traceFn('View.resize', this.vid + '/' + c +
638 ' [' + w + 'x' + h + ']');
639 if (fn) {
640 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800641 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800642 }
643 },
644
Simon Huntf67722a2014-11-10 09:32:06 -0800645 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800646 var c = ctx || '',
647 fn = isF(this.cb.error);
648 traceFn('View.error', this.vid + ', ' + c);
649 if (fn) {
650 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800651 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800652 }
653 },
654
Simon Huntf67722a2014-11-10 09:32:06 -0800655 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800656 width: function () {
657 return $(this.$div.node()).width();
658 },
659
660 height: function () {
661 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800662 },
Simon Hunt25248912014-11-04 11:25:48 -0800663
Simon Hunt934c3ce2014-11-05 11:45:07 -0800664 setRadio: function (btnSet) {
665 setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800666 },
667
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800668 setKeys: function (keyArg) {
669 setKeyBindings(keyArg);
670 },
671
Simon Hunt142d0032014-11-04 20:13:09 -0800672 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800673 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800674 },
675
Simon Huntbb282f52014-11-10 11:08:19 -0800676 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800677 // TODO : implement custom dialogs
678
679 // Consider enhancing alert mechanism to handle multiples
680 // as individually closable.
681 alert: function (msg) {
682 doAlert(msg);
683 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800684
685 dataLoadError: function (err, url) {
686 var msg = 'Data Load Error\n\n' +
687 err.status + ' -- ' + err.statusText + '\n\n' +
688 'relative-url: "' + url + '"\n\n' +
689 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800690 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800691 }
Simon Hunt25248912014-11-04 11:25:48 -0800692
693 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800694 };
695
696 // attach instance methods to the view prototype
697 $.extend(View.prototype, viewInstanceMethods);
698
699 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800700 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800701
Simon Hunta5e89142014-11-14 07:00:33 -0800702 var fpConfig = {
703 TR: {
704 side: 'right'
705
706 },
707 TL: {
708 side: 'left'
709 }
710 };
711
Simon Hunt25248912014-11-04 11:25:48 -0800712 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800713 addLib: function (libName, api) {
714 // TODO: validation of args
715 libApi[libName] = api;
716 },
717
Simon Hunt61d04042014-11-11 17:27:16 -0800718 // TODO: implement floating panel as a class
719 // TODO: parameterize position (currently hard-coded to TopRight)
720 /*
721 * Creates div in floating panels block, with the given id.
722 * Returns panel token used to interact with the panel
723 */
724 addFloatingPanel: function (id, position) {
725 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800726 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800727 el,
728 fp;
729
730 if (fpanels[id]) {
731 buildError('Float panel with id "' + id + '" already exists.');
732 return null;
733 }
734
735 el = $floatPanels.append('div')
736 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800737 .attr('class', 'fpanel')
738 .style('opacity', 0);
739
740 // has to be called after el is set.
741 el.style(cfg.side, pxHide());
742
743 function pxShow() {
744 return '20px';
745 }
746 function pxHide() {
747 return (-20 - widthVal()) + 'px';
748 }
749 function widthVal() {
750 return el.style('width').replace(/px$/, '');
751 }
Simon Hunt61d04042014-11-11 17:27:16 -0800752
753 fp = {
754 id: id,
755 el: el,
756 pos: pos,
Simon Hunta5e89142014-11-14 07:00:33 -0800757
Simon Hunt61d04042014-11-11 17:27:16 -0800758 show: function () {
759 console.log('show pane: ' + id);
760 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800761 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800762 .style('opacity', 1);
763 },
764 hide: function () {
765 console.log('hide pane: ' + id);
766 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800767 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800768 .style('opacity', 0);
769 },
770 empty: function () {
771 return el.html('');
772 },
773 append: function (what) {
774 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800775 },
776 width: function (w) {
777 if (w === undefined) {
778 return widthVal();
779 }
780 el.style('width', w);
Simon Hunt61d04042014-11-11 17:27:16 -0800781 }
782 };
783 fpanels[id] = fp;
784 return fp;
785 },
786
Simon Hunt1a9eff92014-11-07 11:06:34 -0800787 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800788 /** @api ui addView( vid, nid, cb )
789 * Adds a view to the UI.
790 * <p>
791 * Views are loaded/unloaded into the view content pane at
792 * appropriate times, by the navigation framework. This method
793 * adds a view to the UI and returns a token object representing
794 * the view. A view's token is always passed as the first
795 * argument to each of the view's life-cycle callback functions.
796 * <p>
797 * Note that if the view is directly referenced by a nav-item,
798 * or in a group of views with one of those views referenced by
799 * a nav-item, then the <i>nid</i> argument can be omitted as
800 * the framework can infer it.
801 * <p>
802 * <i>cb</i> is a plain object containing callback functions:
803 * "preload", "reset", "load", "unload", "resize", "error".
804 * <pre>
805 * function myLoad(view, ctx) { ... }
806 * ...
807 * // short form...
808 * onos.ui.addView('viewId', {
809 * load: myLoad
810 * });
811 * </pre>
812 *
813 * @param vid (string) [*] view ID (a unique DOM element id)
814 * @param nid (string) nav-item ID (a unique DOM element id)
815 * @param cb (object) [*] callbacks object
816 * @return the view token
817 */
818 addView: function (vid, nid, cb) {
819 traceFn('addView', vid);
820 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800821 token;
822 if (view.ok) {
823 views[vid] = view;
824 token = view.token();
825 } else {
826 token = { vid: view.vid, bad: true };
827 }
828 return token;
829 }
830 };
831
Simon Hunt25248912014-11-04 11:25:48 -0800832 // ..........................................................
833 // View API
834
Simon Huntbb282f52014-11-10 11:08:19 -0800835 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800836 viewApi = {
837 /** @api view empty( )
838 * Empties the current view.
839 * <p>
840 * More specifically, removes all DOM elements from the
841 * current view's display div.
842 */
843 empty: function () {
844 if (!current.view) {
845 return;
846 }
847 current.view.$div.html('');
848 }
849 };
850
851 // ..........................................................
852 // Nav API
853 navApi = {
854
855 };
856
857 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800858 // Library API
859 libApi = {
860
861 };
862
863 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800864 // Exported API
865
Simon Hunt195cb382014-11-03 17:50:51 -0800866 // function to be called from index.html to build the ONOS UI
867 function buildOnosUi() {
868 tsB = new Date().getTime();
869 tsI = tsB - tsI; // initialization duration
870
871 console.log('ONOS UI initialized in ' + tsI + 'ms');
872
873 if (built) {
874 throwError("ONOS UI already built!");
875 }
876 built = true;
877
Simon Huntdb9eb072014-11-04 19:12:46 -0800878 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800879
880 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800881 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800882
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800883 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800884 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800885
Simon Hunt195cb382014-11-03 17:50:51 -0800886 // Invoke hashchange callback to navigate to content
887 // indicated by the window location hash.
888 hash();
889
890 // If there were any build errors, report them
891 reportBuildErrors();
892 }
893
Simon Hunt195cb382014-11-03 17:50:51 -0800894 // export the api and build-UI function
895 return {
Simon Hunt25248912014-11-04 11:25:48 -0800896 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800897 lib: libApi,
898 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800899 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800900 buildUi: buildOnosUi,
901 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800902 };
903 };
904
Simon Huntdb9eb072014-11-04 19:12:46 -0800905}(jQuery));