blob: a9cb1b005707f8dc8dc1fd74edd47b04e1cbc1c0 [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 Hunt56ef0fe2014-11-21 08:24:43 -0800416 slash: [keyMap, 'Show / hide keyboard shortcuts'],
417 esc: [escapeKey, 'Dismiss dialog or cancel selections'],
418 T: [toggleTheme, "Toggle theme"]
Thomas Vachuska65368e32014-11-08 16:10:20 -0800419 };
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),
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800479 gk = keyHandler.globalKeys[key],
480 gcb = isF(gk) || (isA(gk) && isF(gk[0])),
Simon Hunt988c6fc2014-11-20 17:43:03 -0800481 vk = keyHandler.viewKeys[key],
482 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(keyHandler.viewFn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800483
484 // global callback?
485 if (gcb && gcb(current.view.token(), key, keyCode, event)) {
486 // if the event was 'handled', we are done
487 return;
488 }
489 // otherwise, let the view callback have a shot
490 if (vcb) {
491 vcb(current.view.token(), key, keyCode, event);
492 }
493 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800494
495 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800496 $alerts.style('display', 'block');
497 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800498 .attr('class', 'close')
499 .text('X')
500 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800501 $alerts.append('pre');
502 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800503 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800504 alerts.open = true;
505 alerts.count = 0;
506 }
507
508 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800509 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800510 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800511 alerts.open = false;
512 }
513
514 function addAlert(msg) {
515 var lines,
516 oldContent;
517
518 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800519 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800520 }
521
522 lines = msg.split('\n');
523 lines[0] += ' '; // spacing so we don't crowd 'X'
524 lines = lines.join('\n');
525
526 if (oldContent) {
527 lines += '\n----\n' + oldContent;
528 }
529
Simon Hunt61d04042014-11-11 17:27:16 -0800530 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800531 alerts.count++;
532 }
533
534 function doAlert(msg) {
535 if (!alerts.open) {
536 createAlerts();
537 }
538 addAlert(msg);
539 }
540
Simon Hunt25248912014-11-04 11:25:48 -0800541 function resize(e) {
542 d3.selectAll('.onosView').call(setViewDimensions);
543 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800544 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800545 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800546 }
547 }
548
549 // ..........................................................
550 // View class
551 // Captures state information about a view.
552
553 // Constructor
554 // vid : view id
555 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800556 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800557 function View(vid) {
558 var av = 'addView(): ',
559 args = Array.prototype.slice.call(arguments),
560 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800561 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800562
563 args.shift(); // first arg is always vid
564 if (typeof args[0] === 'string') { // nid specified
565 nid = args.shift();
566 }
567 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800568
569 this.vid = vid;
570
571 if (validateViewArgs(vid)) {
572 this.nid = nid; // explicit navitem id (can be null)
573 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800574 this.$div = null; // view not yet added to DOM
575 this.radioButtons = null; // no radio buttons yet
576 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800577 }
Simon Hunt195cb382014-11-03 17:50:51 -0800578 }
579
580 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800581 var av = "ui.addView(...): ",
582 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800583 if (typeof vid !== 'string' || !vid) {
584 doError(av + 'vid required');
585 } else if (views[vid]) {
586 doError(av + 'View ID "' + vid + '" already exists');
587 } else {
588 ok = true;
589 }
590 return ok;
591 }
592
593 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800594 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800595 return {
Simon Hunt25248912014-11-04 11:25:48 -0800596 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800597 vid: this.vid,
598 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800599 $div: this.$div,
600
601 // functions
602 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800603 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800604 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800605 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800606 setKeys: this.setKeys,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800607 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800608 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800609 flash: this.flash,
Simon Hunt625dc402014-11-18 10:57:18 -0800610 theme: this.theme
Simon Hunt195cb382014-11-03 17:50:51 -0800611 }
Simon Hunt25248912014-11-04 11:25:48 -0800612 },
613
Simon Huntf67722a2014-11-10 09:32:06 -0800614 // == Life-cycle functions
615 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800616 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800617 var c = ctx || '',
618 fn = isF(this.cb.preload);
619 traceFn('View.preload', this.vid + ', ' + c);
620 if (fn) {
621 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800622 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800623 }
624 },
625
Simon Huntf67722a2014-11-10 09:32:06 -0800626 reset: function (ctx, flags) {
627 var c = ctx || '',
628 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800629 traceFn('View.reset', this.vid);
630 if (fn) {
631 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800632 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800633 } else if (this.cb.reset === true) {
634 // boolean true signifies "clear view"
635 trace(' [true] cleaing view...');
636 viewApi.empty();
637 }
638 },
639
Simon Hunt56d51852014-11-09 13:03:35 -0800640 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800641 var c = ctx || '',
642 fn = isF(this.cb.load);
643 traceFn('View.load', this.vid + ', ' + c);
644 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800645 if (fn) {
646 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800647 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800648 }
649 },
650
Simon Huntf67722a2014-11-10 09:32:06 -0800651 unload: function (ctx, flags) {
652 var c = ctx | '',
653 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800654 traceFn('View.unload', this.vid);
655 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800656 if (fn) {
657 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800658 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800659 }
660 },
661
Simon Hunt56d51852014-11-09 13:03:35 -0800662 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800663 var c = ctx || '',
664 fn = isF(this.cb.resize),
665 w = this.width(),
666 h = this.height();
667 traceFn('View.resize', this.vid + '/' + c +
668 ' [' + w + 'x' + h + ']');
669 if (fn) {
670 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800671 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800672 }
673 },
674
Simon Huntf67722a2014-11-10 09:32:06 -0800675 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800676 var c = ctx || '',
677 fn = isF(this.cb.error);
678 traceFn('View.error', this.vid + ', ' + c);
679 if (fn) {
680 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800681 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800682 }
683 },
684
Simon Huntf67722a2014-11-10 09:32:06 -0800685 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800686 width: function () {
687 return $(this.$div.node()).width();
688 },
689
690 height: function () {
691 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800692 },
Simon Hunt25248912014-11-04 11:25:48 -0800693
Simon Hunt934c3ce2014-11-05 11:45:07 -0800694 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800695 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800696 },
697
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800698 setKeys: function (keyArg) {
699 setKeyBindings(keyArg);
700 },
701
Simon Hunt625dc402014-11-18 10:57:18 -0800702 theme: function () {
703 return current.theme;
704 },
705
Simon Hunt142d0032014-11-04 20:13:09 -0800706 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800707 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800708 },
709
Simon Huntbb282f52014-11-10 11:08:19 -0800710 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800711 // TODO : implement custom dialogs
712
713 // Consider enhancing alert mechanism to handle multiples
714 // as individually closable.
715 alert: function (msg) {
716 doAlert(msg);
717 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800718
Simon Hunta3dd9572014-11-20 15:22:41 -0800719 flash: function (msg) {
720 libApi.feedback.flash(msg);
721 },
722
Simon Huntc7ee0662014-11-05 16:44:37 -0800723 dataLoadError: function (err, url) {
724 var msg = 'Data Load Error\n\n' +
725 err.status + ' -- ' + err.statusText + '\n\n' +
726 'relative-url: "' + url + '"\n\n' +
727 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800728 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800729 }
Simon Hunt25248912014-11-04 11:25:48 -0800730
731 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800732 };
733
734 // attach instance methods to the view prototype
735 $.extend(View.prototype, viewInstanceMethods);
736
737 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800738 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800739
Simon Hunta5e89142014-11-14 07:00:33 -0800740 var fpConfig = {
741 TR: {
742 side: 'right'
743
744 },
745 TL: {
746 side: 'left'
747 }
748 };
749
Simon Hunt25248912014-11-04 11:25:48 -0800750 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800751 addLib: function (libName, api) {
752 // TODO: validation of args
753 libApi[libName] = api;
754 },
755
Simon Hunt61d04042014-11-11 17:27:16 -0800756 // TODO: implement floating panel as a class
757 // TODO: parameterize position (currently hard-coded to TopRight)
758 /*
759 * Creates div in floating panels block, with the given id.
760 * Returns panel token used to interact with the panel
761 */
762 addFloatingPanel: function (id, position) {
763 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800764 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800765 el,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800766 fp,
767 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800768
769 if (fpanels[id]) {
770 buildError('Float panel with id "' + id + '" already exists.');
771 return null;
772 }
773
774 el = $floatPanels.append('div')
775 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800776 .attr('class', 'fpanel')
777 .style('opacity', 0);
778
779 // has to be called after el is set.
780 el.style(cfg.side, pxHide());
781
782 function pxShow() {
783 return '20px';
784 }
785 function pxHide() {
786 return (-20 - widthVal()) + 'px';
787 }
788 function widthVal() {
789 return el.style('width').replace(/px$/, '');
790 }
Simon Hunt61d04042014-11-11 17:27:16 -0800791
792 fp = {
793 id: id,
794 el: el,
795 pos: pos,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800796 isVisible: function () {
797 return on;
798 },
Simon Hunta5e89142014-11-14 07:00:33 -0800799
Simon Hunt61d04042014-11-11 17:27:16 -0800800 show: function () {
801 console.log('show pane: ' + id);
Thomas Vachuska47635c62014-11-22 01:21:36 -0800802 on = true;
Simon Hunt61d04042014-11-11 17:27:16 -0800803 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800804 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800805 .style('opacity', 1);
806 },
807 hide: function () {
808 console.log('hide pane: ' + id);
Thomas Vachuska47635c62014-11-22 01:21:36 -0800809 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800810 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800811 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800812 .style('opacity', 0);
813 },
814 empty: function () {
815 return el.html('');
816 },
817 append: function (what) {
818 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800819 },
820 width: function (w) {
821 if (w === undefined) {
822 return widthVal();
823 }
824 el.style('width', w);
Simon Hunt61d04042014-11-11 17:27:16 -0800825 }
826 };
827 fpanels[id] = fp;
828 return fp;
829 },
830
Simon Hunt1a9eff92014-11-07 11:06:34 -0800831 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800832 /** @api ui addView( vid, nid, cb )
833 * Adds a view to the UI.
834 * <p>
835 * Views are loaded/unloaded into the view content pane at
836 * appropriate times, by the navigation framework. This method
837 * adds a view to the UI and returns a token object representing
838 * the view. A view's token is always passed as the first
839 * argument to each of the view's life-cycle callback functions.
840 * <p>
841 * Note that if the view is directly referenced by a nav-item,
842 * or in a group of views with one of those views referenced by
843 * a nav-item, then the <i>nid</i> argument can be omitted as
844 * the framework can infer it.
845 * <p>
846 * <i>cb</i> is a plain object containing callback functions:
847 * "preload", "reset", "load", "unload", "resize", "error".
848 * <pre>
849 * function myLoad(view, ctx) { ... }
850 * ...
851 * // short form...
852 * onos.ui.addView('viewId', {
853 * load: myLoad
854 * });
855 * </pre>
856 *
857 * @param vid (string) [*] view ID (a unique DOM element id)
858 * @param nid (string) nav-item ID (a unique DOM element id)
859 * @param cb (object) [*] callbacks object
860 * @return the view token
861 */
862 addView: function (vid, nid, cb) {
863 traceFn('addView', vid);
864 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800865 token;
866 if (view.ok) {
867 views[vid] = view;
868 token = view.token();
869 } else {
870 token = { vid: view.vid, bad: true };
871 }
872 return token;
873 }
874 };
875
Simon Hunt25248912014-11-04 11:25:48 -0800876 // ..........................................................
877 // View API
878
Simon Huntbb282f52014-11-10 11:08:19 -0800879 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800880 viewApi = {
881 /** @api view empty( )
882 * Empties the current view.
883 * <p>
884 * More specifically, removes all DOM elements from the
885 * current view's display div.
886 */
887 empty: function () {
888 if (!current.view) {
889 return;
890 }
891 current.view.$div.html('');
892 }
893 };
894
895 // ..........................................................
896 // Nav API
897 navApi = {
898
899 };
900
901 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800902 // Library API
903 libApi = {
904
905 };
906
907 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800908 // Exported API
909
Simon Hunt195cb382014-11-03 17:50:51 -0800910 // function to be called from index.html to build the ONOS UI
911 function buildOnosUi() {
912 tsB = new Date().getTime();
913 tsI = tsB - tsI; // initialization duration
914
915 console.log('ONOS UI initialized in ' + tsI + 'ms');
916
917 if (built) {
918 throwError("ONOS UI already built!");
919 }
920 built = true;
921
Simon Huntdb9eb072014-11-04 19:12:46 -0800922 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800923
924 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800925 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800926
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800927 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800928 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800929
Simon Hunt195cb382014-11-03 17:50:51 -0800930 // Invoke hashchange callback to navigate to content
931 // indicated by the window location hash.
932 hash();
933
934 // If there were any build errors, report them
935 reportBuildErrors();
936 }
937
Simon Hunt195cb382014-11-03 17:50:51 -0800938 // export the api and build-UI function
939 return {
Simon Hunt25248912014-11-04 11:25:48 -0800940 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800941 lib: libApi,
942 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800943 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800944 buildUi: buildOnosUi,
945 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800946 };
947 };
948
Simon Huntdb9eb072014-11-04 19:12:46 -0800949}(jQuery));