blob: 89768db8170e7f9eed1da869484c4ccc7d9266ee [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: {},
Simon Hunt87514342014-11-24 16:41:27 -080066 viewFn: null,
67 viewGestures: []
Thomas Vachuska65368e32014-11-08 16:10:20 -080068 },
69 alerts = {
70 open: false,
71 count: 0
Simon Hunt934c3ce2014-11-05 11:45:07 -080072 };
Simon Hunt195cb382014-11-03 17:50:51 -080073
74 // DOM elements etc.
Simon Hunt61d04042014-11-11 17:27:16 -080075 // TODO: verify existence of following elements...
76 var $view = d3.select('#view'),
77 $floatPanels = d3.select('#floatPanels'),
78 $alerts = d3.select('#alerts'),
79 // note, following elements added programmatically...
Simon Huntdb9eb072014-11-04 19:12:46 -080080 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080081
82
Simon Hunt0df1b1d2014-11-04 22:58:29 -080083 function whatKey(code) {
84 switch (code) {
85 case 13: return 'enter';
86 case 16: return 'shift';
87 case 17: return 'ctrl';
88 case 18: return 'alt';
89 case 27: return 'esc';
90 case 32: return 'space';
91 case 37: return 'leftArrow';
92 case 38: return 'upArrow';
93 case 39: return 'rightArrow';
94 case 40: return 'downArrow';
95 case 91: return 'cmdLeft';
96 case 93: return 'cmdRight';
Simon Hunt8f40cce2014-11-23 15:57:30 -080097 case 187: return 'equals';
98 case 189: return 'dash';
Simon Hunt988c6fc2014-11-20 17:43:03 -080099 case 191: return 'slash';
Thomas Vachuska1e68bdd2014-11-29 13:53:10 -0800100 case 192: return 'backQuote';
Simon Hunt41effbe2014-12-04 09:41:44 -0800101 case 220: return 'backSlash';
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800102 default:
103 if ((code >= 48 && code <= 57) ||
104 (code >= 65 && code <= 90)) {
105 return String.fromCharCode(code);
106 } else if (code >= 112 && code <= 123) {
107 return 'F' + (code - 111);
108 }
109 return '.';
110 }
111 }
112
113
Simon Hunt195cb382014-11-03 17:50:51 -0800114 // ..........................................................
115 // Internal functions
116
117 // throw an error
118 function throwError(msg) {
119 // separate function, as we might add tracing here too, later
120 throw new Error(msg);
121 }
122
123 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800124 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800125 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800126 }
127
128 function trace(msg) {
129 if (settings.trace) {
130 console.log(msg);
131 }
132 }
133
134 function traceFn(fn, params) {
135 if (settings.trace) {
136 console.log('*FN* ' + fn + '(...): ' + params);
137 }
Simon Hunt195cb382014-11-03 17:50:51 -0800138 }
139
140 // hash navigation
141 function hash() {
142 var hash = window.location.hash,
143 redo = false,
144 view,
145 t;
146
Simon Hunt25248912014-11-04 11:25:48 -0800147 traceFn('hash', hash);
148
Simon Hunt195cb382014-11-03 17:50:51 -0800149 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800150 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800151 redo = true;
152 }
153
154 t = parseHash(hash);
155 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800156 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800157 }
158
159 view = views[t.vid];
160 if (!view) {
161 doError('No view defined with id: ' + t.vid);
162 }
163
164 if (redo) {
165 window.location.hash = makeHash(t);
166 // the above will result in a hashchange event, invoking
167 // this function again
168 } else {
169 // hash was not modified... navigate to where we need to be
170 navigate(hash, view, t);
171 }
Simon Hunt195cb382014-11-03 17:50:51 -0800172 }
173
174 function parseHash(s) {
175 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800176 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800177 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800178
Simon Hunt56d51852014-11-09 13:03:35 -0800179 // look for use of flags, first
180 var vidctx,
181 vid,
182 ctx,
183 flags,
184 flagMap,
185 m;
186
187 // RE that includes flags ('?flag1,flag2')
188 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800189 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800190 vidctx = m[1];
191 flags = m[2];
192 flagMap = {};
193 } else {
194 // no flags
195 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
196 if (m) {
197 vidctx = m[1];
198 } else {
199 // bad hash
200 return null;
201 }
Simon Hunt195cb382014-11-03 17:50:51 -0800202 }
203
Simon Hunt56d51852014-11-09 13:03:35 -0800204 vidctx = vidctx.split(',');
205 vid = vidctx[0];
206 ctx = vidctx[1];
207 if (flags) {
208 flags.split(',').forEach(function (f) {
209 flagMap[f.trim()] = true;
210 });
211 }
212
213 return {
214 vid: vid.trim(),
215 ctx: ctx ? ctx.trim() : '',
216 flags: flagMap
217 };
218
Simon Hunt195cb382014-11-03 17:50:51 -0800219 }
220
Simon Hunt56d51852014-11-09 13:03:35 -0800221 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800222 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800223 // make a hash string from the given navigation coordinates,
224 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800225 // if t is not an object, then it is a vid
226 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800227 c = ctx || '',
228 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800229
230 if ($.isPlainObject(t)) {
231 h = t.vid;
232 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800233 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800234 }
235
236 if (c) {
237 h += ',' + c;
238 }
Simon Hunt56d51852014-11-09 13:03:35 -0800239 if (f) {
240 h += '?' + d3.map(f).keys().join(',');
241 }
Simon Hunt25248912014-11-04 11:25:48 -0800242 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800243 return h;
244 }
245
246 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800247 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800248 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800249 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800250 createView(view);
251 setView(view, hash, t);
252 }
253
Simon Hunt61d04042014-11-11 17:27:16 -0800254 function buildError(msg) {
255 buildErrors.push(msg);
256 }
257
Simon Hunt195cb382014-11-03 17:50:51 -0800258 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800259 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800260 var nerr = buildErrors.length,
261 errmsg;
262 if (!nerr) {
263 console.log('(no build errors)');
264 } else {
265 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
266 buildErrors.join('\n');
267 doAlert(errmsg);
268 console.error(errmsg);
269 }
Simon Hunt195cb382014-11-03 17:50:51 -0800270 }
271
Simon Hunt25248912014-11-04 11:25:48 -0800272 // returns the reference if it is a function, null otherwise
273 function isF(f) {
274 return $.isFunction(f) ? f : null;
275 }
276
Simon Hunt988c6fc2014-11-20 17:43:03 -0800277 // returns the reference if it is an array, null otherwise
278 function isA(a) {
279 return $.isArray(a) ? a : null;
280 }
281
Simon Hunt195cb382014-11-03 17:50:51 -0800282 // ..........................................................
283 // View life-cycle functions
284
Simon Hunt25248912014-11-04 11:25:48 -0800285 function setViewDimensions(sel) {
286 var w = window.innerWidth,
287 h = window.innerHeight - mastHeight;
288 sel.each(function () {
289 $(this)
290 .css('width', w + 'px')
291 .css('height', h + 'px')
292 });
293 }
294
Simon Hunt195cb382014-11-03 17:50:51 -0800295 function createView(view) {
296 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800297
Simon Hunt195cb382014-11-03 17:50:51 -0800298 // lazy initialization of the view
299 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800300 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800301 $d = $view.append('div')
302 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800303 id: view.vid,
304 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800305 });
Simon Hunt25248912014-11-04 11:25:48 -0800306 setViewDimensions($d);
307 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800308 }
309 }
310
311 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800312 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800313 // set the specified view as current, while invoking the
314 // appropriate life-cycle callbacks
315
Simon Hunt56d51852014-11-09 13:03:35 -0800316 // first, we'll start by closing the alerts pane, if open
317 closeAlerts();
318
Simon Hunt195cb382014-11-03 17:50:51 -0800319 // if there is a current view, and it is not the same as
320 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800321 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800322 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800323
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800324 // detach radio buttons, key handlers, etc.
325 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800326 keyHandler.viewKeys = {};
327 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800328 }
329
330 // cache new view and context
331 current.view = view;
332 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800333 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800334
Simon Hunta2994cc2014-12-02 14:19:15 -0800335 // init is called only once, after the view is in the DOM
336 if (!view.inited) {
337 view.init(current.ctx, current.flags);
338 view.inited = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800339 }
340
341 // clear the view of stale data
342 view.reset();
343
344 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800345 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800346 }
347
Simon Huntdb9eb072014-11-04 19:12:46 -0800348 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800349 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800350 return view.vid + '-' + id;
351 }
352
353 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800354 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800355 var re = new RegExp('^' + view.vid + '-');
356 return uid.replace(re, '');
357 }
358
Simon Hunt934c3ce2014-11-05 11:45:07 -0800359 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800360 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800361 btnG,
362 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800363
364 // lazily create the buttons...
365 if (!(btnG = view.radioButtons)) {
366 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800367 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800368
369 btnSet.forEach(function (btn, i) {
370 var bid = btn.id || 'b' + i,
371 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800372 uid = makeUid(view, bid),
373 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800374 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800375 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800376 class: 'radio'
377 })
378 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800379
Simon Hunt9462e8c2014-11-14 17:28:09 -0800380 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800381 btnG.buttonDef[uid] = btn;
382
Simon Huntdb9eb072014-11-04 19:12:46 -0800383 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800384 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800385 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800386 }
387 });
388
389 btnG.selectAll('span')
390 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800391 var button = d3.select(this),
392 uid = button.attr('id'),
393 btn = btnG.buttonDef[uid],
394 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800395
396 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800397 btnG.selectAll('span').classed('active', false);
398 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800399 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800400 if (isF(btn.cb)) {
401 btn.cb(view.token(), btn);
402 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800403 }
404 });
405
406 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800407
408 api.selected = function () {
409 return btnG.selected;
410 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800411 }
412
413 // attach the buttons to the masthead
414 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800415 // return an api for interacting with the button set
416 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800417 }
418
Thomas Vachuska65368e32014-11-08 16:10:20 -0800419 function setupGlobalKeys() {
420 keyHandler.globalKeys = {
Simon Hunt5cef9062014-11-24 15:24:35 -0800421 slash: [quickHelp, 'Show / hide Quick Help'],
Simon Hunt41effbe2014-12-04 09:41:44 -0800422 backSlash: [quickHelp, 'Show / hide Quick Help'],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800423 esc: [escapeKey, 'Dismiss dialog or cancel selections'],
424 T: [toggleTheme, "Toggle theme"]
Thomas Vachuska65368e32014-11-08 16:10:20 -0800425 };
426 // Masked keys are global key handlers that always return true.
427 // That is, the view will never see the event for that key.
428 keyHandler.maskedKeys = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800429 slash: true,
Simon Hunt41effbe2014-12-04 09:41:44 -0800430 backSlash: true,
Thomas Vachuska65368e32014-11-08 16:10:20 -0800431 T: true
432 };
433 }
434
Simon Hunt5cef9062014-11-24 15:24:35 -0800435 function quickHelp(view, key, code, ev) {
436 libApi.quickHelp.show(keyHandler);
Simon Hunt988c6fc2014-11-20 17:43:03 -0800437 return true;
438 }
439
Thomas Vachuska65368e32014-11-08 16:10:20 -0800440 function escapeKey(view, key, code, ev) {
441 if (alerts.open) {
442 closeAlerts();
443 return true;
444 }
Simon Hunt5cef9062014-11-24 15:24:35 -0800445 if (libApi.quickHelp.hide()) {
446 return true;
447 }
Thomas Vachuska65368e32014-11-08 16:10:20 -0800448 return false;
449 }
450
451 function toggleTheme(view, key, code, ev) {
452 var body = d3.select('body');
453 current.theme = (current.theme === 'light') ? 'dark' : 'light';
454 body.classed('light dark', false);
455 body.classed(current.theme, true);
Simon Hunt8f40cce2014-11-23 15:57:30 -0800456 theme(view);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800457 return true;
458 }
459
Simon Hunt87514342014-11-24 16:41:27 -0800460 function setGestureNotes(g) {
461 keyHandler.viewGestures = isA(g) || [];
462 }
463
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800464 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800465 var viewKeys,
466 masked = [];
467
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800468 if ($.isFunction(keyArg)) {
469 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800470 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800471 } else {
472 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800473 viewKeys = d3.map(keyArg).keys();
474 viewKeys.forEach(function (key) {
475 if (keyHandler.maskedKeys[key]) {
476 masked.push(' Key "' + key + '" is reserved');
477 }
478 });
479
480 if (masked.length) {
481 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
482 }
483 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800484 }
485 }
486
Thomas Vachuska65368e32014-11-08 16:10:20 -0800487 function keyIn() {
488 var event = d3.event,
489 keyCode = event.keyCode,
490 key = whatKey(keyCode),
Simon Hunta1162d82014-12-03 14:34:43 -0800491 kh = keyHandler,
492 gk = kh.globalKeys[key],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800493 gcb = isF(gk) || (isA(gk) && isF(gk[0])),
Simon Hunta1162d82014-12-03 14:34:43 -0800494 vk = kh.viewKeys[key],
495 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(kh.viewFn),
496 token = current.view.token();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800497
498 // global callback?
Simon Hunta1162d82014-12-03 14:34:43 -0800499 if (gcb && gcb(token, key, keyCode, event)) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800500 // if the event was 'handled', we are done
501 return;
502 }
503 // otherwise, let the view callback have a shot
504 if (vcb) {
Simon Hunta1162d82014-12-03 14:34:43 -0800505 vcb(token, key, keyCode, event);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800506 }
507 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800508
509 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800510 $alerts.style('display', 'block');
511 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800512 .attr('class', 'close')
513 .text('X')
514 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800515 $alerts.append('pre');
516 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800517 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800518 alerts.open = true;
519 alerts.count = 0;
520 }
521
522 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800523 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800524 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800525 alerts.open = false;
526 }
527
528 function addAlert(msg) {
529 var lines,
530 oldContent;
531
532 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800533 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800534 }
535
536 lines = msg.split('\n');
537 lines[0] += ' '; // spacing so we don't crowd 'X'
538 lines = lines.join('\n');
539
540 if (oldContent) {
541 lines += '\n----\n' + oldContent;
542 }
543
Simon Hunt61d04042014-11-11 17:27:16 -0800544 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800545 alerts.count++;
546 }
547
548 function doAlert(msg) {
549 if (!alerts.open) {
550 createAlerts();
551 }
552 addAlert(msg);
553 }
554
Simon Hunt25248912014-11-04 11:25:48 -0800555 function resize(e) {
556 d3.selectAll('.onosView').call(setViewDimensions);
557 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800558 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800559 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800560 }
561 }
562
Simon Hunt8f40cce2014-11-23 15:57:30 -0800563 function theme() {
564 // allow current view to react to theme event...
565 if (current.view) {
566 current.view.theme(current.ctx, current.flags);
567 }
568 }
569
Simon Hunt195cb382014-11-03 17:50:51 -0800570 // ..........................................................
571 // View class
572 // Captures state information about a view.
573
574 // Constructor
575 // vid : view id
576 // nid : id of associated nav-item (optional)
Simon Hunta2994cc2014-12-02 14:19:15 -0800577 // cb : callbacks (init, reset, load, unload, resize, theme, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800578 function View(vid) {
579 var av = 'addView(): ',
580 args = Array.prototype.slice.call(arguments),
581 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800582 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800583
584 args.shift(); // first arg is always vid
585 if (typeof args[0] === 'string') { // nid specified
586 nid = args.shift();
587 }
588 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800589
590 this.vid = vid;
591
592 if (validateViewArgs(vid)) {
593 this.nid = nid; // explicit navitem id (can be null)
594 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800595 this.$div = null; // view not yet added to DOM
596 this.radioButtons = null; // no radio buttons yet
597 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800598 }
Simon Hunt195cb382014-11-03 17:50:51 -0800599 }
600
601 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800602 var av = "ui.addView(...): ",
603 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800604 if (typeof vid !== 'string' || !vid) {
605 doError(av + 'vid required');
606 } else if (views[vid]) {
607 doError(av + 'View ID "' + vid + '" already exists');
608 } else {
609 ok = true;
610 }
611 return ok;
612 }
613
614 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800615 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800616 return {
Simon Hunt25248912014-11-04 11:25:48 -0800617 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800618 vid: this.vid,
619 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800620 $div: this.$div,
621
622 // functions
623 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800624 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800625 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800626 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800627 setKeys: this.setKeys,
Simon Hunt87514342014-11-24 16:41:27 -0800628 setGestures: this.setGestures,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800629 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800630 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800631 flash: this.flash,
Simon Hunt8f40cce2014-11-23 15:57:30 -0800632 getTheme: this.getTheme
Simon Hunt195cb382014-11-03 17:50:51 -0800633 }
Simon Hunt25248912014-11-04 11:25:48 -0800634 },
635
Simon Huntf67722a2014-11-10 09:32:06 -0800636 // == Life-cycle functions
637 // TODO: factor common code out of life-cycle
Simon Hunta2994cc2014-12-02 14:19:15 -0800638 init: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800639 var c = ctx || '',
Simon Hunta2994cc2014-12-02 14:19:15 -0800640 fn = isF(this.cb.init);
641 traceFn('View.init', this.vid + ', ' + c);
Simon Hunt25248912014-11-04 11:25:48 -0800642 if (fn) {
Simon Hunta2994cc2014-12-02 14:19:15 -0800643 trace('INIT cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800644 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800645 }
646 },
647
Simon Huntf67722a2014-11-10 09:32:06 -0800648 reset: function (ctx, flags) {
649 var c = ctx || '',
650 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800651 traceFn('View.reset', this.vid);
652 if (fn) {
653 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800654 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800655 } else if (this.cb.reset === true) {
656 // boolean true signifies "clear view"
657 trace(' [true] cleaing view...');
658 viewApi.empty();
659 }
660 },
661
Simon Hunt56d51852014-11-09 13:03:35 -0800662 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800663 var c = ctx || '',
664 fn = isF(this.cb.load);
665 traceFn('View.load', this.vid + ', ' + c);
666 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800667 if (fn) {
668 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800669 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800670 }
671 },
672
Simon Huntf67722a2014-11-10 09:32:06 -0800673 unload: function (ctx, flags) {
674 var c = ctx | '',
675 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800676 traceFn('View.unload', this.vid);
677 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800678 if (fn) {
679 trace('UNLOAD 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 Hunt56d51852014-11-09 13:03:35 -0800684 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800685 var c = ctx || '',
686 fn = isF(this.cb.resize),
687 w = this.width(),
688 h = this.height();
689 traceFn('View.resize', this.vid + '/' + c +
690 ' [' + w + 'x' + h + ']');
691 if (fn) {
692 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800693 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800694 }
695 },
696
Simon Hunt8f40cce2014-11-23 15:57:30 -0800697 theme: function (ctx, flags) {
698 var c = ctx | '',
699 fn = isF(this.cb.theme);
700 traceFn('View.theme', this.vid);
701 if (fn) {
702 trace('THEME cb for ' + this.vid);
703 fn(this.token(), c, flags);
704 }
705 },
706
Simon Huntf67722a2014-11-10 09:32:06 -0800707 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800708 var c = ctx || '',
709 fn = isF(this.cb.error);
710 traceFn('View.error', this.vid + ', ' + c);
711 if (fn) {
712 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800713 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800714 }
715 },
716
Simon Huntf67722a2014-11-10 09:32:06 -0800717 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800718 width: function () {
719 return $(this.$div.node()).width();
720 },
721
722 height: function () {
723 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800724 },
Simon Hunt25248912014-11-04 11:25:48 -0800725
Simon Hunt934c3ce2014-11-05 11:45:07 -0800726 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800727 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800728 },
729
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800730 setKeys: function (keyArg) {
731 setKeyBindings(keyArg);
732 },
733
Simon Hunt87514342014-11-24 16:41:27 -0800734 setGestures: function (g) {
735 setGestureNotes(g);
736 },
737
Simon Hunt8f40cce2014-11-23 15:57:30 -0800738 getTheme: function () {
Simon Hunt625dc402014-11-18 10:57:18 -0800739 return current.theme;
740 },
741
Simon Hunt142d0032014-11-04 20:13:09 -0800742 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800743 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800744 },
745
Simon Huntbb282f52014-11-10 11:08:19 -0800746 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800747 // TODO : implement custom dialogs
748
749 // Consider enhancing alert mechanism to handle multiples
750 // as individually closable.
751 alert: function (msg) {
752 doAlert(msg);
753 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800754
Simon Hunta3dd9572014-11-20 15:22:41 -0800755 flash: function (msg) {
756 libApi.feedback.flash(msg);
757 },
758
Simon Huntc7ee0662014-11-05 16:44:37 -0800759 dataLoadError: function (err, url) {
760 var msg = 'Data Load Error\n\n' +
761 err.status + ' -- ' + err.statusText + '\n\n' +
762 'relative-url: "' + url + '"\n\n' +
763 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800764 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800765 }
Simon Hunt25248912014-11-04 11:25:48 -0800766
767 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800768 };
769
770 // attach instance methods to the view prototype
771 $.extend(View.prototype, viewInstanceMethods);
772
773 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800774 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800775
Simon Hunta5e89142014-11-14 07:00:33 -0800776 var fpConfig = {
777 TR: {
778 side: 'right'
Simon Hunta5e89142014-11-14 07:00:33 -0800779 },
780 TL: {
781 side: 'left'
782 }
783 };
784
Simon Hunt25248912014-11-04 11:25:48 -0800785 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800786 addLib: function (libName, api) {
787 // TODO: validation of args
788 libApi[libName] = api;
789 },
790
Simon Hunt61d04042014-11-11 17:27:16 -0800791 // TODO: implement floating panel as a class
792 // TODO: parameterize position (currently hard-coded to TopRight)
793 /*
794 * Creates div in floating panels block, with the given id.
795 * Returns panel token used to interact with the panel
796 */
797 addFloatingPanel: function (id, position) {
798 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800799 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800800 el,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800801 fp,
802 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800803
804 if (fpanels[id]) {
805 buildError('Float panel with id "' + id + '" already exists.');
806 return null;
807 }
808
809 el = $floatPanels.append('div')
810 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800811 .attr('class', 'fpanel')
812 .style('opacity', 0);
813
814 // has to be called after el is set.
815 el.style(cfg.side, pxHide());
816
817 function pxShow() {
818 return '20px';
819 }
820 function pxHide() {
821 return (-20 - widthVal()) + 'px';
822 }
Simon Hunt7b403bc2014-11-22 19:01:00 -0800823 function noPx(what) {
824 return el.style(what).replace(/px$/, '');
825 }
Simon Hunta5e89142014-11-14 07:00:33 -0800826 function widthVal() {
Simon Hunt7b403bc2014-11-22 19:01:00 -0800827 return noPx('width');
828 }
829 function heightVal() {
830 return noPx('height');
Simon Hunta5e89142014-11-14 07:00:33 -0800831 }
Simon Hunt61d04042014-11-11 17:27:16 -0800832
Simon Hunt06811b72014-11-25 18:54:48 -0800833 function noop() {}
834
Simon Hunt61d04042014-11-11 17:27:16 -0800835 fp = {
836 id: id,
837 el: el,
838 pos: pos,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800839 isVisible: function () {
840 return on;
841 },
Simon Hunta5e89142014-11-14 07:00:33 -0800842
Simon Hunt06811b72014-11-25 18:54:48 -0800843 show: function (cb) {
844 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800845 on = true;
Simon Hunt61d04042014-11-11 17:27:16 -0800846 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800847 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800848 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800849 .style('opacity', 1);
850 },
Simon Hunt06811b72014-11-25 18:54:48 -0800851 hide: function (cb) {
852 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800853 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800854 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800855 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800856 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800857 .style('opacity', 0);
858 },
859 empty: function () {
860 return el.html('');
861 },
862 append: function (what) {
863 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800864 },
865 width: function (w) {
866 if (w === undefined) {
867 return widthVal();
868 }
Simon Huntb82f6902014-11-22 11:53:15 -0800869 el.style('width', w + 'px');
Simon Hunt7b403bc2014-11-22 19:01:00 -0800870 },
871 height: function (h) {
872 if (h === undefined) {
873 return heightVal();
874 }
875 el.style('height', h + 'px');
Simon Hunt61d04042014-11-11 17:27:16 -0800876 }
877 };
878 fpanels[id] = fp;
879 return fp;
880 },
881
Simon Hunt1a9eff92014-11-07 11:06:34 -0800882 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800883 /** @api ui addView( vid, nid, cb )
884 * Adds a view to the UI.
885 * <p>
886 * Views are loaded/unloaded into the view content pane at
887 * appropriate times, by the navigation framework. This method
888 * adds a view to the UI and returns a token object representing
889 * the view. A view's token is always passed as the first
890 * argument to each of the view's life-cycle callback functions.
891 * <p>
892 * Note that if the view is directly referenced by a nav-item,
893 * or in a group of views with one of those views referenced by
894 * a nav-item, then the <i>nid</i> argument can be omitted as
895 * the framework can infer it.
896 * <p>
897 * <i>cb</i> is a plain object containing callback functions:
Simon Hunta2994cc2014-12-02 14:19:15 -0800898 * "init", "reset", "load", "unload", "resize", "theme", "error".
Simon Hunt25248912014-11-04 11:25:48 -0800899 * <pre>
900 * function myLoad(view, ctx) { ... }
901 * ...
902 * // short form...
903 * onos.ui.addView('viewId', {
904 * load: myLoad
905 * });
906 * </pre>
907 *
908 * @param vid (string) [*] view ID (a unique DOM element id)
909 * @param nid (string) nav-item ID (a unique DOM element id)
910 * @param cb (object) [*] callbacks object
911 * @return the view token
912 */
913 addView: function (vid, nid, cb) {
914 traceFn('addView', vid);
915 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800916 token;
917 if (view.ok) {
918 views[vid] = view;
919 token = view.token();
920 } else {
921 token = { vid: view.vid, bad: true };
922 }
923 return token;
924 }
925 };
926
Simon Hunt25248912014-11-04 11:25:48 -0800927 // ..........................................................
928 // View API
929
Simon Huntbb282f52014-11-10 11:08:19 -0800930 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800931 viewApi = {
932 /** @api view empty( )
933 * Empties the current view.
934 * <p>
935 * More specifically, removes all DOM elements from the
936 * current view's display div.
937 */
938 empty: function () {
939 if (!current.view) {
940 return;
941 }
942 current.view.$div.html('');
943 }
944 };
945
946 // ..........................................................
947 // Nav API
948 navApi = {
949
950 };
951
952 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800953 // Library API
954 libApi = {
955
956 };
957
958 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800959 // Exported API
960
Simon Hunt195cb382014-11-03 17:50:51 -0800961 // function to be called from index.html to build the ONOS UI
962 function buildOnosUi() {
963 tsB = new Date().getTime();
964 tsI = tsB - tsI; // initialization duration
965
966 console.log('ONOS UI initialized in ' + tsI + 'ms');
967
968 if (built) {
969 throwError("ONOS UI already built!");
970 }
971 built = true;
972
Simon Huntdb9eb072014-11-04 19:12:46 -0800973 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800974
975 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800976 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800977
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800978 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800979 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800980
Simon Hunt195cb382014-11-03 17:50:51 -0800981 // Invoke hashchange callback to navigate to content
982 // indicated by the window location hash.
983 hash();
984
985 // If there were any build errors, report them
986 reportBuildErrors();
987 }
988
Simon Hunt195cb382014-11-03 17:50:51 -0800989 // export the api and build-UI function
990 return {
Simon Hunt25248912014-11-04 11:25:48 -0800991 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800992 lib: libApi,
993 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800994 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800995 buildUi: buildOnosUi,
996 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800997 };
998 };
999
Simon Huntdb9eb072014-11-04 19:12:46 -08001000}(jQuery));