blob: c122177dfd10a8f0cf9339959161f5b5822173be [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
Simon Hunt195cb382014-11-03 17:50:51 -080019 */
20
21(function ($) {
22 'use strict';
23 var tsI = new Date().getTime(), // initialize time stamp
24 tsB, // build time stamp
Simon Hunt25248912014-11-04 11:25:48 -080025 mastHeight = 36, // see mast2.css
Simon Hunt142d0032014-11-04 20:13:09 -080026 defaultVid = 'sample';
Simon Hunt195cb382014-11-03 17:50:51 -080027
28
29 // attach our main function to the jQuery object
30 $.onos = function (options) {
Simon Hunt25248912014-11-04 11:25:48 -080031 var uiApi,
32 viewApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -080033 navApi,
Simon Huntbb282f52014-11-10 11:08:19 -080034 libApi,
35 exported = {};
Simon Hunt25248912014-11-04 11:25:48 -080036
37 var defaultOptions = {
Simon Hunt142d0032014-11-04 20:13:09 -080038 trace: false,
Thomas Vachuskaece59ee2014-11-19 19:06:11 -080039 theme: 'dark',
Simon Hunt142d0032014-11-04 20:13:09 -080040 startVid: defaultVid
Simon Hunt25248912014-11-04 11:25:48 -080041 };
42
43 // compute runtime settings
44 var settings = $.extend({}, defaultOptions, options);
Simon Hunt195cb382014-11-03 17:50:51 -080045
Thomas Vachuska65368e32014-11-08 16:10:20 -080046 // set the selected theme
47 d3.select('body').classed(settings.theme, true);
48
Simon Hunt195cb382014-11-03 17:50:51 -080049 // internal state
50 var views = {},
Simon Hunt61d04042014-11-11 17:27:16 -080051 fpanels = {},
Simon Hunt195cb382014-11-03 17:50:51 -080052 current = {
53 view: null,
Thomas Vachuska65368e32014-11-08 16:10:20 -080054 ctx: '',
Simon Hunt56d51852014-11-09 13:03:35 -080055 flags: {},
Thomas Vachuska65368e32014-11-08 16:10:20 -080056 theme: settings.theme
Simon Hunt195cb382014-11-03 17:50:51 -080057 },
58 built = false,
Simon Hunt61d04042014-11-11 17:27:16 -080059 buildErrors = [],
Simon Hunt934c3ce2014-11-05 11:45:07 -080060 keyHandler = {
Thomas Vachuska65368e32014-11-08 16:10:20 -080061 globalKeys: {},
62 maskedKeys: {},
63 viewKeys: {},
Simon Hunt87514342014-11-24 16:41:27 -080064 viewFn: null,
65 viewGestures: []
Thomas Vachuska65368e32014-11-08 16:10:20 -080066 },
67 alerts = {
68 open: false,
69 count: 0
Simon Hunt934c3ce2014-11-05 11:45:07 -080070 };
Simon Hunt195cb382014-11-03 17:50:51 -080071
72 // DOM elements etc.
Simon Hunt61d04042014-11-11 17:27:16 -080073 // TODO: verify existence of following elements...
74 var $view = d3.select('#view'),
75 $floatPanels = d3.select('#floatPanels'),
76 $alerts = d3.select('#alerts'),
77 // note, following elements added programmatically...
Simon Huntdb9eb072014-11-04 19:12:46 -080078 $mastRadio;
Simon Hunt195cb382014-11-03 17:50:51 -080079
80
Simon Hunt0df1b1d2014-11-04 22:58:29 -080081 function whatKey(code) {
82 switch (code) {
83 case 13: return 'enter';
84 case 16: return 'shift';
85 case 17: return 'ctrl';
86 case 18: return 'alt';
87 case 27: return 'esc';
88 case 32: return 'space';
89 case 37: return 'leftArrow';
90 case 38: return 'upArrow';
91 case 39: return 'rightArrow';
92 case 40: return 'downArrow';
93 case 91: return 'cmdLeft';
94 case 93: return 'cmdRight';
Simon Hunt8f40cce2014-11-23 15:57:30 -080095 case 187: return 'equals';
96 case 189: return 'dash';
Simon Hunt988c6fc2014-11-20 17:43:03 -080097 case 191: return 'slash';
Thomas Vachuska1e68bdd2014-11-29 13:53:10 -080098 case 192: return 'backQuote';
Simon Hunt41effbe2014-12-04 09:41:44 -080099 case 220: return 'backSlash';
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800100 default:
101 if ((code >= 48 && code <= 57) ||
102 (code >= 65 && code <= 90)) {
103 return String.fromCharCode(code);
104 } else if (code >= 112 && code <= 123) {
105 return 'F' + (code - 111);
106 }
107 return '.';
108 }
109 }
110
111
Simon Hunt195cb382014-11-03 17:50:51 -0800112 // ..........................................................
113 // Internal functions
114
115 // throw an error
116 function throwError(msg) {
117 // separate function, as we might add tracing here too, later
118 throw new Error(msg);
119 }
120
121 function doError(msg) {
Simon Hunt25248912014-11-04 11:25:48 -0800122 console.error(msg);
Simon Hunt56d51852014-11-09 13:03:35 -0800123 doAlert(msg);
Simon Hunt25248912014-11-04 11:25:48 -0800124 }
125
126 function trace(msg) {
127 if (settings.trace) {
128 console.log(msg);
129 }
130 }
131
132 function traceFn(fn, params) {
133 if (settings.trace) {
134 console.log('*FN* ' + fn + '(...): ' + params);
135 }
Simon Hunt195cb382014-11-03 17:50:51 -0800136 }
137
138 // hash navigation
139 function hash() {
140 var hash = window.location.hash,
141 redo = false,
142 view,
143 t;
144
Simon Hunt25248912014-11-04 11:25:48 -0800145 traceFn('hash', hash);
146
Simon Hunt195cb382014-11-03 17:50:51 -0800147 if (!hash) {
Simon Hunt142d0032014-11-04 20:13:09 -0800148 hash = settings.startVid;
Simon Hunt195cb382014-11-03 17:50:51 -0800149 redo = true;
150 }
151
152 t = parseHash(hash);
153 if (!t || !t.vid) {
Simon Hunt56d51852014-11-09 13:03:35 -0800154 doError('Unable to parse target hash: "' + hash + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800155 }
156
157 view = views[t.vid];
158 if (!view) {
159 doError('No view defined with id: ' + t.vid);
160 }
161
162 if (redo) {
163 window.location.hash = makeHash(t);
164 // the above will result in a hashchange event, invoking
165 // this function again
166 } else {
167 // hash was not modified... navigate to where we need to be
168 navigate(hash, view, t);
169 }
Simon Hunt195cb382014-11-03 17:50:51 -0800170 }
171
172 function parseHash(s) {
173 // extract navigation coordinates from the supplied string
Simon Hunt56d51852014-11-09 13:03:35 -0800174 // "vid,ctx?flag1,flag2" --> { vid:vid, ctx:ctx, flags:{...} }
Simon Hunt25248912014-11-04 11:25:48 -0800175 traceFn('parseHash', s);
Simon Hunt195cb382014-11-03 17:50:51 -0800176
Simon Hunt56d51852014-11-09 13:03:35 -0800177 // look for use of flags, first
178 var vidctx,
179 vid,
180 ctx,
181 flags,
182 flagMap,
183 m;
184
185 // RE that includes flags ('?flag1,flag2')
186 m = /^[#]{0,1}(.+)\?(.+)$/.exec(s);
Simon Hunt195cb382014-11-03 17:50:51 -0800187 if (m) {
Simon Hunt56d51852014-11-09 13:03:35 -0800188 vidctx = m[1];
189 flags = m[2];
190 flagMap = {};
191 } else {
192 // no flags
193 m = /^[#]{0,1}((.+)(,.+)*)$/.exec(s);
194 if (m) {
195 vidctx = m[1];
196 } else {
197 // bad hash
198 return null;
199 }
Simon Hunt195cb382014-11-03 17:50:51 -0800200 }
201
Simon Hunt56d51852014-11-09 13:03:35 -0800202 vidctx = vidctx.split(',');
203 vid = vidctx[0];
204 ctx = vidctx[1];
205 if (flags) {
206 flags.split(',').forEach(function (f) {
207 flagMap[f.trim()] = true;
208 });
209 }
210
211 return {
212 vid: vid.trim(),
213 ctx: ctx ? ctx.trim() : '',
214 flags: flagMap
215 };
216
Simon Hunt195cb382014-11-03 17:50:51 -0800217 }
218
Simon Hunt56d51852014-11-09 13:03:35 -0800219 function makeHash(t, ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800220 traceFn('makeHash');
Simon Hunt56d51852014-11-09 13:03:35 -0800221 // make a hash string from the given navigation coordinates,
222 // and optional flags map.
Simon Hunt195cb382014-11-03 17:50:51 -0800223 // if t is not an object, then it is a vid
224 var h = t,
Simon Hunt56d51852014-11-09 13:03:35 -0800225 c = ctx || '',
226 f = $.isPlainObject(flags) ? flags : null;
Simon Hunt195cb382014-11-03 17:50:51 -0800227
228 if ($.isPlainObject(t)) {
229 h = t.vid;
230 c = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800231 f = t.flags || null;
Simon Hunt195cb382014-11-03 17:50:51 -0800232 }
233
234 if (c) {
235 h += ',' + c;
236 }
Simon Hunt56d51852014-11-09 13:03:35 -0800237 if (f) {
238 h += '?' + d3.map(f).keys().join(',');
239 }
Simon Hunt25248912014-11-04 11:25:48 -0800240 trace('hash = "' + h + '"');
Simon Hunt195cb382014-11-03 17:50:51 -0800241 return h;
242 }
243
244 function navigate(hash, view, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800245 traceFn('navigate', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800246 // closePanes() // flyouts etc.
Simon Hunt25248912014-11-04 11:25:48 -0800247 // updateNav() // accordion / selected nav item etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800248 createView(view);
249 setView(view, hash, t);
250 }
251
Simon Hunt61d04042014-11-11 17:27:16 -0800252 function buildError(msg) {
253 buildErrors.push(msg);
254 }
255
Simon Hunt195cb382014-11-03 17:50:51 -0800256 function reportBuildErrors() {
Simon Hunt25248912014-11-04 11:25:48 -0800257 traceFn('reportBuildErrors');
Simon Hunt61d04042014-11-11 17:27:16 -0800258 var nerr = buildErrors.length,
259 errmsg;
260 if (!nerr) {
261 console.log('(no build errors)');
262 } else {
263 errmsg = 'Build errors: ' + nerr + ' found...\n\n' +
264 buildErrors.join('\n');
265 doAlert(errmsg);
266 console.error(errmsg);
267 }
Simon Hunt195cb382014-11-03 17:50:51 -0800268 }
269
Simon Hunt25248912014-11-04 11:25:48 -0800270 // returns the reference if it is a function, null otherwise
271 function isF(f) {
272 return $.isFunction(f) ? f : null;
273 }
274
Simon Hunt988c6fc2014-11-20 17:43:03 -0800275 // returns the reference if it is an array, null otherwise
276 function isA(a) {
277 return $.isArray(a) ? a : null;
278 }
279
Simon Hunt195cb382014-11-03 17:50:51 -0800280 // ..........................................................
281 // View life-cycle functions
282
Simon Hunt25248912014-11-04 11:25:48 -0800283 function setViewDimensions(sel) {
284 var w = window.innerWidth,
285 h = window.innerHeight - mastHeight;
286 sel.each(function () {
287 $(this)
288 .css('width', w + 'px')
289 .css('height', h + 'px')
290 });
291 }
292
Simon Hunt195cb382014-11-03 17:50:51 -0800293 function createView(view) {
294 var $d;
Simon Hunt25248912014-11-04 11:25:48 -0800295
Simon Hunt195cb382014-11-03 17:50:51 -0800296 // lazy initialization of the view
297 if (view && !view.$div) {
Simon Hunt25248912014-11-04 11:25:48 -0800298 trace('creating view for ' + view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800299 $d = $view.append('div')
300 .attr({
Simon Hunt25248912014-11-04 11:25:48 -0800301 id: view.vid,
302 class: 'onosView'
Simon Hunt195cb382014-11-03 17:50:51 -0800303 });
Simon Hunt25248912014-11-04 11:25:48 -0800304 setViewDimensions($d);
305 view.$div = $d; // cache a reference to the D3 selection
Simon Hunt195cb382014-11-03 17:50:51 -0800306 }
307 }
308
309 function setView(view, hash, t) {
Simon Hunt25248912014-11-04 11:25:48 -0800310 traceFn('setView', view.vid);
Simon Hunt195cb382014-11-03 17:50:51 -0800311 // set the specified view as current, while invoking the
312 // appropriate life-cycle callbacks
313
Simon Hunt56d51852014-11-09 13:03:35 -0800314 // first, we'll start by closing the alerts pane, if open
315 closeAlerts();
316
Simon Hunt195cb382014-11-03 17:50:51 -0800317 // if there is a current view, and it is not the same as
318 // the incoming view, then unload it...
Simon Hunt25248912014-11-04 11:25:48 -0800319 if (current.view && (current.view.vid !== view.vid)) {
Simon Hunt195cb382014-11-03 17:50:51 -0800320 current.view.unload();
Simon Huntdb9eb072014-11-04 19:12:46 -0800321
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800322 // detach radio buttons, key handlers, etc.
323 $('#mastRadio').children().detach();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800324 keyHandler.viewKeys = {};
325 keyHandler.viewFn = null;
Simon Hunt195cb382014-11-03 17:50:51 -0800326 }
327
328 // cache new view and context
329 current.view = view;
330 current.ctx = t.ctx || '';
Simon Hunt56d51852014-11-09 13:03:35 -0800331 current.flags = t.flags || {};
Simon Hunt195cb382014-11-03 17:50:51 -0800332
Simon Hunta2994cc2014-12-02 14:19:15 -0800333 // init is called only once, after the view is in the DOM
334 if (!view.inited) {
335 view.init(current.ctx, current.flags);
336 view.inited = true;
Simon Hunt195cb382014-11-03 17:50:51 -0800337 }
338
339 // clear the view of stale data
340 view.reset();
341
342 // load the view
Simon Hunt56d51852014-11-09 13:03:35 -0800343 view.load(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800344 }
345
Simon Huntdb9eb072014-11-04 19:12:46 -0800346 // generate 'unique' id by prefixing view id
Simon Hunt934c3ce2014-11-05 11:45:07 -0800347 function makeUid(view, id) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800348 return view.vid + '-' + id;
349 }
350
351 // restore id by removing view id prefix
Simon Hunt934c3ce2014-11-05 11:45:07 -0800352 function unmakeUid(view, uid) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800353 var re = new RegExp('^' + view.vid + '-');
354 return uid.replace(re, '');
355 }
356
Simon Hunt934c3ce2014-11-05 11:45:07 -0800357 function setRadioButtons(vid, btnSet) {
Simon Huntdb9eb072014-11-04 19:12:46 -0800358 var view = views[vid],
Simon Hunt9462e8c2014-11-14 17:28:09 -0800359 btnG,
360 api = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800361
362 // lazily create the buttons...
363 if (!(btnG = view.radioButtons)) {
364 btnG = d3.select(document.createElement('div'));
Simon Hunt934c3ce2014-11-05 11:45:07 -0800365 btnG.buttonDef = {};
Simon Huntdb9eb072014-11-04 19:12:46 -0800366
367 btnSet.forEach(function (btn, i) {
368 var bid = btn.id || 'b' + i,
369 txt = btn.text || 'Button #' + i,
Simon Hunt934c3ce2014-11-05 11:45:07 -0800370 uid = makeUid(view, bid),
371 button = btnG.append('span')
Simon Huntdb9eb072014-11-04 19:12:46 -0800372 .attr({
Simon Hunt934c3ce2014-11-05 11:45:07 -0800373 id: uid,
Simon Huntdb9eb072014-11-04 19:12:46 -0800374 class: 'radio'
375 })
376 .text(txt);
Simon Hunt934c3ce2014-11-05 11:45:07 -0800377
Simon Hunt9462e8c2014-11-14 17:28:09 -0800378 btn.id = bid;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800379 btnG.buttonDef[uid] = btn;
380
Simon Huntdb9eb072014-11-04 19:12:46 -0800381 if (i === 0) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800382 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800383 btnG.selected = bid;
Simon Huntdb9eb072014-11-04 19:12:46 -0800384 }
385 });
386
387 btnG.selectAll('span')
388 .on('click', function (d) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800389 var button = d3.select(this),
390 uid = button.attr('id'),
391 btn = btnG.buttonDef[uid],
392 act = button.classed('active');
Simon Huntdb9eb072014-11-04 19:12:46 -0800393
394 if (!act) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800395 btnG.selectAll('span').classed('active', false);
396 button.classed('active', true);
Simon Hunt9462e8c2014-11-14 17:28:09 -0800397 btnG.selected = btn.id;
Simon Hunt934c3ce2014-11-05 11:45:07 -0800398 if (isF(btn.cb)) {
399 btn.cb(view.token(), btn);
400 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800401 }
402 });
403
404 view.radioButtons = btnG;
Simon Hunt9462e8c2014-11-14 17:28:09 -0800405
406 api.selected = function () {
407 return btnG.selected;
408 }
Simon Huntdb9eb072014-11-04 19:12:46 -0800409 }
410
411 // attach the buttons to the masthead
412 $mastRadio.node().appendChild(btnG.node());
Simon Hunt9462e8c2014-11-14 17:28:09 -0800413 // return an api for interacting with the button set
414 return api;
Simon Huntdb9eb072014-11-04 19:12:46 -0800415 }
416
Thomas Vachuska65368e32014-11-08 16:10:20 -0800417 function setupGlobalKeys() {
Simon Hunta1a00c22014-12-04 16:10:40 -0800418 $.extend(keyHandler, {
419 globalKeys: {
420 backSlash: [quickHelp, 'Show / hide Quick Help'],
421 slash: [quickHelp, 'Show / hide Quick Help'],
422 esc: [escapeKey, 'Dismiss dialog or cancel selections'],
423 T: [toggleTheme, "Toggle theme"]
424 },
425 globalFormat: ['backSlash', 'slash', 'esc', 'T'],
426
427 // Masked keys are global key handlers that always return true.
428 // That is, the view will never see the event for that key.
429 maskedKeys: {
430 slash: true,
431 backSlash: true,
432 T: true
433 }
434 });
Thomas Vachuska65368e32014-11-08 16:10:20 -0800435 }
436
Simon Hunt5cef9062014-11-24 15:24:35 -0800437 function quickHelp(view, key, code, ev) {
438 libApi.quickHelp.show(keyHandler);
Simon Hunt988c6fc2014-11-20 17:43:03 -0800439 return true;
440 }
441
Thomas Vachuska65368e32014-11-08 16:10:20 -0800442 function escapeKey(view, key, code, ev) {
443 if (alerts.open) {
444 closeAlerts();
445 return true;
446 }
Simon Hunt5cef9062014-11-24 15:24:35 -0800447 if (libApi.quickHelp.hide()) {
448 return true;
449 }
Thomas Vachuska65368e32014-11-08 16:10:20 -0800450 return false;
451 }
452
453 function toggleTheme(view, key, code, ev) {
454 var body = d3.select('body');
455 current.theme = (current.theme === 'light') ? 'dark' : 'light';
456 body.classed('light dark', false);
457 body.classed(current.theme, true);
Simon Hunt8f40cce2014-11-23 15:57:30 -0800458 theme(view);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800459 return true;
460 }
461
Simon Hunt87514342014-11-24 16:41:27 -0800462 function setGestureNotes(g) {
463 keyHandler.viewGestures = isA(g) || [];
464 }
465
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800466 function setKeyBindings(keyArg) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800467 var viewKeys,
468 masked = [];
469
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800470 if ($.isFunction(keyArg)) {
471 // set general key handler callback
Thomas Vachuska65368e32014-11-08 16:10:20 -0800472 keyHandler.viewFn = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800473 } else {
474 // set specific key filter map
Thomas Vachuska65368e32014-11-08 16:10:20 -0800475 viewKeys = d3.map(keyArg).keys();
476 viewKeys.forEach(function (key) {
477 if (keyHandler.maskedKeys[key]) {
478 masked.push(' Key "' + key + '" is reserved');
479 }
480 });
481
482 if (masked.length) {
483 doAlert('WARNING...\n\nsetKeys():\n' + masked.join('\n'));
484 }
485 keyHandler.viewKeys = keyArg;
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800486 }
487 }
488
Thomas Vachuska65368e32014-11-08 16:10:20 -0800489 function keyIn() {
490 var event = d3.event,
491 keyCode = event.keyCode,
492 key = whatKey(keyCode),
Simon Hunta1162d82014-12-03 14:34:43 -0800493 kh = keyHandler,
494 gk = kh.globalKeys[key],
Simon Hunt56ef0fe2014-11-21 08:24:43 -0800495 gcb = isF(gk) || (isA(gk) && isF(gk[0])),
Simon Hunta1162d82014-12-03 14:34:43 -0800496 vk = kh.viewKeys[key],
497 vcb = isF(vk) || (isA(vk) && isF(vk[0])) || isF(kh.viewFn),
498 token = current.view.token();
Thomas Vachuska65368e32014-11-08 16:10:20 -0800499
500 // global callback?
Simon Hunta1162d82014-12-03 14:34:43 -0800501 if (gcb && gcb(token, key, keyCode, event)) {
Thomas Vachuska65368e32014-11-08 16:10:20 -0800502 // if the event was 'handled', we are done
503 return;
504 }
505 // otherwise, let the view callback have a shot
506 if (vcb) {
Simon Hunta1162d82014-12-03 14:34:43 -0800507 vcb(token, key, keyCode, event);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800508 }
509 }
Simon Hunt1a9eff92014-11-07 11:06:34 -0800510
511 function createAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800512 $alerts.style('display', 'block');
513 $alerts.append('span')
Simon Hunt1a9eff92014-11-07 11:06:34 -0800514 .attr('class', 'close')
515 .text('X')
516 .on('click', closeAlerts);
Simon Hunt61d04042014-11-11 17:27:16 -0800517 $alerts.append('pre');
518 $alerts.append('p').attr('class', 'footnote')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800519 .text('Press ESCAPE to close');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800520 alerts.open = true;
521 alerts.count = 0;
522 }
523
524 function closeAlerts() {
Simon Hunt61d04042014-11-11 17:27:16 -0800525 $alerts.style('display', 'none')
Thomas Vachuska65368e32014-11-08 16:10:20 -0800526 .html('');
Simon Hunt1a9eff92014-11-07 11:06:34 -0800527 alerts.open = false;
528 }
529
530 function addAlert(msg) {
531 var lines,
532 oldContent;
533
534 if (alerts.count) {
Simon Hunt61d04042014-11-11 17:27:16 -0800535 oldContent = $alerts.select('pre').html();
Simon Hunt1a9eff92014-11-07 11:06:34 -0800536 }
537
538 lines = msg.split('\n');
539 lines[0] += ' '; // spacing so we don't crowd 'X'
540 lines = lines.join('\n');
541
542 if (oldContent) {
543 lines += '\n----\n' + oldContent;
544 }
545
Simon Hunt61d04042014-11-11 17:27:16 -0800546 $alerts.select('pre').html(lines);
Simon Hunt1a9eff92014-11-07 11:06:34 -0800547 alerts.count++;
548 }
549
550 function doAlert(msg) {
551 if (!alerts.open) {
552 createAlerts();
553 }
554 addAlert(msg);
555 }
556
Simon Hunt25248912014-11-04 11:25:48 -0800557 function resize(e) {
558 d3.selectAll('.onosView').call(setViewDimensions);
559 // allow current view to react to resize event...
Simon Hunt195cb382014-11-03 17:50:51 -0800560 if (current.view) {
Simon Hunt56d51852014-11-09 13:03:35 -0800561 current.view.resize(current.ctx, current.flags);
Simon Hunt195cb382014-11-03 17:50:51 -0800562 }
563 }
564
Simon Hunt8f40cce2014-11-23 15:57:30 -0800565 function theme() {
566 // allow current view to react to theme event...
567 if (current.view) {
568 current.view.theme(current.ctx, current.flags);
569 }
570 }
571
Simon Hunt195cb382014-11-03 17:50:51 -0800572 // ..........................................................
573 // View class
574 // Captures state information about a view.
575
576 // Constructor
577 // vid : view id
578 // nid : id of associated nav-item (optional)
Simon Hunta2994cc2014-12-02 14:19:15 -0800579 // cb : callbacks (init, reset, load, unload, resize, theme, error)
Simon Hunt195cb382014-11-03 17:50:51 -0800580 function View(vid) {
581 var av = 'addView(): ',
582 args = Array.prototype.slice.call(arguments),
583 nid,
Simon Hunt25248912014-11-04 11:25:48 -0800584 cb;
Simon Hunt195cb382014-11-03 17:50:51 -0800585
586 args.shift(); // first arg is always vid
587 if (typeof args[0] === 'string') { // nid specified
588 nid = args.shift();
589 }
590 cb = args.shift();
Simon Hunt195cb382014-11-03 17:50:51 -0800591
592 this.vid = vid;
593
594 if (validateViewArgs(vid)) {
595 this.nid = nid; // explicit navitem id (can be null)
596 this.cb = $.isPlainObject(cb) ? cb : {}; // callbacks
Simon Huntdb9eb072014-11-04 19:12:46 -0800597 this.$div = null; // view not yet added to DOM
598 this.radioButtons = null; // no radio buttons yet
599 this.ok = true; // valid view
Simon Hunt195cb382014-11-03 17:50:51 -0800600 }
Simon Hunt195cb382014-11-03 17:50:51 -0800601 }
602
603 function validateViewArgs(vid) {
Simon Hunt25248912014-11-04 11:25:48 -0800604 var av = "ui.addView(...): ",
605 ok = false;
Simon Hunt195cb382014-11-03 17:50:51 -0800606 if (typeof vid !== 'string' || !vid) {
607 doError(av + 'vid required');
608 } else if (views[vid]) {
609 doError(av + 'View ID "' + vid + '" already exists');
610 } else {
611 ok = true;
612 }
613 return ok;
614 }
615
616 var viewInstanceMethods = {
Simon Hunt25248912014-11-04 11:25:48 -0800617 token: function () {
Simon Hunt195cb382014-11-03 17:50:51 -0800618 return {
Simon Hunt25248912014-11-04 11:25:48 -0800619 // attributes
Simon Hunt195cb382014-11-03 17:50:51 -0800620 vid: this.vid,
621 nid: this.nid,
Simon Hunt25248912014-11-04 11:25:48 -0800622 $div: this.$div,
623
624 // functions
625 width: this.width,
Simon Huntdb9eb072014-11-04 19:12:46 -0800626 height: this.height,
Simon Hunt142d0032014-11-04 20:13:09 -0800627 uid: this.uid,
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800628 setRadio: this.setRadio,
Simon Huntc7ee0662014-11-05 16:44:37 -0800629 setKeys: this.setKeys,
Simon Hunt87514342014-11-24 16:41:27 -0800630 setGestures: this.setGestures,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800631 dataLoadError: this.dataLoadError,
Simon Hunt625dc402014-11-18 10:57:18 -0800632 alert: this.alert,
Simon Hunta3dd9572014-11-20 15:22:41 -0800633 flash: this.flash,
Simon Hunt8f40cce2014-11-23 15:57:30 -0800634 getTheme: this.getTheme
Simon Hunt195cb382014-11-03 17:50:51 -0800635 }
Simon Hunt25248912014-11-04 11:25:48 -0800636 },
637
Simon Huntf67722a2014-11-10 09:32:06 -0800638 // == Life-cycle functions
639 // TODO: factor common code out of life-cycle
Simon Hunta2994cc2014-12-02 14:19:15 -0800640 init: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800641 var c = ctx || '',
Simon Hunta2994cc2014-12-02 14:19:15 -0800642 fn = isF(this.cb.init);
643 traceFn('View.init', this.vid + ', ' + c);
Simon Hunt25248912014-11-04 11:25:48 -0800644 if (fn) {
Simon Hunta2994cc2014-12-02 14:19:15 -0800645 trace('INIT cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800646 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800647 }
648 },
649
Simon Huntf67722a2014-11-10 09:32:06 -0800650 reset: function (ctx, flags) {
651 var c = ctx || '',
652 fn = isF(this.cb.reset);
Simon Hunt25248912014-11-04 11:25:48 -0800653 traceFn('View.reset', this.vid);
654 if (fn) {
655 trace('RESET cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800656 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800657 } else if (this.cb.reset === true) {
658 // boolean true signifies "clear view"
659 trace(' [true] cleaing view...');
660 viewApi.empty();
661 }
662 },
663
Simon Hunt56d51852014-11-09 13:03:35 -0800664 load: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800665 var c = ctx || '',
666 fn = isF(this.cb.load);
667 traceFn('View.load', this.vid + ', ' + c);
668 this.$div.classed('currentView', true);
Simon Hunt25248912014-11-04 11:25:48 -0800669 if (fn) {
670 trace('LOAD cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800671 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800672 }
673 },
674
Simon Huntf67722a2014-11-10 09:32:06 -0800675 unload: function (ctx, flags) {
676 var c = ctx | '',
677 fn = isF(this.cb.unload);
Simon Hunt25248912014-11-04 11:25:48 -0800678 traceFn('View.unload', this.vid);
679 this.$div.classed('currentView', false);
Simon Hunt25248912014-11-04 11:25:48 -0800680 if (fn) {
681 trace('UNLOAD cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800682 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800683 }
684 },
685
Simon Hunt56d51852014-11-09 13:03:35 -0800686 resize: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800687 var c = ctx || '',
688 fn = isF(this.cb.resize),
689 w = this.width(),
690 h = this.height();
691 traceFn('View.resize', this.vid + '/' + c +
692 ' [' + w + 'x' + h + ']');
693 if (fn) {
694 trace('RESIZE cb for ' + this.vid);
Simon Hunt56d51852014-11-09 13:03:35 -0800695 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800696 }
697 },
698
Simon Hunt8f40cce2014-11-23 15:57:30 -0800699 theme: function (ctx, flags) {
700 var c = ctx | '',
701 fn = isF(this.cb.theme);
702 traceFn('View.theme', this.vid);
703 if (fn) {
704 trace('THEME cb for ' + this.vid);
705 fn(this.token(), c, flags);
706 }
707 },
708
Simon Huntf67722a2014-11-10 09:32:06 -0800709 error: function (ctx, flags) {
Simon Hunt25248912014-11-04 11:25:48 -0800710 var c = ctx || '',
711 fn = isF(this.cb.error);
712 traceFn('View.error', this.vid + ', ' + c);
713 if (fn) {
714 trace('ERROR cb for ' + this.vid);
Simon Huntf67722a2014-11-10 09:32:06 -0800715 fn(this.token(), c, flags);
Simon Hunt25248912014-11-04 11:25:48 -0800716 }
717 },
718
Simon Huntf67722a2014-11-10 09:32:06 -0800719 // == Token API functions
Simon Hunt25248912014-11-04 11:25:48 -0800720 width: function () {
721 return $(this.$div.node()).width();
722 },
723
724 height: function () {
725 return $(this.$div.node()).height();
Simon Huntdb9eb072014-11-04 19:12:46 -0800726 },
Simon Hunt25248912014-11-04 11:25:48 -0800727
Simon Hunt934c3ce2014-11-05 11:45:07 -0800728 setRadio: function (btnSet) {
Simon Hunt9462e8c2014-11-14 17:28:09 -0800729 return setRadioButtons(this.vid, btnSet);
Simon Hunt142d0032014-11-04 20:13:09 -0800730 },
731
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800732 setKeys: function (keyArg) {
733 setKeyBindings(keyArg);
734 },
735
Simon Hunt87514342014-11-24 16:41:27 -0800736 setGestures: function (g) {
737 setGestureNotes(g);
738 },
739
Simon Hunt8f40cce2014-11-23 15:57:30 -0800740 getTheme: function () {
Simon Hunt625dc402014-11-18 10:57:18 -0800741 return current.theme;
742 },
743
Simon Hunt142d0032014-11-04 20:13:09 -0800744 uid: function (id) {
Simon Hunt934c3ce2014-11-05 11:45:07 -0800745 return makeUid(this, id);
Simon Huntc7ee0662014-11-05 16:44:37 -0800746 },
747
Simon Huntbb282f52014-11-10 11:08:19 -0800748 // TODO : add exportApi and importApi methods
Simon Hunt1a9eff92014-11-07 11:06:34 -0800749 // TODO : implement custom dialogs
750
751 // Consider enhancing alert mechanism to handle multiples
752 // as individually closable.
753 alert: function (msg) {
754 doAlert(msg);
755 },
Simon Huntc7ee0662014-11-05 16:44:37 -0800756
Simon Hunta3dd9572014-11-20 15:22:41 -0800757 flash: function (msg) {
758 libApi.feedback.flash(msg);
759 },
760
Simon Huntc7ee0662014-11-05 16:44:37 -0800761 dataLoadError: function (err, url) {
762 var msg = 'Data Load Error\n\n' +
763 err.status + ' -- ' + err.statusText + '\n\n' +
764 'relative-url: "' + url + '"\n\n' +
765 'complete-url: "' + err.responseURL + '"';
Simon Hunt1a9eff92014-11-07 11:06:34 -0800766 this.alert(msg);
Simon Huntdb9eb072014-11-04 19:12:46 -0800767 }
Simon Hunt25248912014-11-04 11:25:48 -0800768
769 // TODO: consider schedule, clearTimer, etc.
Simon Hunt195cb382014-11-03 17:50:51 -0800770 };
771
772 // attach instance methods to the view prototype
773 $.extend(View.prototype, viewInstanceMethods);
774
775 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800776 // UI API
Simon Hunt195cb382014-11-03 17:50:51 -0800777
Simon Hunta5e89142014-11-14 07:00:33 -0800778 var fpConfig = {
779 TR: {
780 side: 'right'
Simon Hunta5e89142014-11-14 07:00:33 -0800781 },
782 TL: {
783 side: 'left'
784 }
785 };
786
Simon Hunt25248912014-11-04 11:25:48 -0800787 uiApi = {
Simon Hunt1a9eff92014-11-07 11:06:34 -0800788 addLib: function (libName, api) {
789 // TODO: validation of args
790 libApi[libName] = api;
791 },
792
Simon Hunt61d04042014-11-11 17:27:16 -0800793 // TODO: implement floating panel as a class
794 // TODO: parameterize position (currently hard-coded to TopRight)
795 /*
796 * Creates div in floating panels block, with the given id.
797 * Returns panel token used to interact with the panel
798 */
799 addFloatingPanel: function (id, position) {
800 var pos = position || 'TR',
Simon Hunta5e89142014-11-14 07:00:33 -0800801 cfg = fpConfig[pos],
Simon Hunt61d04042014-11-11 17:27:16 -0800802 el,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800803 fp,
804 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800805
806 if (fpanels[id]) {
807 buildError('Float panel with id "' + id + '" already exists.');
808 return null;
809 }
810
811 el = $floatPanels.append('div')
812 .attr('id', id)
Simon Hunta5e89142014-11-14 07:00:33 -0800813 .attr('class', 'fpanel')
814 .style('opacity', 0);
815
816 // has to be called after el is set.
817 el.style(cfg.side, pxHide());
818
819 function pxShow() {
820 return '20px';
821 }
822 function pxHide() {
823 return (-20 - widthVal()) + 'px';
824 }
Simon Hunt7b403bc2014-11-22 19:01:00 -0800825 function noPx(what) {
826 return el.style(what).replace(/px$/, '');
827 }
Simon Hunta5e89142014-11-14 07:00:33 -0800828 function widthVal() {
Simon Hunt7b403bc2014-11-22 19:01:00 -0800829 return noPx('width');
830 }
831 function heightVal() {
832 return noPx('height');
Simon Hunta5e89142014-11-14 07:00:33 -0800833 }
Simon Hunt61d04042014-11-11 17:27:16 -0800834
Simon Hunt06811b72014-11-25 18:54:48 -0800835 function noop() {}
836
Simon Hunt61d04042014-11-11 17:27:16 -0800837 fp = {
838 id: id,
839 el: el,
840 pos: pos,
Thomas Vachuska47635c62014-11-22 01:21:36 -0800841 isVisible: function () {
842 return on;
843 },
Simon Hunta5e89142014-11-14 07:00:33 -0800844
Simon Hunt06811b72014-11-25 18:54:48 -0800845 show: function (cb) {
846 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800847 on = true;
Simon Hunt61d04042014-11-11 17:27:16 -0800848 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800849 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800850 .style(cfg.side, pxShow())
Simon Hunt61d04042014-11-11 17:27:16 -0800851 .style('opacity', 1);
852 },
Simon Hunt06811b72014-11-25 18:54:48 -0800853 hide: function (cb) {
854 var endCb = isF(cb) || noop;
Thomas Vachuska47635c62014-11-22 01:21:36 -0800855 on = false;
Simon Hunt61d04042014-11-11 17:27:16 -0800856 el.transition().duration(750)
Simon Hunt06811b72014-11-25 18:54:48 -0800857 .each('end', endCb)
Simon Hunta5e89142014-11-14 07:00:33 -0800858 .style(cfg.side, pxHide())
Simon Hunt61d04042014-11-11 17:27:16 -0800859 .style('opacity', 0);
860 },
861 empty: function () {
862 return el.html('');
863 },
864 append: function (what) {
865 return el.append(what);
Simon Hunta5e89142014-11-14 07:00:33 -0800866 },
867 width: function (w) {
868 if (w === undefined) {
869 return widthVal();
870 }
Simon Huntb82f6902014-11-22 11:53:15 -0800871 el.style('width', w + 'px');
Simon Hunt7b403bc2014-11-22 19:01:00 -0800872 },
873 height: function (h) {
874 if (h === undefined) {
875 return heightVal();
876 }
877 el.style('height', h + 'px');
Simon Hunt61d04042014-11-11 17:27:16 -0800878 }
879 };
880 fpanels[id] = fp;
881 return fp;
882 },
883
Simon Hunt1a9eff92014-11-07 11:06:34 -0800884 // TODO: it remains to be seen whether we keep this style of docs
Simon Hunt25248912014-11-04 11:25:48 -0800885 /** @api ui addView( vid, nid, cb )
886 * Adds a view to the UI.
887 * <p>
888 * Views are loaded/unloaded into the view content pane at
889 * appropriate times, by the navigation framework. This method
890 * adds a view to the UI and returns a token object representing
891 * the view. A view's token is always passed as the first
892 * argument to each of the view's life-cycle callback functions.
893 * <p>
894 * Note that if the view is directly referenced by a nav-item,
895 * or in a group of views with one of those views referenced by
896 * a nav-item, then the <i>nid</i> argument can be omitted as
897 * the framework can infer it.
898 * <p>
899 * <i>cb</i> is a plain object containing callback functions:
Simon Hunta2994cc2014-12-02 14:19:15 -0800900 * "init", "reset", "load", "unload", "resize", "theme", "error".
Simon Hunt25248912014-11-04 11:25:48 -0800901 * <pre>
902 * function myLoad(view, ctx) { ... }
903 * ...
904 * // short form...
905 * onos.ui.addView('viewId', {
906 * load: myLoad
907 * });
908 * </pre>
909 *
910 * @param vid (string) [*] view ID (a unique DOM element id)
911 * @param nid (string) nav-item ID (a unique DOM element id)
912 * @param cb (object) [*] callbacks object
913 * @return the view token
914 */
915 addView: function (vid, nid, cb) {
916 traceFn('addView', vid);
917 var view = new View(vid, nid, cb),
Simon Hunt195cb382014-11-03 17:50:51 -0800918 token;
919 if (view.ok) {
920 views[vid] = view;
921 token = view.token();
922 } else {
923 token = { vid: view.vid, bad: true };
924 }
925 return token;
926 }
927 };
928
Simon Hunt25248912014-11-04 11:25:48 -0800929 // ..........................................................
930 // View API
931
Simon Huntbb282f52014-11-10 11:08:19 -0800932 // TODO: deprecated
Simon Hunt25248912014-11-04 11:25:48 -0800933 viewApi = {
934 /** @api view empty( )
935 * Empties the current view.
936 * <p>
937 * More specifically, removes all DOM elements from the
938 * current view's display div.
939 */
940 empty: function () {
941 if (!current.view) {
942 return;
943 }
944 current.view.$div.html('');
945 }
946 };
947
948 // ..........................................................
949 // Nav API
950 navApi = {
951
952 };
953
954 // ..........................................................
Simon Hunt1a9eff92014-11-07 11:06:34 -0800955 // Library API
956 libApi = {
957
958 };
959
960 // ..........................................................
Simon Hunt25248912014-11-04 11:25:48 -0800961 // Exported API
962
Simon Hunt195cb382014-11-03 17:50:51 -0800963 // function to be called from index.html to build the ONOS UI
964 function buildOnosUi() {
965 tsB = new Date().getTime();
966 tsI = tsB - tsI; // initialization duration
967
968 console.log('ONOS UI initialized in ' + tsI + 'ms');
969
970 if (built) {
971 throwError("ONOS UI already built!");
972 }
973 built = true;
974
Simon Huntdb9eb072014-11-04 19:12:46 -0800975 $mastRadio = d3.select('#mastRadio');
Simon Hunt195cb382014-11-03 17:50:51 -0800976
977 $(window).on('hashchange', hash);
Simon Hunt25248912014-11-04 11:25:48 -0800978 $(window).on('resize', resize);
Simon Hunt195cb382014-11-03 17:50:51 -0800979
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800980 d3.select('body').on('keydown', keyIn);
Thomas Vachuska65368e32014-11-08 16:10:20 -0800981 setupGlobalKeys();
Simon Hunt0df1b1d2014-11-04 22:58:29 -0800982
Simon Hunt195cb382014-11-03 17:50:51 -0800983 // Invoke hashchange callback to navigate to content
984 // indicated by the window location hash.
985 hash();
986
987 // If there were any build errors, report them
988 reportBuildErrors();
989 }
990
Simon Hunt195cb382014-11-03 17:50:51 -0800991 // export the api and build-UI function
992 return {
Simon Hunt25248912014-11-04 11:25:48 -0800993 ui: uiApi,
Simon Hunt1a9eff92014-11-07 11:06:34 -0800994 lib: libApi,
995 //view: viewApi,
Simon Hunt25248912014-11-04 11:25:48 -0800996 nav: navApi,
Simon Huntbb282f52014-11-10 11:08:19 -0800997 buildUi: buildOnosUi,
998 exported: exported
Simon Hunt195cb382014-11-03 17:50:51 -0800999 };
1000 };
1001
Simon Huntdb9eb072014-11-04 19:12:46 -08001002}(jQuery));