blob: 6e808ceb4dbc0ccd77991e73ef10034c5367da70 [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 Hunt8f40cce2014-11-23 15:57:30 -080096 case 187: return 'equals';
97 case 189: return 'dash';
Simon Hunt988c6fc2014-11-20 17:43:03 -080098 case 191: return 'slash';
Simon Hunt0df1b1d2014-11-04 22:58:29 -080099 default:
100 if ((code >= 48 && code <= 57) ||
101 (code >= 65 && code <= 90)) {
102 return String.fromCharCode(code);
103 } else if (code >= 112 && code <= 123) {
104 return 'F' + (code - 111);
105 }
106 return '.';
107 }
108 }
109
110
Simon Hunt195cb382014-11-03 17:50:51 -0800111 // ..........................................................
112 // Internal functions
113
114 // throw an error
115 function throwError(msg) {
116 // separate function, as we might add tracing here too, later
117 throw new Error(msg);
118 }
119
120 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800121 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800122 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800123 }
124
125 function trace(msg) {
126 if (settings.trace) {
127 console.log(msg);
128 }
129 }
130
131 function traceFn(fn, params) {
132 if (settings.trace) {
133 console.log('*FN* ' + fn + '(...): ' + params);
134 }
Simon Hunt195cb382014-11-03 17:50:51 -0800135 }
136
137 // hash navigation
138 function hash() {
139 var hash = window.location.hash,
140 redo = false,
141 view,
142 t;
143
Simon Hunt25248912014-11-04 11:25:48 -0800144 traceFn('hash', hash);
145
Simon Hunt195cb382014-11-03 17:50:51 -0800146 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800147 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800148 redo = true;
149 }
150
151 t = parseHash(hash);
152 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800153 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800154 }
155
156 view = views[t.vid];
157 if (!view) {
158 doError('No view defined with id: ' + t.vid);
159 }
160
161 if (redo) {
162 window.location.hash = makeHash(t);
163 // the above will result in a hashchange event, invoking
164 // this function again
165 } else {
166 // hash was not modified... navigate to where we need to be
167 navigate(hash, view, t);
168 }
Simon Hunt195cb382014-11-03 17:50:51 -0800169 }
170
171 function parseHash(s) {
172 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800173 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800174 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800175
Simon Hunt56d51852014-11-09 13:03:35 -0800176 // look for use of flags, first
177 var vidctx,
178 vid,
179 ctx,
180 flags,
181 flagMap,
182 m;
183
184 // RE that includes flags ('?flag1,flag2')
185 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800186 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800187 vidctx = m[1];
188 flags = m[2];
189 flagMap = {};
190 } else {
191 // no flags
192 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
193 if (m) {
194 vidctx = m[1];
195 } else {
196 // bad hash
197 return null;
198 }
Simon Hunt195cb382014-11-03 17:50:51 -0800199 }
200
Simon Hunt56d51852014-11-09 13:03:35 -0800201 vidctx = vidctx.split(',');
202 vid = vidctx[0];
203 ctx = vidctx[1];
204 if (flags) {
205 flags.split(',').forEach(function (f) {
206 flagMap[f.trim()] = true;
207 });
208 }
209
210 return {
211 vid: vid.trim(),
212 ctx: ctx ? ctx.trim() : '',
213 flags: flagMap
214 };
215
Simon Hunt195cb382014-11-03 17:50:51 -0800216 }
217
Simon Hunt56d51852014-11-09 13:03:35 -0800218 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800219 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800220 // make a hash string from the given navigation coordinates,
221 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800222 // if t is not an object, then it is a vid
223 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800224 c = ctx || '',
225 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800226
227 if ($.isPlainObject(t)) {
228 h = t.vid;
229 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800230 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800231 }
232
233 if (c) {
234 h += ',' + c;
235 }
Simon Hunt56d51852014-11-09 13:03:35 -0800236 if (f) {
237 h += '?' + d3.map(f).keys().join(',');
238 }
Simon Hunt25248912014-11-04 11:25:48 -0800239 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800240 return h;
241 }
242
243 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800244 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800245 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800246 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800247 createView(view);
248 setView(view, hash, t);
249 }
250
Simon Hunt61d04042014-11-11 17:27:16 -0800251 function buildError(msg) {
252 buildErrors.push(msg);
253 }
254
Simon Hunt195cb382014-11-03 17:50:51 -0800255 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800256 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800257 var nerr = buildErrors.length,
258 errmsg;
259 if (!nerr) {
260 console.log('(no build errors)');
261 } else {
262 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
263 buildErrors.join('\n');
264 doAlert(errmsg);
265 console.error(errmsg);
266 }
Simon Hunt195cb382014-11-03 17:50:51 -0800267 }
268
Simon Hunt25248912014-11-04 11:25:48 -0800269 // returns the reference if it is a function, null otherwise
270 function isF(f) {
271 return $.isFunction(f) ? f : null;
272 }
273
Simon Hunt988c6fc2014-11-20 17:43:03 -0800274 // returns the reference if it is an array, null otherwise
275 function isA(a) {
276 return $.isArray(a) ? a : null;
277 }
278
Simon Hunt195cb382014-11-03 17:50:51 -0800279 // ..........................................................
280 // View life-cycle functions
281
Simon Hunt25248912014-11-04 11:25:48 -0800282 function setViewDimensions(sel) {
283 var w = window.innerWidth,
284 h = window.innerHeight - mastHeight;
285 sel.each(function () {
286 $(this)
287 .css('width', w + 'px')
288 .css('height', h + 'px')
289 });
290 }
291
Simon Hunt195cb382014-11-03 17:50:51 -0800292 function createView(view) {
293 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800294
Simon Hunt195cb382014-11-03 17:50:51 -0800295 // lazy initialization of the view
296 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800297 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800298 $d = $view.append('div')
299 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800300 id: view.vid,
301 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800302 });
Simon Hunt25248912014-11-04 11:25:48 -0800303 setViewDimensions($d);
304 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800305 }
306 }
307
308 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800309 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800310 // set the specified view as current, while invoking the
311 // appropriate life-cycle callbacks
312
Simon Hunt56d51852014-11-09 13:03:35 -0800313 // first, we'll start by closing the alerts pane, if open
314 closeAlerts();
315
Simon Hunt195cb382014-11-03 17:50:51 -0800316 // if there is a current view, and it is not the same as
317 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800318 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800319 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800320
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800321 // detach radio buttons, key handlers, etc.
322 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800323 keyHandler.viewKeys = {};
324 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800325 }
326
327 // cache new view and context
328 current.view = view;
329 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800330 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800331
Simon Hunt195cb382014-11-03 17:50:51 -0800332 // preload is called only once, after the view is in the DOM
333 if (!view.preloaded) {
Simon Hunt56d51852014-11-09 13:03:35 -0800334 view.preload(current.ctx, current.flags);
Simon Hunt25248912014-11-04 11:25:48 -0800335 view.preloaded = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800336 }
337
338 // clear the view of stale data
339 view.reset();
340
341 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800342 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800343 }
344
Simon Huntdb9eb072014-11-04 19:12:46 -0800345 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800346 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800347 return view.vid + '-' + id;
348 }
349
350 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800351 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800352 var re = new RegExp('^' + view.vid + '-');
353 return uid.replace(re, '');
354 }
355
Simon Hunt934c3ce2014-11-05 11:45:07 -0800356 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800357 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800358 btnG,
359 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800360
361 // lazily create the buttons...
362 if (!(btnG = view.radioButtons)) {
363 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800364 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800365
366 btnSet.forEach(function (btn, i) {
367 var bid = btn.id || 'b' + i,
368 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800369 uid = makeUid(view, bid),
370 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800371 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800372 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800373 class: 'radio'
374 })
375 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800376
Simon Hunt9462e8c2014-11-14 17:28:09 -0800377 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800378 btnG.buttonDef[uid] = btn;
379
Simon Huntdb9eb072014-11-04 19:12:46 -0800380 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800381 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800382 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800383 }
384 });
385
386 btnG.selectAll('span')
387 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800388 var button = d3.select(this),
389 uid = button.attr('id'),
390 btn = btnG.buttonDef[uid],
391 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800392
393 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800394 btnG.selectAll('span').classed('active', false);
395 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800396 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800397 if (isF(btn.cb)) {
398 btn.cb(view.token(), btn);
399 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800400 }
401 });
402
403 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800404
405 api.selected = function () {
406 return btnG.selected;
407 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800408 }
409
410 // attach the buttons to the masthead
411 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800412 // return an api for interacting with the button set
413 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800414 }
415
Thomas Vachuska65368e32014-11-08 16:10:20 -0800416 function setupGlobalKeys() {
417 keyHandler.globalKeys = {
Simon Hunt5cef9062014-11-24 15:24:35 -0800418 slash: [quickHelp, 'Show / hide Quick Help'],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800419 esc: [escapeKey, 'Dismiss dialog or cancel selections'],
420 T: [toggleTheme, "Toggle theme"]
Thomas Vachuska65368e32014-11-08 16:10:20 -0800421 };
422 // Masked keys are global key handlers that always return true.
423 // That is, the view will never see the event for that key.
424 keyHandler.maskedKeys = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800425 slash: true,
Thomas Vachuska65368e32014-11-08 16:10:20 -0800426 T: true
427 };
428 }
429
Simon Hunt5cef9062014-11-24 15:24:35 -0800430 function quickHelp(view, key, code, ev) {
431 libApi.quickHelp.show(keyHandler);
Simon Hunt988c6fc2014-11-20 17:43:03 -0800432 return true;
433 }
434
Thomas Vachuska65368e32014-11-08 16:10:20 -0800435 function escapeKey(view, key, code, ev) {
436 if (alerts.open) {
437 closeAlerts();
438 return true;
439 }
Simon Hunt5cef9062014-11-24 15:24:35 -0800440 if (libApi.quickHelp.hide()) {
441 return true;
442 }
Thomas Vachuska65368e32014-11-08 16:10:20 -0800443 return false;
444 }
445
446 function toggleTheme(view, key, code, ev) {
447 var body = d3.select('body');
448 current.theme = (current.theme === 'light') ? 'dark' : 'light';
449 body.classed('light dark', false);
450 body.classed(current.theme, true);
Simon Hunt8f40cce2014-11-23 15:57:30 -0800451 theme(view);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800452 return true;
453 }
454
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800455 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800456 var viewKeys,
457 masked = [];
458
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800459 if ($.isFunction(keyArg)) {
460 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800461 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800462 } else {
463 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800464 viewKeys = d3.map(keyArg).keys();
465 viewKeys.forEach(function (key) {
466 if (keyHandler.maskedKeys[key]) {
467 masked.push(' Key "' + key + '" is reserved');
468 }
469 });
470
471 if (masked.length) {
472 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
473 }
474 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800475 }
476 }
477
Thomas Vachuska65368e32014-11-08 16:10:20 -0800478 function keyIn() {
479 var event = d3.event,
480 keyCode = event.keyCode,
481 key = whatKey(keyCode),
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800482 gk = keyHandler.globalKeys[key],
483 gcb = isF(gk) || (isA(gk) && isF(gk[0])),
Simon Hunt988c6fc2014-11-20 17:43:03 -0800484 vk = keyHandler.viewKeys[key],
485 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(keyHandler.viewFn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800486
487 // global callback?
488 if (gcb && gcb(current.view.token(), key, keyCode, event)) {
489 // if the event was 'handled', we are done
490 return;
491 }
492 // otherwise, let the view callback have a shot
493 if (vcb) {
494 vcb(current.view.token(), key, keyCode, event);
495 }
496 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800497
498 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800499 $alerts.style('display', 'block');
500 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800501 .attr('class', 'close')
502 .text('X')
503 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800504 $alerts.append('pre');
505 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800506 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800507 alerts.open = true;
508 alerts.count = 0;
509 }
510
511 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800512 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800513 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800514 alerts.open = false;
515 }
516
517 function addAlert(msg) {
518 var lines,
519 oldContent;
520
521 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800522 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800523 }
524
525 lines = msg.split('\n');
526 lines[0] += ' '; // spacing so we don't crowd 'X'
527 lines = lines.join('\n');
528
529 if (oldContent) {
530 lines += '\n----\n' + oldContent;
531 }
532
Simon Hunt61d04042014-11-11 17:27:16 -0800533 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800534 alerts.count++;
535 }
536
537 function doAlert(msg) {
538 if (!alerts.open) {
539 createAlerts();
540 }
541 addAlert(msg);
542 }
543
Simon Hunt25248912014-11-04 11:25:48 -0800544 function resize(e) {
545 d3.selectAll('.onosView').call(setViewDimensions);
546 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800547 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800548 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800549 }
550 }
551
Simon Hunt8f40cce2014-11-23 15:57:30 -0800552 function theme() {
553 // allow current view to react to theme event...
554 if (current.view) {
555 current.view.theme(current.ctx, current.flags);
556 }
557 }
558
Simon Hunt195cb382014-11-03 17:50:51 -0800559 // ..........................................................
560 // View class
561 // Captures state information about a view.
562
563 // Constructor
564 // vid : view id
565 // nid : id of associated nav-item (optional)
Simon Hunt25248912014-11-04 11:25:48 -0800566 // cb : callbacks (preload, reset, load, unload, resize, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800567 function View(vid) {
568 var av = 'addView(): ',
569 args = Array.prototype.slice.call(arguments),
570 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800571 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800572
573 args.shift(); // first arg is always vid
574 if (typeof args[0] === 'string') { // nid specified
575 nid = args.shift();
576 }
577 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800578
579 this.vid = vid;
580
581 if (validateViewArgs(vid)) {
582 this.nid = nid; // explicit navitem id (can be null)
583 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800584 this.$div = null; // view not yet added to DOM
585 this.radioButtons = null; // no radio buttons yet
586 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800587 }
Simon Hunt195cb382014-11-03 17:50:51 -0800588 }
589
590 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800591 var av = "ui.addView(...): ",
592 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800593 if (typeof vid !== 'string' || !vid) {
594 doError(av + 'vid required');
595 } else if (views[vid]) {
596 doError(av + 'View ID "' + vid + '" already exists');
597 } else {
598 ok = true;
599 }
600 return ok;
601 }
602
603 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800604 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800605 return {
Simon Hunt25248912014-11-04 11:25:48 -0800606 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800607 vid: this.vid,
608 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800609 $div: this.$div,
610
611 // functions
612 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800613 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800614 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800615 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800616 setKeys: this.setKeys,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800617 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800618 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800619 flash: this.flash,
Simon Hunt8f40cce2014-11-23 15:57:30 -0800620 getTheme: this.getTheme
Simon Hunt195cb382014-11-03 17:50:51 -0800621 }
Simon Hunt25248912014-11-04 11:25:48 -0800622 },
623
Simon Huntf67722a2014-11-10 09:32:06 -0800624 // == Life-cycle functions
625 // TODO: factor common code out of life-cycle
Simon Hunt56d51852014-11-09 13:03:35 -0800626 preload: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800627 var c = ctx || '',
628 fn = isF(this.cb.preload);
629 traceFn('View.preload', this.vid + ', ' + c);
630 if (fn) {
631 trace('PRELOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800632 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800633 }
634 },
635
Simon Huntf67722a2014-11-10 09:32:06 -0800636 reset: function (ctx, flags) {
637 var c = ctx || '',
638 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800639 traceFn('View.reset', this.vid);
640 if (fn) {
641 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800642 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800643 } else if (this.cb.reset === true) {
644 // boolean true signifies "clear view"
645 trace(' [true] cleaing view...');
646 viewApi.empty();
647 }
648 },
649
Simon Hunt56d51852014-11-09 13:03:35 -0800650 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800651 var c = ctx || '',
652 fn = isF(this.cb.load);
653 traceFn('View.load', this.vid + ', ' + c);
654 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800655 if (fn) {
656 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800657 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800658 }
659 },
660
Simon Huntf67722a2014-11-10 09:32:06 -0800661 unload: function (ctx, flags) {
662 var c = ctx | '',
663 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800664 traceFn('View.unload', this.vid);
665 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800666 if (fn) {
667 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800668 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800669 }
670 },
671
Simon Hunt56d51852014-11-09 13:03:35 -0800672 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800673 var c = ctx || '',
674 fn = isF(this.cb.resize),
675 w = this.width(),
676 h = this.height();
677 traceFn('View.resize', this.vid + '/' + c +
678 ' [' + w + 'x' + h + ']');
679 if (fn) {
680 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800681 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800682 }
683 },
684
Simon Hunt8f40cce2014-11-23 15:57:30 -0800685 theme: function (ctx, flags) {
686 var c = ctx | '',
687 fn = isF(this.cb.theme);
688 traceFn('View.theme', this.vid);
689 if (fn) {
690 trace('THEME cb for ' + this.vid);
691 fn(this.token(), c, flags);
692 }
693 },
694
Simon Huntf67722a2014-11-10 09:32:06 -0800695 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800696 var c = ctx || '',
697 fn = isF(this.cb.error);
698 traceFn('View.error', this.vid + ', ' + c);
699 if (fn) {
700 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800701 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800702 }
703 },
704
Simon Huntf67722a2014-11-10 09:32:06 -0800705 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800706 width: function () {
707 return $(this.$div.node()).width();
708 },
709
710 height: function () {
711 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800712 },
Simon Hunt25248912014-11-04 11:25:48 -0800713
Simon Hunt934c3ce2014-11-05 11:45:07 -0800714 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800715 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800716 },
717
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800718 setKeys: function (keyArg) {
719 setKeyBindings(keyArg);
720 },
721
Simon Hunt8f40cce2014-11-23 15:57:30 -0800722 getTheme: function () {
Simon Hunt625dc402014-11-18 10:57:18 -0800723 return current.theme;
724 },
725
Simon Hunt142d0032014-11-04 20:13:09 -0800726 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800727 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800728 },
729
Simon Huntbb282f52014-11-10 11:08:19 -0800730 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800731 // TODO : implement custom dialogs
732
733 // Consider enhancing alert mechanism to handle multiples
734 // as individually closable.
735 alert: function (msg) {
736 doAlert(msg);
737 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800738
Simon Hunta3dd9572014-11-20 15:22:41 -0800739 flash: function (msg) {
740 libApi.feedback.flash(msg);
741 },
742
Simon Huntc7ee0662014-11-05 16:44:37 -0800743 dataLoadError: function (err, url) {
744 var msg = 'Data Load Error\n\n' +
745 err.status + ' -- ' + err.statusText + '\n\n' +
746 'relative-url: "' + url + '"\n\n' +
747 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800748 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800749 }
Simon Hunt25248912014-11-04 11:25:48 -0800750
751 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800752 };
753
754 // attach instance methods to the view prototype
755 $.extend(View.prototype, viewInstanceMethods);
756
757 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800758 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800759
Simon Hunta5e89142014-11-14 07:00:33 -0800760 var fpConfig = {
761 TR: {
762 side: 'right'
763
764 },
765 TL: {
766 side: 'left'
767 }
768 };
769
Simon Hunt25248912014-11-04 11:25:48 -0800770 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800771 addLib: function (libName, api) {
772 // TODO: validation of args
773 libApi[libName] = api;
774 },
775
Simon Hunt61d04042014-11-11 17:27:16 -0800776 // TODO: implement floating panel as a class
777 // TODO: parameterize position (currently hard-coded to TopRight)
778 /*
779 * Creates div in floating panels block, with the given id.
780 * Returns panel token used to interact with the panel
781 */
782 addFloatingPanel: function (id, position) {
783 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800784 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800785 el,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800786 fp,
787 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800788
789 if (fpanels[id]) {
790 buildError('Float panel with id "' + id + '" already exists.');
791 return null;
792 }
793
794 el = $floatPanels.append('div')
795 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800796 .attr('class', 'fpanel')
797 .style('opacity', 0);
798
799 // has to be called after el is set.
800 el.style(cfg.side, pxHide());
801
802 function pxShow() {
803 return '20px';
804 }
805 function pxHide() {
806 return (-20 - widthVal()) + 'px';
807 }
Simon Hunt7b403bc2014-11-22 19:01:00 -0800808 function noPx(what) {
809 return el.style(what).replace(/px$/, '');
810 }
Simon Hunta5e89142014-11-14 07:00:33 -0800811 function widthVal() {
Simon Hunt7b403bc2014-11-22 19:01:00 -0800812 return noPx('width');
813 }
814 function heightVal() {
815 return noPx('height');
Simon Hunta5e89142014-11-14 07:00:33 -0800816 }
Simon Hunt61d04042014-11-11 17:27:16 -0800817
818 fp = {
819 id: id,
820 el: el,
821 pos: pos,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800822 isVisible: function () {
823 return on;
824 },
Simon Hunta5e89142014-11-14 07:00:33 -0800825
Simon Hunt61d04042014-11-11 17:27:16 -0800826 show: function () {
827 console.log('show pane: ' + id);
Thomas Vachuska47635c62014-11-22 01:21:36 -0800828 on = true;
Simon Hunt61d04042014-11-11 17:27:16 -0800829 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800830 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800831 .style('opacity', 1);
832 },
833 hide: function () {
834 console.log('hide pane: ' + id);
Thomas Vachuska47635c62014-11-22 01:21:36 -0800835 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800836 el.transition().duration(750)
Simon Hunta5e89142014-11-14 07:00:33 -0800837 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800838 .style('opacity', 0);
839 },
840 empty: function () {
841 return el.html('');
842 },
843 append: function (what) {
844 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800845 },
846 width: function (w) {
847 if (w === undefined) {
848 return widthVal();
849 }
Simon Huntb82f6902014-11-22 11:53:15 -0800850 el.style('width', w + 'px');
Simon Hunt7b403bc2014-11-22 19:01:00 -0800851 },
852 height: function (h) {
853 if (h === undefined) {
854 return heightVal();
855 }
856 el.style('height', h + 'px');
Simon Hunt61d04042014-11-11 17:27:16 -0800857 }
858 };
859 fpanels[id] = fp;
860 return fp;
861 },
862
Simon Hunt1a9eff92014-11-07 11:06:34 -0800863 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800864 /** @api ui addView( vid, nid, cb )
865 * Adds a view to the UI.
866 * <p>
867 * Views are loaded/unloaded into the view content pane at
868 * appropriate times, by the navigation framework. This method
869 * adds a view to the UI and returns a token object representing
870 * the view. A view's token is always passed as the first
871 * argument to each of the view's life-cycle callback functions.
872 * <p>
873 * Note that if the view is directly referenced by a nav-item,
874 * or in a group of views with one of those views referenced by
875 * a nav-item, then the <i>nid</i> argument can be omitted as
876 * the framework can infer it.
877 * <p>
878 * <i>cb</i> is a plain object containing callback functions:
879 * "preload", "reset", "load", "unload", "resize", "error".
880 * <pre>
881 * function myLoad(view, ctx) { ... }
882 * ...
883 * // short form...
884 * onos.ui.addView('viewId', {
885 * load: myLoad
886 * });
887 * </pre>
888 *
889 * @param vid (string) [*] view ID (a unique DOM element id)
890 * @param nid (string) nav-item ID (a unique DOM element id)
891 * @param cb (object) [*] callbacks object
892 * @return the view token
893 */
894 addView: function (vid, nid, cb) {
895 traceFn('addView', vid);
896 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800897 token;
898 if (view.ok) {
899 views[vid] = view;
900 token = view.token();
901 } else {
902 token = { vid: view.vid, bad: true };
903 }
904 return token;
905 }
906 };
907
Simon Hunt25248912014-11-04 11:25:48 -0800908 // ..........................................................
909 // View API
910
Simon Huntbb282f52014-11-10 11:08:19 -0800911 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800912 viewApi = {
913 /** @api view empty( )
914 * Empties the current view.
915 * <p>
916 * More specifically, removes all DOM elements from the
917 * current view's display div.
918 */
919 empty: function () {
920 if (!current.view) {
921 return;
922 }
923 current.view.$div.html('');
924 }
925 };
926
927 // ..........................................................
928 // Nav API
929 navApi = {
930
931 };
932
933 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800934 // Library API
935 libApi = {
936
937 };
938
939 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800940 // Exported API
941
Simon Hunt195cb382014-11-03 17:50:51 -0800942 // function to be called from index.html to build the ONOS UI
943 function buildOnosUi() {
944 tsB = new Date().getTime();
945 tsI = tsB - tsI; // initialization duration
946
947 console.log('ONOS UI initialized in ' + tsI + 'ms');
948
949 if (built) {
950 throwError("ONOS UI already built!");
951 }
952 built = true;
953
Simon Huntdb9eb072014-11-04 19:12:46 -0800954 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800955
956 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800957 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800958
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800959 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800960 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800961
Simon Hunt195cb382014-11-03 17:50:51 -0800962 // Invoke hashchange callback to navigate to content
963 // indicated by the window location hash.
964 hash();
965
966 // If there were any build errors, report them
967 reportBuildErrors();
968 }
969
Simon Hunt195cb382014-11-03 17:50:51 -0800970 // export the api and build-UI function
971 return {
Simon Hunt25248912014-11-04 11:25:48 -0800972 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800973 lib: libApi,
974 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800975 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800976 buildUi: buildOnosUi,
977 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800978 };
979 };
980
Simon Huntdb9eb072014-11-04 19:12:46 -0800981}(jQuery));