blob: ae7119e8daab1a46ea7637e328f40445d8cb3a76 [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';
Simon Hunt988c6fc2014-11-20 17:43:03 -080096 case 191: return 'slash';
Simon Hunt0df1b1d2014-11-04 22:58:29 -080097 default:
98 if ((code >= 48 && code <= 57) ||
99 (code >= 65 && code <= 90)) {
100 return String.fromCharCode(code);
101 } else if (code >= 112 && code <= 123) {
102 return 'F' + (code - 111);
103 }
104 return '.';
105 }
106 }
107
108
Simon Hunt195cb382014-11-03 17:50:51 -0800109 // ..........................................................
110 // Internal functions
111
112 // throw an error
113 function throwError(msg) {
114 // separate function, as we might add tracing here too, later
115 throw new Error(msg);
116 }
117
118 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800119 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800120 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800121 }
122
123 function trace(msg) {
124 if (settings.trace) {
125 console.log(msg);
126 }
127 }
128
129 function traceFn(fn, params) {
130 if (settings.trace) {
131 console.log('*FN* ' + fn + '(...): ' + params);
132 }
Simon Hunt195cb382014-11-03 17:50:51 -0800133 }
134
135 // hash navigation
136 function hash() {
137 var hash = window.location.hash,
138 redo = false,
139 view,
140 t;
141
Simon Hunt25248912014-11-04 11:25:48 -0800142 traceFn('hash', hash);
143
Simon Hunt195cb382014-11-03 17:50:51 -0800144 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800145 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800146 redo = true;
147 }
148
149 t = parseHash(hash);
150 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800151 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800152 }
153
154 view = views[t.vid];
155 if (!view) {
156 doError('No view defined with id: ' + t.vid);
157 }
158
159 if (redo) {
160 window.location.hash = makeHash(t);
161 // the above will result in a hashchange event, invoking
162 // this function again
163 } else {
164 // hash was not modified... navigate to where we need to be
165 navigate(hash, view, t);
166 }
Simon Hunt195cb382014-11-03 17:50:51 -0800167 }
168
169 function parseHash(s) {
170 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800171 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800172 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800173
Simon Hunt56d51852014-11-09 13:03:35 -0800174 // look for use of flags, first
175 var vidctx,
176 vid,
177 ctx,
178 flags,
179 flagMap,
180 m;
181
182 // RE that includes flags ('?flag1,flag2')
183 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800184 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800185 vidctx = m[1];
186 flags = m[2];
187 flagMap = {};
188 } else {
189 // no flags
190 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
191 if (m) {
192 vidctx = m[1];
193 } else {
194 // bad hash
195 return null;
196 }
Simon Hunt195cb382014-11-03 17:50:51 -0800197 }
198
Simon Hunt56d51852014-11-09 13:03:35 -0800199 vidctx = vidctx.split(',');
200 vid = vidctx[0];
201 ctx = vidctx[1];
202 if (flags) {
203 flags.split(',').forEach(function (f) {
204 flagMap[f.trim()] = true;
205 });
206 }
207
208 return {
209 vid: vid.trim(),
210 ctx: ctx ? ctx.trim() : '',
211 flags: flagMap
212 };
213
Simon Hunt195cb382014-11-03 17:50:51 -0800214 }
215
Simon Hunt56d51852014-11-09 13:03:35 -0800216 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800217 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800218 // make a hash string from the given navigation coordinates,
219 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800220 // if t is not an object, then it is a vid
221 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800222 c = ctx || '',
223 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800224
225 if ($.isPlainObject(t)) {
226 h = t.vid;
227 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800228 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800229 }
230
231 if (c) {
232 h += ',' + c;
233 }
Simon Hunt56d51852014-11-09 13:03:35 -0800234 if (f) {
235 h += '?' + d3.map(f).keys().join(',');
236 }
Simon Hunt25248912014-11-04 11:25:48 -0800237 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800238 return h;
239 }
240
241 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800242 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800243 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800244 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800245 createView(view);
246 setView(view, hash, t);
247 }
248
Simon Hunt61d04042014-11-11 17:27:16 -0800249 function buildError(msg) {
250 buildErrors.push(msg);
251 }
252
Simon Hunt195cb382014-11-03 17:50:51 -0800253 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800254 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800255 var nerr = buildErrors.length,
256 errmsg;
257 if (!nerr) {
258 console.log('(no build errors)');
259 } else {
260 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
261 buildErrors.join('\n');
262 doAlert(errmsg);
263 console.error(errmsg);
264 }
Simon Hunt195cb382014-11-03 17:50:51 -0800265 }
266
Simon Hunt25248912014-11-04 11:25:48 -0800267 // returns the reference if it is a function, null otherwise
268 function isF(f) {
269 return $.isFunction(f) ? f : null;
270 }
271
Simon Hunt988c6fc2014-11-20 17:43:03 -0800272 // returns the reference if it is an array, null otherwise
273 function isA(a) {
274 return $.isArray(a) ? a : null;
275 }
276
Simon Hunt195cb382014-11-03 17:50:51 -0800277 // ..........................................................
278 // View life-cycle functions
279
Simon Hunt25248912014-11-04 11:25:48 -0800280 function setViewDimensions(sel) {
281 var w = window.innerWidth,
282 h = window.innerHeight - mastHeight;
283 sel.each(function () {
284 $(this)
285 .css('width', w + 'px')
286 .css('height', h + 'px')
287 });
288 }
289
Simon Hunt195cb382014-11-03 17:50:51 -0800290 function createView(view) {
291 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800292
Simon Hunt195cb382014-11-03 17:50:51 -0800293 // lazy initialization of the view
294 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800295 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800296 $d = $view.append('div')
297 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800298 id: view.vid,
299 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800300 });
Simon Hunt25248912014-11-04 11:25:48 -0800301 setViewDimensions($d);
302 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800303 }
304 }
305
306 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800307 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800308 // set the specified view as current, while invoking the
309 // appropriate life-cycle callbacks
310
Simon Hunt56d51852014-11-09 13:03:35 -0800311 // first, we'll start by closing the alerts pane, if open
312 closeAlerts();
313
Simon Hunt195cb382014-11-03 17:50:51 -0800314 // if there is a current view, and it is not the same as
315 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800316 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800317 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800318
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800319 // detach radio buttons, key handlers, etc.
320 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800321 keyHandler.viewKeys = {};
322 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800323 }
324
325 // cache new view and context
326 current.view = view;
327 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800328 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800329
Simon Hunt195cb382014-11-03 17:50:51 -0800330 // preload is called only once, after the view is in the DOM
331 if (!view.preloaded) {
Simon Hunt56d51852014-11-09 13:03:35 -0800332 view.preload(current.ctx, current.flags);
Simon Hunt25248912014-11-04 11:25:48 -0800333 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800334 }
335
336 // clear the view of stale data
337 view.reset();
338
339 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800340 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800341 }
342
Simon Huntdb9eb072014-11-04 19:12:46 -0800343 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800344 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800345 return view.vid + '-' + id;
346 }
347
348 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800349 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800350 var re = new RegExp('^' + view.vid + '-');
351 return uid.replace(re, '');
352 }
353
Simon Hunt934c3ce2014-11-05 11:45:07 -0800354 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800355 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800356 btnG,
357 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800358
359 // lazily create the buttons...
360 if (!(btnG = view.radioButtons)) {
361 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800362 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800363
364 btnSet.forEach(function (btn, i) {
365 var bid = btn.id || 'b' + i,
366 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800367 uid = makeUid(view, bid),
368 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800369 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800370 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800371 class: 'radio'
372 })
373 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800374
Simon Hunt9462e8c2014-11-14 17:28:09 -0800375 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800376 btnG.buttonDef[uid] = btn;
377
Simon Huntdb9eb072014-11-04 19:12:46 -0800378 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800379 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800380 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800381 }
382 });
383
384 btnG.selectAll('span')
385 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800386 var button = d3.select(this),
387 uid = button.attr('id'),
388 btn = btnG.buttonDef[uid],
389 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800390
391 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800392 btnG.selectAll('span').classed('active', false);
393 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800394 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800395 if (isF(btn.cb)) {
396 btn.cb(view.token(), btn);
397 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800398 }
399 });
400
401 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800402
403 api.selected = function () {
404 return btnG.selected;
405 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800406 }
407
408 // attach the buttons to the masthead
409 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800410 // return an api for interacting with the button set
411 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800412 }
413
Thomas Vachuska65368e32014-11-08 16:10:20 -0800414 function setupGlobalKeys() {
415 keyHandler.globalKeys = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800416 slash: keyMap,
Thomas Vachuska65368e32014-11-08 16:10:20 -0800417 esc: escapeKey,
418 T: toggleTheme
419 };
420 // Masked keys are global key handlers that always return true.
421 // That is, the view will never see the event for that key.
422 keyHandler.maskedKeys = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800423 slash: true,
Thomas Vachuska65368e32014-11-08 16:10:20 -0800424 T: true
425 };
426 }
427
Simon Hunt988c6fc2014-11-20 17:43:03 -0800428 function keyMap(view, key, code, ev) {
429 libApi.keymap.show(keyHandler);
430 return true;
431 }
432
Thomas Vachuska65368e32014-11-08 16:10:20 -0800433 function escapeKey(view, key, code, ev) {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800434 if (libApi.keymap.hide()) {
435 return true;
436 }
Thomas Vachuska65368e32014-11-08 16:10:20 -0800437 if (alerts.open) {
438 closeAlerts();
439 return true;
440 }
441 return false;
442 }
443
444 function toggleTheme(view, key, code, ev) {
445 var body = d3.select('body');
446 current.theme = (current.theme === 'light') ? 'dark' : 'light';
447 body.classed('light dark', false);
448 body.classed(current.theme, true);
449 return true;
450 }
451
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800452 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800453 var viewKeys,
454 masked = [];
455
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800456 if ($.isFunction(keyArg)) {
457 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800458 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800459 } else {
460 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800461 viewKeys = d3.map(keyArg).keys();
462 viewKeys.forEach(function (key) {
463 if (keyHandler.maskedKeys[key]) {
464 masked.push(' Key "' + key + '" is reserved');
465 }
466 });
467
468 if (masked.length) {
469 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
470 }
471 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800472 }
473 }
474
Thomas Vachuska65368e32014-11-08 16:10:20 -0800475 function keyIn() {
476 var event = d3.event,
477 keyCode = event.keyCode,
478 key = whatKey(keyCode),
479 gcb = isF(keyHandler.globalKeys[key]),
Simon Hunt988c6fc2014-11-20 17:43:03 -0800480 vk = keyHandler.viewKeys[key],
481 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(keyHandler.viewFn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800482
483 // global callback?
484 if (gcb && gcb(current.view.token(), key, keyCode, event)) {
485 // if the event was 'handled', we are done
486 return;
487 }
488 // otherwise, let the view callback have a shot
489 if (vcb) {
490 vcb(current.view.token(), key, keyCode, event);
491 }
492 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800493
494 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800495 $alerts.style('display', 'block');
496 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800497 .attr('class', 'close')
498 .text('X')
499 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800500 $alerts.append('pre');
501 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800502 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800503 alerts.open = true;
504 alerts.count = 0;
505 }
506
507 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800508 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800509 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800510 alerts.open = false;
511 }
512
513 function addAlert(msg) {
514 var lines,
515 oldContent;
516
517 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800518 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800519 }
520
521 lines = msg.split('\n');
522 lines[0] += ' '; // spacing so we don't crowd 'X'
523 lines = lines.join('\n');
524
525 if (oldContent) {
526 lines += '\n----\n' + oldContent;
527 }
528
Simon Hunt61d04042014-11-11 17:27:16 -0800529 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800530 alerts.count++;
531 }
532
533 function doAlert(msg) {
534 if (!alerts.open) {
535 createAlerts();
536 }
537 addAlert(msg);
538 }
539
Simon Hunt25248912014-11-04 11:25:48 -0800540 function resize(e) {
541 d3.selectAll('.onosView').call(setViewDimensions);
542 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800543 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800544 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800545 }
546 }
547
548 // ..........................................................
549 // View class
550 // Captures state information about a view.
551
552 // Constructor
553 // vid : view id
554 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800555 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800556 function View(vid) {
557 var av = 'addView(): ',
558 args = Array.prototype.slice.call(arguments),
559 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800560 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800561
562 args.shift(); // first arg is always vid
563 if (typeof args[0] === 'string') { // nid specified
564 nid = args.shift();
565 }
566 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800567
568 this.vid = vid;
569
570 if (validateViewArgs(vid)) {
571 this.nid = nid; // explicit navitem id (can be null)
572 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800573 this.$div = null; // view not yet added to DOM
574 this.radioButtons = null; // no radio buttons yet
575 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800576 }
Simon Hunt195cb382014-11-03 17:50:51 -0800577 }
578
579 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800580 var av = "ui.addView(...): ",
581 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800582 if (typeof vid !== 'string' || !vid) {
583 doError(av + 'vid required');
584 } else if (views[vid]) {
585 doError(av + 'View ID "' + vid + '" already exists');
586 } else {
587 ok = true;
588 }
589 return ok;
590 }
591
592 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800593 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800594 return {
Simon Hunt25248912014-11-04 11:25:48 -0800595 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800596 vid: this.vid,
597 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800598 $div: this.$div,
599
600 // functions
601 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800602 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800603 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800604 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800605 setKeys: this.setKeys,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800606 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800607 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800608 flash: this.flash,
Simon Hunt625dc402014-11-18 10:57:18 -0800609 theme: this.theme
Simon Hunt195cb382014-11-03 17:50:51 -0800610 }
Simon Hunt25248912014-11-04 11:25:48 -0800611 },
612
Simon Huntf67722a2014-11-10 09:32:06 -0800613 // == Life-cycle functions
614 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800615 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800616 var c = ctx || '',
617 fn = isF(this.cb.preload);
618 traceFn('View.preload', this.vid + ', ' + c);
619 if (fn) {
620 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800621 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800622 }
623 },
624
Simon Huntf67722a2014-11-10 09:32:06 -0800625 reset: function (ctx, flags) {
626 var c = ctx || '',
627 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800628 traceFn('View.reset', this.vid);
629 if (fn) {
630 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800631 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800632 } else if (this.cb.reset === true) {
633 // boolean true signifies "clear view"
634 trace(' [true] cleaing view...');
635 viewApi.empty();
636 }
637 },
638
Simon Hunt56d51852014-11-09 13:03:35 -0800639 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800640 var c = ctx || '',
641 fn = isF(this.cb.load);
642 traceFn('View.load', this.vid + ', ' + c);
643 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800644 if (fn) {
645 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800646 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800647 }
648 },
649
Simon Huntf67722a2014-11-10 09:32:06 -0800650 unload: function (ctx, flags) {
651 var c = ctx | '',
652 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800653 traceFn('View.unload', this.vid);
654 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800655 if (fn) {
656 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800657 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800658 }
659 },
660
Simon Hunt56d51852014-11-09 13:03:35 -0800661 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800662 var c = ctx || '',
663 fn = isF(this.cb.resize),
664 w = this.width(),
665 h = this.height();
666 traceFn('View.resize', this.vid + '/' + c +
667 ' [' + w + 'x' + h + ']');
668 if (fn) {
669 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800670 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800671 }
672 },
673
Simon Huntf67722a2014-11-10 09:32:06 -0800674 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800675 var c = ctx || '',
676 fn = isF(this.cb.error);
677 traceFn('View.error', this.vid + ', ' + c);
678 if (fn) {
679 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800680 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800681 }
682 },
683
Simon Huntf67722a2014-11-10 09:32:06 -0800684 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800685 width: function () {
686 return $(this.$div.node()).width();
687 },
688
689 height: function () {
690 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800691 },
Simon Hunt25248912014-11-04 11:25:48 -0800692
Simon Hunt934c3ce2014-11-05 11:45:07 -0800693 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800694 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800695 },
696
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800697 setKeys: function (keyArg) {
698 setKeyBindings(keyArg);
699 },
700
Simon Hunt625dc402014-11-18 10:57:18 -0800701 theme: function () {
702 return current.theme;
703 },
704
Simon Hunt142d0032014-11-04 20:13:09 -0800705 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800706 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800707 },
708
Simon Huntbb282f52014-11-10 11:08:19 -0800709 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800710 // TODO : implement custom dialogs
711
712 // Consider enhancing alert mechanism to handle multiples
713 // as individually closable.
714 alert: function (msg) {
715 doAlert(msg);
716 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800717
Simon Hunta3dd9572014-11-20 15:22:41 -0800718 flash: function (msg) {
719 libApi.feedback.flash(msg);
720 },
721
Simon Huntc7ee0662014-11-05 16:44:37 -0800722 dataLoadError: function (err, url) {
723 var msg = 'Data Load Error\n\n' +
724 err.status + ' -- ' + err.statusText + '\n\n' +
725 'relative-url: "' + url + '"\n\n' +
726 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800727 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800728 }
Simon Hunt25248912014-11-04 11:25:48 -0800729
730 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800731 };
732
733 // attach instance methods to the view prototype
734 $.extend(View.prototype, viewInstanceMethods);
735
736 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800737 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800738
Simon Hunta5e89142014-11-14 07:00:33 -0800739 var fpConfig = {
740 TR: {
741 side: 'right'
742
743 },
744 TL: {
745 side: 'left'
746 }
747 };
748
Simon Hunt25248912014-11-04 11:25:48 -0800749 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800750 addLib: function (libName, api) {
751 // TODO: validation of args
752 libApi[libName] = api;
753 },
754
Simon Hunt61d04042014-11-11 17:27:16 -0800755 // TODO: implement floating panel as a class
756 // TODO: parameterize position (currently hard-coded to TopRight)
757 /*
758 * Creates div in floating panels block, with the given id.
759 * Returns panel token used to interact with the panel
760 */
761 addFloatingPanel: function (id, position) {
762 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800763 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800764 el,
765 fp;
766
767 if (fpanels[id]) {
768 buildError('Float panel with id "' + id + '" already exists.');
769 return null;
770 }
771
772 el = $floatPanels.append('div')
773 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800774 .attr('class', 'fpanel')
775 .style('opacity', 0);
776
777 // has to be called after el is set.
778 el.style(cfg.side, pxHide());
779
780 function pxShow() {
781 return '20px';
782 }
783 function pxHide() {
784 return (-20 - widthVal()) + 'px';
785 }
786 function widthVal() {
787 return el.style('width').replace(/px$/, '');
788 }
Simon Hunt61d04042014-11-11 17:27:16 -0800789
790 fp = {
791 id: id,
792 el: el,
793 pos: pos,
Simon Hunta5e89142014-11-14 07:00:33 -0800794
Simon Hunt61d04042014-11-11 17:27:16 -0800795 show: function () {
796 console.log('show pane: ' + id);
797 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800798 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800799 .style('opacity', 1);
800 },
801 hide: function () {
802 console.log('hide pane: ' + id);
803 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800804 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800805 .style('opacity', 0);
806 },
807 empty: function () {
808 return el.html('');
809 },
810 append: function (what) {
811 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800812 },
813 width: function (w) {
814 if (w === undefined) {
815 return widthVal();
816 }
817 el.style('width', w);
Simon Hunt61d04042014-11-11 17:27:16 -0800818 }
819 };
820 fpanels[id] = fp;
821 return fp;
822 },
823
Simon Hunt1a9eff92014-11-07 11:06:34 -0800824 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800825 /** @api ui addView( vid, nid, cb )
826 * Adds a view to the UI.
827 * <p>
828 * Views are loaded/unloaded into the view content pane at
829 * appropriate times, by the navigation framework. This method
830 * adds a view to the UI and returns a token object representing
831 * the view. A view's token is always passed as the first
832 * argument to each of the view's life-cycle callback functions.
833 * <p>
834 * Note that if the view is directly referenced by a nav-item,
835 * or in a group of views with one of those views referenced by
836 * a nav-item, then the <i>nid</i> argument can be omitted as
837 * the framework can infer it.
838 * <p>
839 * <i>cb</i> is a plain object containing callback functions:
840 * "preload", "reset", "load", "unload", "resize", "error".
841 * <pre>
842 * function myLoad(view, ctx) { ... }
843 * ...
844 * // short form...
845 * onos.ui.addView('viewId', {
846 * load: myLoad
847 * });
848 * </pre>
849 *
850 * @param vid (string) [*] view ID (a unique DOM element id)
851 * @param nid (string) nav-item ID (a unique DOM element id)
852 * @param cb (object) [*] callbacks object
853 * @return the view token
854 */
855 addView: function (vid, nid, cb) {
856 traceFn('addView', vid);
857 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800858 token;
859 if (view.ok) {
860 views[vid] = view;
861 token = view.token();
862 } else {
863 token = { vid: view.vid, bad: true };
864 }
865 return token;
866 }
867 };
868
Simon Hunt25248912014-11-04 11:25:48 -0800869 // ..........................................................
870 // View API
871
Simon Huntbb282f52014-11-10 11:08:19 -0800872 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800873 viewApi = {
874 /** @api view empty( )
875 * Empties the current view.
876 * <p>
877 * More specifically, removes all DOM elements from the
878 * current view's display div.
879 */
880 empty: function () {
881 if (!current.view) {
882 return;
883 }
884 current.view.$div.html('');
885 }
886 };
887
888 // ..........................................................
889 // Nav API
890 navApi = {
891
892 };
893
894 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800895 // Library API
896 libApi = {
897
898 };
899
900 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800901 // Exported API
902
Simon Hunt195cb382014-11-03 17:50:51 -0800903 // function to be called from index.html to build the ONOS UI
904 function buildOnosUi() {
905 tsB = new Date().getTime();
906 tsI = tsB - tsI; // initialization duration
907
908 console.log('ONOS UI initialized in ' + tsI + 'ms');
909
910 if (built) {
911 throwError("ONOS UI already built!");
912 }
913 built = true;
914
Simon Huntdb9eb072014-11-04 19:12:46 -0800915 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800916
917 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800918 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800919
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800920 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800921 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800922
Simon Hunt195cb382014-11-03 17:50:51 -0800923 // Invoke hashchange callback to navigate to content
924 // indicated by the window location hash.
925 hash();
926
927 // If there were any build errors, report them
928 reportBuildErrors();
929 }
930
Simon Hunt195cb382014-11-03 17:50:51 -0800931 // export the api and build-UI function
932 return {
Simon Hunt25248912014-11-04 11:25:48 -0800933 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800934 lib: libApi,
935 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800936 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800937 buildUi: buildOnosUi,
938 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800939 };
940 };
941
Simon Huntdb9eb072014-11-04 19:12:46 -0800942}(jQuery));