blob: 384c64c49f4b5e46795388254780fd07d08ae2d6 [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 Vachuskaece59ee2014-11-19 19:06:11 -080041 theme: 'dark',
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,
Simon Hunt625dc402014-11-18 10:57:18 -0800590 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800591 flash: this.flash,
Simon Hunt625dc402014-11-18 10:57:18 -0800592 theme: this.theme
Simon Hunt195cb382014-11-03 17:50:51 -0800593 }
Simon Hunt25248912014-11-04 11:25:48 -0800594 },
595
Simon Huntf67722a2014-11-10 09:32:06 -0800596 // == Life-cycle functions
597 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800598 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800599 var c = ctx || '',
600 fn = isF(this.cb.preload);
601 traceFn('View.preload', this.vid + ', ' + c);
602 if (fn) {
603 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800604 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800605 }
606 },
607
Simon Huntf67722a2014-11-10 09:32:06 -0800608 reset: function (ctx, flags) {
609 var c = ctx || '',
610 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800611 traceFn('View.reset', this.vid);
612 if (fn) {
613 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800614 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800615 } else if (this.cb.reset === true) {
616 // boolean true signifies "clear view"
617 trace(' [true] cleaing view...');
618 viewApi.empty();
619 }
620 },
621
Simon Hunt56d51852014-11-09 13:03:35 -0800622 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800623 var c = ctx || '',
624 fn = isF(this.cb.load);
625 traceFn('View.load', this.vid + ', ' + c);
626 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800627 if (fn) {
628 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800629 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800630 }
631 },
632
Simon Huntf67722a2014-11-10 09:32:06 -0800633 unload: function (ctx, flags) {
634 var c = ctx | '',
635 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800636 traceFn('View.unload', this.vid);
637 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800638 if (fn) {
639 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800640 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800641 }
642 },
643
Simon Hunt56d51852014-11-09 13:03:35 -0800644 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800645 var c = ctx || '',
646 fn = isF(this.cb.resize),
647 w = this.width(),
648 h = this.height();
649 traceFn('View.resize', this.vid + '/' + c +
650 ' [' + w + 'x' + h + ']');
651 if (fn) {
652 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800653 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800654 }
655 },
656
Simon Huntf67722a2014-11-10 09:32:06 -0800657 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800658 var c = ctx || '',
659 fn = isF(this.cb.error);
660 traceFn('View.error', this.vid + ', ' + c);
661 if (fn) {
662 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800663 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800664 }
665 },
666
Simon Huntf67722a2014-11-10 09:32:06 -0800667 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800668 width: function () {
669 return $(this.$div.node()).width();
670 },
671
672 height: function () {
673 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800674 },
Simon Hunt25248912014-11-04 11:25:48 -0800675
Simon Hunt934c3ce2014-11-05 11:45:07 -0800676 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800677 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800678 },
679
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800680 setKeys: function (keyArg) {
681 setKeyBindings(keyArg);
682 },
683
Simon Hunt625dc402014-11-18 10:57:18 -0800684 theme: function () {
685 return current.theme;
686 },
687
Simon Hunt142d0032014-11-04 20:13:09 -0800688 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800689 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800690 },
691
Simon Huntbb282f52014-11-10 11:08:19 -0800692 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800693 // TODO : implement custom dialogs
694
695 // Consider enhancing alert mechanism to handle multiples
696 // as individually closable.
697 alert: function (msg) {
698 doAlert(msg);
699 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800700
Simon Hunta3dd9572014-11-20 15:22:41 -0800701 flash: function (msg) {
702 libApi.feedback.flash(msg);
703 },
704
Simon Huntc7ee0662014-11-05 16:44:37 -0800705 dataLoadError: function (err, url) {
706 var msg = 'Data Load Error\n\n' +
707 err.status + ' -- ' + err.statusText + '\n\n' +
708 'relative-url: "' + url + '"\n\n' +
709 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800710 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800711 }
Simon Hunt25248912014-11-04 11:25:48 -0800712
713 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800714 };
715
716 // attach instance methods to the view prototype
717 $.extend(View.prototype, viewInstanceMethods);
718
719 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800720 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800721
Simon Hunta5e89142014-11-14 07:00:33 -0800722 var fpConfig = {
723 TR: {
724 side: 'right'
725
726 },
727 TL: {
728 side: 'left'
729 }
730 };
731
Simon Hunt25248912014-11-04 11:25:48 -0800732 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800733 addLib: function (libName, api) {
734 // TODO: validation of args
735 libApi[libName] = api;
736 },
737
Simon Hunt61d04042014-11-11 17:27:16 -0800738 // TODO: implement floating panel as a class
739 // TODO: parameterize position (currently hard-coded to TopRight)
740 /*
741 * Creates div in floating panels block, with the given id.
742 * Returns panel token used to interact with the panel
743 */
744 addFloatingPanel: function (id, position) {
745 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800746 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800747 el,
748 fp;
749
750 if (fpanels[id]) {
751 buildError('Float panel with id "' + id + '" already exists.');
752 return null;
753 }
754
755 el = $floatPanels.append('div')
756 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800757 .attr('class', 'fpanel')
758 .style('opacity', 0);
759
760 // has to be called after el is set.
761 el.style(cfg.side, pxHide());
762
763 function pxShow() {
764 return '20px';
765 }
766 function pxHide() {
767 return (-20 - widthVal()) + 'px';
768 }
769 function widthVal() {
770 return el.style('width').replace(/px$/, '');
771 }
Simon Hunt61d04042014-11-11 17:27:16 -0800772
773 fp = {
774 id: id,
775 el: el,
776 pos: pos,
Simon Hunta5e89142014-11-14 07:00:33 -0800777
Simon Hunt61d04042014-11-11 17:27:16 -0800778 show: function () {
779 console.log('show pane: ' + id);
780 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800781 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800782 .style('opacity', 1);
783 },
784 hide: function () {
785 console.log('hide pane: ' + id);
786 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800787 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800788 .style('opacity', 0);
789 },
790 empty: function () {
791 return el.html('');
792 },
793 append: function (what) {
794 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800795 },
796 width: function (w) {
797 if (w === undefined) {
798 return widthVal();
799 }
800 el.style('width', w);
Simon Hunt61d04042014-11-11 17:27:16 -0800801 }
802 };
803 fpanels[id] = fp;
804 return fp;
805 },
806
Simon Hunt1a9eff92014-11-07 11:06:34 -0800807 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800808 /** @api ui addView( vid, nid, cb )
809 * Adds a view to the UI.
810 * <p>
811 * Views are loaded/unloaded into the view content pane at
812 * appropriate times, by the navigation framework. This method
813 * adds a view to the UI and returns a token object representing
814 * the view. A view's token is always passed as the first
815 * argument to each of the view's life-cycle callback functions.
816 * <p>
817 * Note that if the view is directly referenced by a nav-item,
818 * or in a group of views with one of those views referenced by
819 * a nav-item, then the <i>nid</i> argument can be omitted as
820 * the framework can infer it.
821 * <p>
822 * <i>cb</i> is a plain object containing callback functions:
823 * "preload", "reset", "load", "unload", "resize", "error".
824 * <pre>
825 * function myLoad(view, ctx) { ... }
826 * ...
827 * // short form...
828 * onos.ui.addView('viewId', {
829 * load: myLoad
830 * });
831 * </pre>
832 *
833 * @param vid (string) [*] view ID (a unique DOM element id)
834 * @param nid (string) nav-item ID (a unique DOM element id)
835 * @param cb (object) [*] callbacks object
836 * @return the view token
837 */
838 addView: function (vid, nid, cb) {
839 traceFn('addView', vid);
840 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800841 token;
842 if (view.ok) {
843 views[vid] = view;
844 token = view.token();
845 } else {
846 token = { vid: view.vid, bad: true };
847 }
848 return token;
849 }
850 };
851
Simon Hunt25248912014-11-04 11:25:48 -0800852 // ..........................................................
853 // View API
854
Simon Huntbb282f52014-11-10 11:08:19 -0800855 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800856 viewApi = {
857 /** @api view empty( )
858 * Empties the current view.
859 * <p>
860 * More specifically, removes all DOM elements from the
861 * current view's display div.
862 */
863 empty: function () {
864 if (!current.view) {
865 return;
866 }
867 current.view.$div.html('');
868 }
869 };
870
871 // ..........................................................
872 // Nav API
873 navApi = {
874
875 };
876
877 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800878 // Library API
879 libApi = {
880
881 };
882
883 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800884 // Exported API
885
Simon Hunt195cb382014-11-03 17:50:51 -0800886 // function to be called from index.html to build the ONOS UI
887 function buildOnosUi() {
888 tsB = new Date().getTime();
889 tsI = tsB - tsI; // initialization duration
890
891 console.log('ONOS UI initialized in ' + tsI + 'ms');
892
893 if (built) {
894 throwError("ONOS UI already built!");
895 }
896 built = true;
897
Simon Huntdb9eb072014-11-04 19:12:46 -0800898 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800899
900 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800901 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800902
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800903 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800904 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800905
Simon Hunt195cb382014-11-03 17:50:51 -0800906 // Invoke hashchange callback to navigate to content
907 // indicated by the window location hash.
908 hash();
909
910 // If there were any build errors, report them
911 reportBuildErrors();
912 }
913
Simon Hunt195cb382014-11-03 17:50:51 -0800914 // export the api and build-UI function
915 return {
Simon Hunt25248912014-11-04 11:25:48 -0800916 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800917 lib: libApi,
918 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800919 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800920 buildUi: buildOnosUi,
921 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800922 };
923 };
924
Simon Huntdb9eb072014-11-04 19:12:46 -0800925}(jQuery));