blob: 1e9120fa54257f4c69a573ec16e9d6c0b3a01133 [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 Hunt0df1b1d2014-11-04 22:58:29 -0800101 default:
102 if ((code >= 48 && code <= 57) ||
103 (code >= 65 && code <= 90)) {
104 return String.fromCharCode(code);
105 } else if (code >= 112 && code <= 123) {
106 return 'F' + (code - 111);
107 }
108 return '.';
109 }
110 }
111
112
Simon Hunt195cb382014-11-03 17:50:51 -0800113 // ..........................................................
114 // Internal functions
115
116 // throw an error
117 function throwError(msg) {
118 // separate function, as we might add tracing here too, later
119 throw new Error(msg);
120 }
121
122 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800123 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800124 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800125 }
126
127 function trace(msg) {
128 if (settings.trace) {
129 console.log(msg);
130 }
131 }
132
133 function traceFn(fn, params) {
134 if (settings.trace) {
135 console.log('*FN* ' + fn + '(...): ' + params);
136 }
Simon Hunt195cb382014-11-03 17:50:51 -0800137 }
138
139 // hash navigation
140 function hash() {
141 var hash = window.location.hash,
142 redo = false,
143 view,
144 t;
145
Simon Hunt25248912014-11-04 11:25:48 -0800146 traceFn('hash', hash);
147
Simon Hunt195cb382014-11-03 17:50:51 -0800148 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800149 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800150 redo = true;
151 }
152
153 t = parseHash(hash);
154 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800155 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800156 }
157
158 view = views[t.vid];
159 if (!view) {
160 doError('No view defined with id: ' + t.vid);
161 }
162
163 if (redo) {
164 window.location.hash = makeHash(t);
165 // the above will result in a hashchange event, invoking
166 // this function again
167 } else {
168 // hash was not modified... navigate to where we need to be
169 navigate(hash, view, t);
170 }
Simon Hunt195cb382014-11-03 17:50:51 -0800171 }
172
173 function parseHash(s) {
174 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800175 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800176 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800177
Simon Hunt56d51852014-11-09 13:03:35 -0800178 // look for use of flags, first
179 var vidctx,
180 vid,
181 ctx,
182 flags,
183 flagMap,
184 m;
185
186 // RE that includes flags ('?flag1,flag2')
187 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800188 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800189 vidctx = m[1];
190 flags = m[2];
191 flagMap = {};
192 } else {
193 // no flags
194 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
195 if (m) {
196 vidctx = m[1];
197 } else {
198 // bad hash
199 return null;
200 }
Simon Hunt195cb382014-11-03 17:50:51 -0800201 }
202
Simon Hunt56d51852014-11-09 13:03:35 -0800203 vidctx = vidctx.split(',');
204 vid = vidctx[0];
205 ctx = vidctx[1];
206 if (flags) {
207 flags.split(',').forEach(function (f) {
208 flagMap[f.trim()] = true;
209 });
210 }
211
212 return {
213 vid: vid.trim(),
214 ctx: ctx ? ctx.trim() : '',
215 flags: flagMap
216 };
217
Simon Hunt195cb382014-11-03 17:50:51 -0800218 }
219
Simon Hunt56d51852014-11-09 13:03:35 -0800220 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800221 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800222 // make a hash string from the given navigation coordinates,
223 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800224 // if t is not an object, then it is a vid
225 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800226 c = ctx || '',
227 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800228
229 if ($.isPlainObject(t)) {
230 h = t.vid;
231 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800232 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800233 }
234
235 if (c) {
236 h += ',' + c;
237 }
Simon Hunt56d51852014-11-09 13:03:35 -0800238 if (f) {
239 h += '?' + d3.map(f).keys().join(',');
240 }
Simon Hunt25248912014-11-04 11:25:48 -0800241 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800242 return h;
243 }
244
245 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800246 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800247 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800248 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800249 createView(view);
250 setView(view, hash, t);
251 }
252
Simon Hunt61d04042014-11-11 17:27:16 -0800253 function buildError(msg) {
254 buildErrors.push(msg);
255 }
256
Simon Hunt195cb382014-11-03 17:50:51 -0800257 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800258 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800259 var nerr = buildErrors.length,
260 errmsg;
261 if (!nerr) {
262 console.log('(no build errors)');
263 } else {
264 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
265 buildErrors.join('\n');
266 doAlert(errmsg);
267 console.error(errmsg);
268 }
Simon Hunt195cb382014-11-03 17:50:51 -0800269 }
270
Simon Hunt25248912014-11-04 11:25:48 -0800271 // returns the reference if it is a function, null otherwise
272 function isF(f) {
273 return $.isFunction(f) ? f : null;
274 }
275
Simon Hunt988c6fc2014-11-20 17:43:03 -0800276 // returns the reference if it is an array, null otherwise
277 function isA(a) {
278 return $.isArray(a) ? a : null;
279 }
280
Simon Hunt195cb382014-11-03 17:50:51 -0800281 // ..........................................................
282 // View life-cycle functions
283
Simon Hunt25248912014-11-04 11:25:48 -0800284 function setViewDimensions(sel) {
285 var w = window.innerWidth,
286 h = window.innerHeight - mastHeight;
287 sel.each(function () {
288 $(this)
289 .css('width', w + 'px')
290 .css('height', h + 'px')
291 });
292 }
293
Simon Hunt195cb382014-11-03 17:50:51 -0800294 function createView(view) {
295 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800296
Simon Hunt195cb382014-11-03 17:50:51 -0800297 // lazy initialization of the view
298 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800299 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800300 $d = $view.append('div')
301 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800302 id: view.vid,
303 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800304 });
Simon Hunt25248912014-11-04 11:25:48 -0800305 setViewDimensions($d);
306 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800307 }
308 }
309
310 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800311 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800312 // set the specified view as current, while invoking the
313 // appropriate life-cycle callbacks
314
Simon Hunt56d51852014-11-09 13:03:35 -0800315 // first, we'll start by closing the alerts pane, if open
316 closeAlerts();
317
Simon Hunt195cb382014-11-03 17:50:51 -0800318 // if there is a current view, and it is not the same as
319 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800320 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800321 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800322
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800323 // detach radio buttons, key handlers, etc.
324 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800325 keyHandler.viewKeys = {};
326 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800327 }
328
329 // cache new view and context
330 current.view = view;
331 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800332 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800333
Simon Hunta2994cc2014-12-02 14:19:15 -0800334 // init is called only once, after the view is in the DOM
335 if (!view.inited) {
336 view.init(current.ctx, current.flags);
337 view.inited = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800338 }
339
340 // clear the view of stale data
341 view.reset();
342
343 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800344 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800345 }
346
Simon Huntdb9eb072014-11-04 19:12:46 -0800347 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800348 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800349 return view.vid + '-' + id;
350 }
351
352 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800353 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800354 var re = new RegExp('^' + view.vid + '-');
355 return uid.replace(re, '');
356 }
357
Simon Hunt934c3ce2014-11-05 11:45:07 -0800358 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800359 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800360 btnG,
361 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800362
363 // lazily create the buttons...
364 if (!(btnG = view.radioButtons)) {
365 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800366 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800367
368 btnSet.forEach(function (btn, i) {
369 var bid = btn.id || 'b' + i,
370 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800371 uid = makeUid(view, bid),
372 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800373 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800374 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800375 class: 'radio'
376 })
377 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800378
Simon Hunt9462e8c2014-11-14 17:28:09 -0800379 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800380 btnG.buttonDef[uid] = btn;
381
Simon Huntdb9eb072014-11-04 19:12:46 -0800382 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800383 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800384 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800385 }
386 });
387
388 btnG.selectAll('span')
389 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800390 var button = d3.select(this),
391 uid = button.attr('id'),
392 btn = btnG.buttonDef[uid],
393 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800394
395 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800396 btnG.selectAll('span').classed('active', false);
397 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800398 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800399 if (isF(btn.cb)) {
400 btn.cb(view.token(), btn);
401 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800402 }
403 });
404
405 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800406
407 api.selected = function () {
408 return btnG.selected;
409 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800410 }
411
412 // attach the buttons to the masthead
413 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800414 // return an api for interacting with the button set
415 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800416 }
417
Thomas Vachuska65368e32014-11-08 16:10:20 -0800418 function setupGlobalKeys() {
419 keyHandler.globalKeys = {
Simon Hunt5cef9062014-11-24 15:24:35 -0800420 slash: [quickHelp, 'Show / hide Quick Help'],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800421 esc: [escapeKey, 'Dismiss dialog or cancel selections'],
422 T: [toggleTheme, "Toggle theme"]
Thomas Vachuska65368e32014-11-08 16:10:20 -0800423 };
424 // Masked keys are global key handlers that always return true.
425 // That is, the view will never see the event for that key.
426 keyHandler.maskedKeys = {
Simon Hunt988c6fc2014-11-20 17:43:03 -0800427 slash: true,
Thomas Vachuska65368e32014-11-08 16:10:20 -0800428 T: true
429 };
430 }
431
Simon Hunt5cef9062014-11-24 15:24:35 -0800432 function quickHelp(view, key, code, ev) {
433 libApi.quickHelp.show(keyHandler);
Simon Hunt988c6fc2014-11-20 17:43:03 -0800434 return true;
435 }
436
Thomas Vachuska65368e32014-11-08 16:10:20 -0800437 function escapeKey(view, key, code, ev) {
438 if (alerts.open) {
439 closeAlerts();
440 return true;
441 }
Simon Hunt5cef9062014-11-24 15:24:35 -0800442 if (libApi.quickHelp.hide()) {
443 return true;
444 }
Thomas Vachuska65368e32014-11-08 16:10:20 -0800445 return false;
446 }
447
448 function toggleTheme(view, key, code, ev) {
449 var body = d3.select('body');
450 current.theme = (current.theme === 'light') ? 'dark' : 'light';
451 body.classed('light dark', false);
452 body.classed(current.theme, true);
Simon Hunt8f40cce2014-11-23 15:57:30 -0800453 theme(view);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800454 return true;
455 }
456
Simon Hunt87514342014-11-24 16:41:27 -0800457 function setGestureNotes(g) {
458 keyHandler.viewGestures = isA(g) || [];
459 }
460
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800461 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800462 var viewKeys,
463 masked = [];
464
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800465 if ($.isFunction(keyArg)) {
466 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800467 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800468 } else {
469 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800470 viewKeys = d3.map(keyArg).keys();
471 viewKeys.forEach(function (key) {
472 if (keyHandler.maskedKeys[key]) {
473 masked.push(' Key "' + key + '" is reserved');
474 }
475 });
476
477 if (masked.length) {
478 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
479 }
480 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800481 }
482 }
483
Thomas Vachuska65368e32014-11-08 16:10:20 -0800484 function keyIn() {
485 var event = d3.event,
486 keyCode = event.keyCode,
487 key = whatKey(keyCode),
Simon Hunta1162d82014-12-03 14:34:43 -0800488 kh = keyHandler,
489 gk = kh.globalKeys[key],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800490 gcb = isF(gk) || (isA(gk) && isF(gk[0])),
Simon Hunta1162d82014-12-03 14:34:43 -0800491 vk = kh.viewKeys[key],
492 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(kh.viewFn),
493 token = current.view.token();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800494
495 // global callback?
Simon Hunta1162d82014-12-03 14:34:43 -0800496 if (gcb && gcb(token, key, keyCode, event)) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800497 // if the event was 'handled', we are done
498 return;
499 }
500 // otherwise, let the view callback have a shot
501 if (vcb) {
Simon Hunta1162d82014-12-03 14:34:43 -0800502 vcb(token, key, keyCode, event);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800503 }
504 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800505
506 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800507 $alerts.style('display', 'block');
508 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800509 .attr('class', 'close')
510 .text('X')
511 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800512 $alerts.append('pre');
513 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800514 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800515 alerts.open = true;
516 alerts.count = 0;
517 }
518
519 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800520 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800521 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800522 alerts.open = false;
523 }
524
525 function addAlert(msg) {
526 var lines,
527 oldContent;
528
529 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800530 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800531 }
532
533 lines = msg.split('\n');
534 lines[0] += ' '; // spacing so we don't crowd 'X'
535 lines = lines.join('\n');
536
537 if (oldContent) {
538 lines += '\n----\n' + oldContent;
539 }
540
Simon Hunt61d04042014-11-11 17:27:16 -0800541 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800542 alerts.count++;
543 }
544
545 function doAlert(msg) {
546 if (!alerts.open) {
547 createAlerts();
548 }
549 addAlert(msg);
550 }
551
Simon Hunt25248912014-11-04 11:25:48 -0800552 function resize(e) {
553 d3.selectAll('.onosView').call(setViewDimensions);
554 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800555 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800556 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800557 }
558 }
559
Simon Hunt8f40cce2014-11-23 15:57:30 -0800560 function theme() {
561 // allow current view to react to theme event...
562 if (current.view) {
563 current.view.theme(current.ctx, current.flags);
564 }
565 }
566
Simon Hunt195cb382014-11-03 17:50:51 -0800567 // ..........................................................
568 // View class
569 // Captures state information about a view.
570
571 // Constructor
572 // vid : view id
573 // nid : id of associated nav-item (optional)
Simon Hunta2994cc2014-12-02 14:19:15 -0800574 // cb : callbacks (init, reset, load, unload, resize, theme, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800575 function View(vid) {
576 var av = 'addView(): ',
577 args = Array.prototype.slice.call(arguments),
578 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800579 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800580
581 args.shift(); // first arg is always vid
582 if (typeof args[0] === 'string') { // nid specified
583 nid = args.shift();
584 }
585 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800586
587 this.vid = vid;
588
589 if (validateViewArgs(vid)) {
590 this.nid = nid; // explicit navitem id (can be null)
591 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800592 this.$div = null; // view not yet added to DOM
593 this.radioButtons = null; // no radio buttons yet
594 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800595 }
Simon Hunt195cb382014-11-03 17:50:51 -0800596 }
597
598 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800599 var av = "ui.addView(...): ",
600 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800601 if (typeof vid !== 'string' || !vid) {
602 doError(av + 'vid required');
603 } else if (views[vid]) {
604 doError(av + 'View ID "' + vid + '" already exists');
605 } else {
606 ok = true;
607 }
608 return ok;
609 }
610
611 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800612 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800613 return {
Simon Hunt25248912014-11-04 11:25:48 -0800614 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800615 vid: this.vid,
616 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800617 $div: this.$div,
618
619 // functions
620 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800621 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800622 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800623 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800624 setKeys: this.setKeys,
Simon Hunt87514342014-11-24 16:41:27 -0800625 setGestures: this.setGestures,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800626 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800627 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800628 flash: this.flash,
Simon Hunt8f40cce2014-11-23 15:57:30 -0800629 getTheme: this.getTheme
Simon Hunt195cb382014-11-03 17:50:51 -0800630 }
Simon Hunt25248912014-11-04 11:25:48 -0800631 },
632
Simon Huntf67722a2014-11-10 09:32:06 -0800633 // == Life-cycle functions
634 // TODO: factor common code out of life-cycle
Simon Hunta2994cc2014-12-02 14:19:15 -0800635 init: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800636 var c = ctx || '',
Simon Hunta2994cc2014-12-02 14:19:15 -0800637 fn = isF(this.cb.init);
638 traceFn('View.init', this.vid + ', ' + c);
Simon Hunt25248912014-11-04 11:25:48 -0800639 if (fn) {
Simon Hunta2994cc2014-12-02 14:19:15 -0800640 trace('INIT cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800641 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800642 }
643 },
644
Simon Huntf67722a2014-11-10 09:32:06 -0800645 reset: function (ctx, flags) {
646 var c = ctx || '',
647 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800648 traceFn('View.reset', this.vid);
649 if (fn) {
650 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800651 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800652 } else if (this.cb.reset === true) {
653 // boolean true signifies "clear view"
654 trace(' [true] cleaing view...');
655 viewApi.empty();
656 }
657 },
658
Simon Hunt56d51852014-11-09 13:03:35 -0800659 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800660 var c = ctx || '',
661 fn = isF(this.cb.load);
662 traceFn('View.load', this.vid + ', ' + c);
663 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800664 if (fn) {
665 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800666 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800667 }
668 },
669
Simon Huntf67722a2014-11-10 09:32:06 -0800670 unload: function (ctx, flags) {
671 var c = ctx | '',
672 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800673 traceFn('View.unload', this.vid);
674 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800675 if (fn) {
676 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800677 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800678 }
679 },
680
Simon Hunt56d51852014-11-09 13:03:35 -0800681 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800682 var c = ctx || '',
683 fn = isF(this.cb.resize),
684 w = this.width(),
685 h = this.height();
686 traceFn('View.resize', this.vid + '/' + c +
687 ' [' + w + 'x' + h + ']');
688 if (fn) {
689 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800690 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800691 }
692 },
693
Simon Hunt8f40cce2014-11-23 15:57:30 -0800694 theme: function (ctx, flags) {
695 var c = ctx | '',
696 fn = isF(this.cb.theme);
697 traceFn('View.theme', this.vid);
698 if (fn) {
699 trace('THEME cb for ' + this.vid);
700 fn(this.token(), c, flags);
701 }
702 },
703
Simon Huntf67722a2014-11-10 09:32:06 -0800704 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800705 var c = ctx || '',
706 fn = isF(this.cb.error);
707 traceFn('View.error', this.vid + ', ' + c);
708 if (fn) {
709 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800710 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800711 }
712 },
713
Simon Huntf67722a2014-11-10 09:32:06 -0800714 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800715 width: function () {
716 return $(this.$div.node()).width();
717 },
718
719 height: function () {
720 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800721 },
Simon Hunt25248912014-11-04 11:25:48 -0800722
Simon Hunt934c3ce2014-11-05 11:45:07 -0800723 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800724 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800725 },
726
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800727 setKeys: function (keyArg) {
728 setKeyBindings(keyArg);
729 },
730
Simon Hunt87514342014-11-24 16:41:27 -0800731 setGestures: function (g) {
732 setGestureNotes(g);
733 },
734
Simon Hunt8f40cce2014-11-23 15:57:30 -0800735 getTheme: function () {
Simon Hunt625dc402014-11-18 10:57:18 -0800736 return current.theme;
737 },
738
Simon Hunt142d0032014-11-04 20:13:09 -0800739 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800740 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800741 },
742
Simon Huntbb282f52014-11-10 11:08:19 -0800743 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800744 // TODO : implement custom dialogs
745
746 // Consider enhancing alert mechanism to handle multiples
747 // as individually closable.
748 alert: function (msg) {
749 doAlert(msg);
750 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800751
Simon Hunta3dd9572014-11-20 15:22:41 -0800752 flash: function (msg) {
753 libApi.feedback.flash(msg);
754 },
755
Simon Huntc7ee0662014-11-05 16:44:37 -0800756 dataLoadError: function (err, url) {
757 var msg = 'Data Load Error\n\n' +
758 err.status + ' -- ' + err.statusText + '\n\n' +
759 'relative-url: "' + url + '"\n\n' +
760 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800761 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800762 }
Simon Hunt25248912014-11-04 11:25:48 -0800763
764 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800765 };
766
767 // attach instance methods to the view prototype
768 $.extend(View.prototype, viewInstanceMethods);
769
770 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800771 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800772
Simon Hunta5e89142014-11-14 07:00:33 -0800773 var fpConfig = {
774 TR: {
775 side: 'right'
Simon Hunta5e89142014-11-14 07:00:33 -0800776 },
777 TL: {
778 side: 'left'
779 }
780 };
781
Simon Hunt25248912014-11-04 11:25:48 -0800782 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800783 addLib: function (libName, api) {
784 // TODO: validation of args
785 libApi[libName] = api;
786 },
787
Simon Hunt61d04042014-11-11 17:27:16 -0800788 // TODO: implement floating panel as a class
789 // TODO: parameterize position (currently hard-coded to TopRight)
790 /*
791 * Creates div in floating panels block, with the given id.
792 * Returns panel token used to interact with the panel
793 */
794 addFloatingPanel: function (id, position) {
795 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800796 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800797 el,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800798 fp,
799 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800800
801 if (fpanels[id]) {
802 buildError('Float panel with id "' + id + '" already exists.');
803 return null;
804 }
805
806 el = $floatPanels.append('div')
807 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800808 .attr('class', 'fpanel')
809 .style('opacity', 0);
810
811 // has to be called after el is set.
812 el.style(cfg.side, pxHide());
813
814 function pxShow() {
815 return '20px';
816 }
817 function pxHide() {
818 return (-20 - widthVal()) + 'px';
819 }
Simon Hunt7b403bc2014-11-22 19:01:00 -0800820 function noPx(what) {
821 return el.style(what).replace(/px$/, '');
822 }
Simon Hunta5e89142014-11-14 07:00:33 -0800823 function widthVal() {
Simon Hunt7b403bc2014-11-22 19:01:00 -0800824 return noPx('width');
825 }
826 function heightVal() {
827 return noPx('height');
Simon Hunta5e89142014-11-14 07:00:33 -0800828 }
Simon Hunt61d04042014-11-11 17:27:16 -0800829
Simon Hunt06811b72014-11-25 18:54:48 -0800830 function noop() {}
831
Simon Hunt61d04042014-11-11 17:27:16 -0800832 fp = {
833 id: id,
834 el: el,
835 pos: pos,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800836 isVisible: function () {
837 return on;
838 },
Simon Hunta5e89142014-11-14 07:00:33 -0800839
Simon Hunt06811b72014-11-25 18:54:48 -0800840 show: function (cb) {
841 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800842 on = true;
Simon Hunt61d04042014-11-11 17:27:16 -0800843 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800844 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800845 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800846 .style('opacity', 1);
847 },
Simon Hunt06811b72014-11-25 18:54:48 -0800848 hide: function (cb) {
849 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800850 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800851 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800852 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800853 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800854 .style('opacity', 0);
855 },
856 empty: function () {
857 return el.html('');
858 },
859 append: function (what) {
860 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800861 },
862 width: function (w) {
863 if (w === undefined) {
864 return widthVal();
865 }
Simon Huntb82f6902014-11-22 11:53:15 -0800866 el.style('width', w + 'px');
Simon Hunt7b403bc2014-11-22 19:01:00 -0800867 },
868 height: function (h) {
869 if (h === undefined) {
870 return heightVal();
871 }
872 el.style('height', h + 'px');
Simon Hunt61d04042014-11-11 17:27:16 -0800873 }
874 };
875 fpanels[id] = fp;
876 return fp;
877 },
878
Simon Hunt1a9eff92014-11-07 11:06:34 -0800879 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800880 /** @api ui addView( vid, nid, cb )
881 * Adds a view to the UI.
882 * <p>
883 * Views are loaded/unloaded into the view content pane at
884 * appropriate times, by the navigation framework. This method
885 * adds a view to the UI and returns a token object representing
886 * the view. A view's token is always passed as the first
887 * argument to each of the view's life-cycle callback functions.
888 * <p>
889 * Note that if the view is directly referenced by a nav-item,
890 * or in a group of views with one of those views referenced by
891 * a nav-item, then the <i>nid</i> argument can be omitted as
892 * the framework can infer it.
893 * <p>
894 * <i>cb</i> is a plain object containing callback functions:
Simon Hunta2994cc2014-12-02 14:19:15 -0800895 * "init", "reset", "load", "unload", "resize", "theme", "error".
Simon Hunt25248912014-11-04 11:25:48 -0800896 * <pre>
897 * function myLoad(view, ctx) { ... }
898 * ...
899 * // short form...
900 * onos.ui.addView('viewId', {
901 * load: myLoad
902 * });
903 * </pre>
904 *
905 * @param vid (string) [*] view ID (a unique DOM element id)
906 * @param nid (string) nav-item ID (a unique DOM element id)
907 * @param cb (object) [*] callbacks object
908 * @return the view token
909 */
910 addView: function (vid, nid, cb) {
911 traceFn('addView', vid);
912 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800913 token;
914 if (view.ok) {
915 views[vid] = view;
916 token = view.token();
917 } else {
918 token = { vid: view.vid, bad: true };
919 }
920 return token;
921 }
922 };
923
Simon Hunt25248912014-11-04 11:25:48 -0800924 // ..........................................................
925 // View API
926
Simon Huntbb282f52014-11-10 11:08:19 -0800927 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800928 viewApi = {
929 /** @api view empty( )
930 * Empties the current view.
931 * <p>
932 * More specifically, removes all DOM elements from the
933 * current view's display div.
934 */
935 empty: function () {
936 if (!current.view) {
937 return;
938 }
939 current.view.$div.html('');
940 }
941 };
942
943 // ..........................................................
944 // Nav API
945 navApi = {
946
947 };
948
949 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800950 // Library API
951 libApi = {
952
953 };
954
955 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800956 // Exported API
957
Simon Hunt195cb382014-11-03 17:50:51 -0800958 // function to be called from index.html to build the ONOS UI
959 function buildOnosUi() {
960 tsB = new Date().getTime();
961 tsI = tsB - tsI; // initialization duration
962
963 console.log('ONOS UI initialized in ' + tsI + 'ms');
964
965 if (built) {
966 throwError("ONOS UI already built!");
967 }
968 built = true;
969
Simon Huntdb9eb072014-11-04 19:12:46 -0800970 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800971
972 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800973 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800974
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800975 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800976 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800977
Simon Hunt195cb382014-11-03 17:50:51 -0800978 // Invoke hashchange callback to navigate to content
979 // indicated by the window location hash.
980 hash();
981
982 // If there were any build errors, report them
983 reportBuildErrors();
984 }
985
Simon Hunt195cb382014-11-03 17:50:51 -0800986 // export the api and build-UI function
987 return {
Simon Hunt25248912014-11-04 11:25:48 -0800988 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800989 lib: libApi,
990 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800991 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800992 buildUi: buildOnosUi,
993 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800994 };
995 };
996
Simon Huntdb9eb072014-11-04 19:12:46 -0800997}(jQuery));