Upstream version 7.36.149.0
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / inspect / inspect.js
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 var MIN_VERSION_TAB_CLOSE = 25;
6 var MIN_VERSION_TARGET_ID = 26;
7 var MIN_VERSION_NEW_TAB = 29;
8 var MIN_VERSION_TAB_ACTIVATE = 30;
9
10 function sendCommand(command, args) {
11   chrome.send(command, Array.prototype.slice.call(arguments, 1));
12 }
13
14 function sendTargetCommand(command, target) {
15   sendCommand(command, target.source, target.id);
16 }
17
18 function removeChildren(element_id) {
19   var element = $(element_id);
20   element.textContent = '';
21 }
22
23 function onload() {
24   var tabContents = document.querySelectorAll('#content > div');
25   for (var i = 0; i != tabContents.length; i++) {
26     var tabContent = tabContents[i];
27     var tabName = tabContent.querySelector('.content-header').textContent;
28
29     var tabHeader = document.createElement('div');
30     tabHeader.className = 'tab-header';
31     var button = document.createElement('button');
32     button.textContent = tabName;
33     tabHeader.appendChild(button);
34     tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
35     $('navigation').appendChild(tabHeader);
36   }
37   onHashChange();
38   initSettings();
39   sendCommand('init-ui');
40 }
41
42 function onHashChange() {
43   var hash = window.location.hash.slice(1).toLowerCase();
44   if (!selectTab(hash))
45     selectTab('devices');
46 }
47
48 /**
49  * @param {string} id Tab id.
50  * @return {boolean} True if successful.
51  */
52 function selectTab(id) {
53   closePortForwardingConfig();
54
55   var tabContents = document.querySelectorAll('#content > div');
56   var tabHeaders = $('navigation').querySelectorAll('.tab-header');
57   var found = false;
58   for (var i = 0; i != tabContents.length; i++) {
59     var tabContent = tabContents[i];
60     var tabHeader = tabHeaders[i];
61     if (tabContent.id == id) {
62       tabContent.classList.add('selected');
63       tabHeader.classList.add('selected');
64       found = true;
65     } else {
66       tabContent.classList.remove('selected');
67       tabHeader.classList.remove('selected');
68     }
69   }
70   if (!found)
71     return false;
72   window.location.hash = id;
73   return true;
74 }
75
76 function populateTargets(source, data) {
77   if (source == 'renderers')
78     populateWebContentsTargets(data);
79   else if (source == 'workers')
80     populateWorkerTargets(data);
81   else if (source == 'adb')
82     populateRemoteTargets(data);
83   else
84     console.error('Unknown source type: ' + source);
85 }
86
87 function populateWebContentsTargets(data) {
88   removeChildren('pages-list');
89   removeChildren('extensions-list');
90   removeChildren('apps-list');
91   removeChildren('others-list');
92
93   for (var i = 0; i < data.length; i++) {
94     if (data[i].type === 'page')
95       addToPagesList(data[i]);
96     else if (data[i].type === 'background_page')
97       addToExtensionsList(data[i]);
98     else if (data[i].type === 'app')
99       addToAppsList(data[i]);
100     else
101       addToOthersList(data[i]);
102   }
103 }
104
105 function populateWorkerTargets(data) {
106   removeChildren('workers-list');
107
108   for (var i = 0; i < data.length; i++)
109     addToWorkersList(data[i]);
110 }
111
112 function populateRemoteTargets(devices) {
113   if (!devices)
114     return;
115
116   if (window.modal) {
117     window.holdDevices = devices;
118     return;
119   }
120
121   function alreadyDisplayed(element, data) {
122     var json = JSON.stringify(data);
123     if (element.cachedJSON == json)
124       return true;
125     element.cachedJSON = json;
126     return false;
127   }
128
129   function insertChildSortedById(parent, child) {
130     for (var sibling = parent.firstElementChild;
131                      sibling;
132                      sibling = sibling.nextElementSibling) {
133       if (sibling.id > child.id) {
134         parent.insertBefore(child, sibling);
135         return;
136       }
137     }
138     parent.appendChild(child);
139   }
140
141   var deviceList = $('devices-list');
142   if (alreadyDisplayed(deviceList, devices))
143     return;
144
145   function removeObsolete(validIds, section) {
146     if (validIds.indexOf(section.id) < 0)
147       section.remove();
148   }
149
150   var newDeviceIds = devices.map(function(d) { return d.id });
151   Array.prototype.forEach.call(
152       deviceList.querySelectorAll('.device'),
153       removeObsolete.bind(null, newDeviceIds));
154
155   $('devices-help').hidden = !!devices.length;
156
157   for (var d = 0; d < devices.length; d++) {
158     var device = devices[d];
159
160     var deviceSection = $(device.id);
161     if (!deviceSection) {
162       deviceSection = document.createElement('div');
163       deviceSection.id = device.id;
164       deviceSection.className = 'device';
165       deviceList.appendChild(deviceSection);
166
167       var deviceHeader = document.createElement('div');
168       deviceHeader.className = 'device-header';
169       deviceSection.appendChild(deviceHeader);
170
171       var deviceName = document.createElement('div');
172       deviceName.className = 'device-name';
173       deviceHeader.appendChild(deviceName);
174
175       var deviceSerial = document.createElement('div');
176       deviceSerial.className = 'device-serial';
177       deviceSerial.textContent = '#' + device.adbSerial.toUpperCase();
178       deviceHeader.appendChild(deviceSerial);
179
180       var devicePorts = document.createElement('div');
181       devicePorts.className = 'device-ports';
182       deviceHeader.appendChild(devicePorts);
183
184       var browserList = document.createElement('div');
185       browserList.className = 'browsers';
186       deviceSection.appendChild(browserList);
187
188       var authenticating = document.createElement('div');
189       authenticating.className = 'device-auth';
190       deviceSection.appendChild(authenticating);
191     }
192
193     if (alreadyDisplayed(deviceSection, device))
194       continue;
195
196     deviceSection.querySelector('.device-name').textContent = device.adbModel;
197     deviceSection.querySelector('.device-auth').textContent =
198         device.adbConnected ? '' : 'Pending authentication: please accept ' +
199           'debugging session on the device.';
200
201     var browserList = deviceSection.querySelector('.browsers');
202     var newBrowserIds =
203         device.browsers.map(function(b) { return b.id });
204     Array.prototype.forEach.call(
205         browserList.querySelectorAll('.browser'),
206         removeObsolete.bind(null, newBrowserIds));
207
208     for (var b = 0; b < device.browsers.length; b++) {
209       var browser = device.browsers[b];
210
211       var majorChromeVersion = browser.adbBrowserChromeVersion;
212
213       var incompatibleVersion = browser.hasOwnProperty('compatibleVersion') &&
214                                 !browser.compatibleVersion;
215       var pageList;
216       var browserSection = $(browser.id);
217       if (browserSection) {
218         pageList = browserSection.querySelector('.pages');
219       } else {
220         browserSection = document.createElement('div');
221         browserSection.id = browser.id;
222         browserSection.className = 'browser';
223         insertChildSortedById(browserList, browserSection);
224
225         var browserHeader = document.createElement('div');
226         browserHeader.className = 'browser-header';
227
228         var browserName = document.createElement('div');
229         browserName.className = 'browser-name';
230         browserHeader.appendChild(browserName);
231         browserName.textContent = browser.adbBrowserName;
232         if (browser.adbBrowserVersion)
233           browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
234         browserSection.appendChild(browserHeader);
235
236         if (incompatibleVersion) {
237           var warningSection = document.createElement('div');
238           warningSection.className = 'warning';
239           warningSection.textContent =
240             'You may need a newer version of desktop Chrome. ' +
241             'Please try Chrome ' + browser.adbBrowserVersion + ' or later.';
242           browserHeader.appendChild(warningSection);
243         } else if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
244           var newPage = document.createElement('div');
245           newPage.className = 'open';
246
247           var newPageUrl = document.createElement('input');
248           newPageUrl.type = 'text';
249           newPageUrl.placeholder = 'Open tab with url';
250           newPage.appendChild(newPageUrl);
251
252           var openHandler = function(sourceId, browserId, input) {
253             sendCommand(
254                 'open', sourceId, browserId, input.value || 'about:blank');
255             input.value = '';
256           }.bind(null, browser.source, browser.id, newPageUrl);
257           newPageUrl.addEventListener('keyup', function(handler, event) {
258             if (event.keyIdentifier == 'Enter' && event.target.value)
259               handler();
260           }.bind(null, openHandler), true);
261
262           var newPageButton = document.createElement('button');
263           newPageButton.textContent = 'Open';
264           newPage.appendChild(newPageButton);
265           newPageButton.addEventListener('click', openHandler, true);
266
267           browserHeader.appendChild(newPage);
268         }
269
270         pageList = document.createElement('div');
271         pageList.className = 'list pages';
272         browserSection.appendChild(pageList);
273       }
274
275       if (incompatibleVersion || alreadyDisplayed(browserSection, browser))
276         continue;
277
278       pageList.textContent = '';
279       for (var p = 0; p < browser.pages.length; p++) {
280         var page = browser.pages[p];
281         // Attached targets have no unique id until Chrome 26. For such targets
282         // it is impossible to activate existing DevTools window.
283         page.hasNoUniqueId = page.attached &&
284             (majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID);
285         var row = addTargetToList(page, pageList, ['name', 'url']);
286         if (page['description'])
287           addWebViewDetails(row, page);
288         else
289           addFavicon(row, page);
290         if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
291           addActionLink(row, 'focus tab',
292               sendTargetCommand.bind(null, 'activate', page), false);
293         }
294         if (majorChromeVersion) {
295           addActionLink(row, 'reload',
296               sendTargetCommand.bind(null, 'reload', page), page.attached);
297         }
298         if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
299           addActionLink(row, 'close',
300               sendTargetCommand.bind(null, 'close', page), page.attached);
301         }
302       }
303     }
304   }
305 }
306
307 function addToPagesList(data) {
308   var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
309   addFavicon(row, data);
310   if (data.guests)
311     addGuestViews(row, data.guests);
312 }
313
314 function addToExtensionsList(data) {
315   var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
316   addFavicon(row, data);
317   if (data.guests)
318     addGuestViews(row, data.guests);
319 }
320
321 function addToAppsList(data) {
322   var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
323   addFavicon(row, data);
324   if (data.guests)
325     addGuestViews(row, data.guests);
326 }
327
328 function addGuestViews(row, guests) {
329   Array.prototype.forEach.call(guests, function(guest) {
330     var guestRow = addTargetToList(guest, row, ['name', 'url']);
331     guestRow.classList.add('guest');
332     addFavicon(guestRow, guest);
333   });
334 }
335
336 function addToWorkersList(data) {
337   var row =
338       addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
339   addActionLink(row, 'terminate',
340       sendTargetCommand.bind(null, 'close', data), data.attached);
341 }
342
343 function addToOthersList(data) {
344   addTargetToList(data, $('others-list'), ['url']);
345 }
346
347 function formatValue(data, property) {
348   var value = data[property];
349
350   if (property == 'name' && value == '') {
351     value = 'untitled';
352   }
353
354   var text = value ? String(value) : '';
355   if (text.length > 100)
356     text = text.substring(0, 100) + '\u2026';
357
358   var div = document.createElement('div');
359   div.textContent = text;
360   div.className = property;
361   return div;
362 }
363
364 function addFavicon(row, data) {
365   var favicon = document.createElement('img');
366   if (data['faviconUrl'])
367     favicon.src = data['faviconUrl'];
368   var propertiesBox = row.querySelector('.properties-box');
369   propertiesBox.insertBefore(favicon, propertiesBox.firstChild);
370 }
371
372 function addWebViewDetails(row, data) {
373   var webview;
374   try {
375     webview = JSON.parse(data['description']);
376   } catch (e) {
377     return;
378   }
379   addWebViewDescription(row, webview);
380   if (data.adbScreenWidth && data.adbScreenHeight)
381     addWebViewThumbnail(
382         row, webview, data.adbScreenWidth, data.adbScreenHeight);
383 }
384
385 function addWebViewDescription(row, webview) {
386   var viewStatus = { visibility: '', position: '', size: '' };
387   if (!webview.empty) {
388     if (webview.attached && !webview.visible)
389       viewStatus.visibility = 'hidden';
390     else if (!webview.attached)
391       viewStatus.visibility = 'detached';
392     viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
393   } else {
394     viewStatus.visibility = 'empty';
395   }
396   if (webview.attached) {
397       viewStatus.position =
398         'at (' + webview.screenX + ', ' + webview.screenY + ')';
399   }
400
401   var subRow = document.createElement('div');
402   subRow.className = 'subrow webview';
403   if (webview.empty || !webview.attached || !webview.visible)
404     subRow.className += ' invisible-view';
405   if (viewStatus.visibility)
406     subRow.appendChild(formatValue(viewStatus, 'visibility'));
407   if (viewStatus.position)
408     subRow.appendChild(formatValue(viewStatus, 'position'));
409   subRow.appendChild(formatValue(viewStatus, 'size'));
410   var subrowBox = row.querySelector('.subrow-box');
411   subrowBox.insertBefore(subRow, row.querySelector('.actions'));
412 }
413
414 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
415   var maxScreenRectSize = 50;
416   var screenRectWidth;
417   var screenRectHeight;
418
419   var aspectRatio = screenWidth / screenHeight;
420   if (aspectRatio < 1) {
421     screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
422     screenRectHeight = maxScreenRectSize;
423   } else {
424     screenRectWidth = maxScreenRectSize;
425     screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
426   }
427
428   var thumbnail = document.createElement('div');
429   thumbnail.className = 'webview-thumbnail';
430   var thumbnailWidth = 3 * screenRectWidth;
431   var thumbnailHeight = 60;
432   thumbnail.style.width = thumbnailWidth + 'px';
433   thumbnail.style.height = thumbnailHeight + 'px';
434
435   var screenRect = document.createElement('div');
436   screenRect.className = 'screen-rect';
437   screenRect.style.left = screenRectWidth + 'px';
438   screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
439   screenRect.style.width = screenRectWidth + 'px';
440   screenRect.style.height = screenRectHeight + 'px';
441   thumbnail.appendChild(screenRect);
442
443   if (!webview.empty && webview.attached) {
444     var viewRect = document.createElement('div');
445     viewRect.className = 'view-rect';
446     if (!webview.visible)
447       viewRect.classList.add('hidden');
448     function percent(ratio) {
449       return ratio * 100 + '%';
450     }
451     viewRect.style.left = percent(webview.screenX / screenWidth);
452     viewRect.style.top = percent(webview.screenY / screenHeight);
453     viewRect.style.width = percent(webview.width / screenWidth);
454     viewRect.style.height = percent(webview.height / screenHeight);
455     screenRect.appendChild(viewRect);
456   }
457
458   var propertiesBox = row.querySelector('.properties-box');
459   propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild);
460 }
461
462 function addTargetToList(data, list, properties) {
463   var row = document.createElement('div');
464   row.className = 'row';
465
466   var propertiesBox = document.createElement('div');
467   propertiesBox.className = 'properties-box';
468   row.appendChild(propertiesBox);
469
470   var subrowBox = document.createElement('div');
471   subrowBox.className = 'subrow-box';
472   propertiesBox.appendChild(subrowBox);
473
474   var subrow = document.createElement('div');
475   subrow.className = 'subrow';
476   subrowBox.appendChild(subrow);
477
478   for (var j = 0; j < properties.length; j++)
479     subrow.appendChild(formatValue(data, properties[j]));
480
481   var actionBox = document.createElement('div');
482   actionBox.className = 'actions';
483   subrowBox.appendChild(actionBox);
484
485   addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data),
486       data.hasNoUniqueId || data.adbAttachedForeign);
487
488   list.appendChild(row);
489   return row;
490 }
491
492 function addActionLink(row, text, handler, opt_disabled) {
493   var link = document.createElement('span');
494   link.classList.add('action');
495   link.setAttribute('tabindex', 1);
496   if (opt_disabled)
497     link.classList.add('disabled');
498   else
499     link.classList.remove('disabled');
500
501   link.textContent = text;
502   link.addEventListener('click', handler, true);
503   row.querySelector('.actions').appendChild(link);
504 }
505
506
507 function initSettings() {
508   $('discover-usb-devices-enable').addEventListener('change',
509                                                     enableDiscoverUsbDevices);
510
511   $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
512   $('port-forwarding-config-open').addEventListener(
513       'click', openPortForwardingConfig);
514   $('port-forwarding-config-close').addEventListener(
515       'click', closePortForwardingConfig);
516   $('port-forwarding-config-done').addEventListener(
517       'click', commitPortForwardingConfig.bind(true));
518 }
519
520 function enableDiscoverUsbDevices(event) {
521   sendCommand('set-discover-usb-devices-enabled', event.target.checked);
522 }
523
524 function enablePortForwarding(event) {
525   sendCommand('set-port-forwarding-enabled', event.target.checked);
526 }
527
528 function handleKey(event) {
529   switch (event.keyCode) {
530     case 13:  // Enter
531       if (event.target.nodeName == 'INPUT') {
532         var line = event.target.parentNode;
533         if (!line.classList.contains('fresh') ||
534             line.classList.contains('empty')) {
535           commitPortForwardingConfig(true);
536         } else {
537           commitFreshLineIfValid(true /* select new line */);
538           commitPortForwardingConfig(false);
539         }
540       } else {
541         commitPortForwardingConfig(true);
542       }
543       break;
544
545     case 27:
546       commitPortForwardingConfig(true);
547       break;
548   }
549 }
550
551 function setModal(dialog) {
552   dialog.deactivatedNodes = Array.prototype.filter.call(
553       document.querySelectorAll('*'),
554       function(n) {
555         return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
556       });
557
558   dialog.tabIndexes = dialog.deactivatedNodes.map(
559     function(n) { return n.getAttribute('tabindex'); });
560
561   dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; });
562   window.modal = dialog;
563 }
564
565 function unsetModal(dialog) {
566   for (var i = 0; i < dialog.deactivatedNodes.length; i++) {
567     var node = dialog.deactivatedNodes[i];
568     if (dialog.tabIndexes[i] === null)
569       node.removeAttribute('tabindex');
570     else
571       node.setAttribute('tabindex', tabIndexes[i]);
572   }
573
574   if (window.holdDevices) {
575     populateRemoteTargets(window.holdDevices);
576     delete window.holdDevices;
577   }
578
579   delete dialog.deactivatedNodes;
580   delete dialog.tabIndexes;
581   delete window.modal;
582 }
583
584 function openPortForwardingConfig() {
585   loadPortForwardingConfig(window.portForwardingConfig);
586
587   $('port-forwarding-overlay').classList.add('open');
588   document.addEventListener('keyup', handleKey);
589
590   var freshPort = document.querySelector('.fresh .port');
591   if (freshPort)
592     freshPort.focus();
593   else
594     $('port-forwarding-config-done').focus();
595
596   setModal($('port-forwarding-overlay'));
597 }
598
599 function closePortForwardingConfig() {
600   if (!$('port-forwarding-overlay').classList.contains('open'))
601     return;
602
603   $('port-forwarding-overlay').classList.remove('open');
604   document.removeEventListener('keyup', handleKey);
605   unsetModal($('port-forwarding-overlay'));
606 }
607
608 function loadPortForwardingConfig(config) {
609   var list = $('port-forwarding-config-list');
610   list.textContent = '';
611   for (var port in config)
612     list.appendChild(createConfigLine(port, config[port]));
613   list.appendChild(createEmptyConfigLine());
614 }
615
616 function commitPortForwardingConfig(closeConfig) {
617   if (closeConfig)
618     closePortForwardingConfig();
619
620   commitFreshLineIfValid();
621   var lines = document.querySelectorAll('.port-forwarding-pair');
622   var config = {};
623   for (var i = 0; i != lines.length; i++) {
624     var line = lines[i];
625     var portInput = line.querySelector('.port');
626     var locationInput = line.querySelector('.location');
627
628     var port = portInput.classList.contains('invalid') ?
629                portInput.lastValidValue :
630                portInput.value;
631
632     var location = locationInput.classList.contains('invalid') ?
633                    locationInput.lastValidValue :
634                    locationInput.value;
635
636     if (port && location)
637       config[port] = location;
638   }
639   sendCommand('set-port-forwarding-config', config);
640 }
641
642 function updateDiscoverUsbDevicesEnabled(enabled) {
643   var checkbox = $('discover-usb-devices-enable');
644   checkbox.checked = !!enabled;
645   checkbox.disabled = false;
646 }
647
648 function updatePortForwardingEnabled(enabled) {
649   var checkbox = $('port-forwarding-enable');
650   checkbox.checked = !!enabled;
651   checkbox.disabled = false;
652 }
653
654 function updatePortForwardingConfig(config) {
655   window.portForwardingConfig = config;
656   $('port-forwarding-config-open').disabled = !config;
657 }
658
659 function createConfigLine(port, location) {
660   var line = document.createElement('div');
661   line.className = 'port-forwarding-pair';
662
663   var portInput = createConfigField(port, 'port', 'Port', validatePort);
664   line.appendChild(portInput);
665
666   var locationInput = createConfigField(
667       location, 'location', 'IP address and port', validateLocation);
668   line.appendChild(locationInput);
669   locationInput.addEventListener('keydown', function(e) {
670     if (e.keyIdentifier == 'U+0009' &&  // Tab
671         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
672         line.classList.contains('fresh') &&
673         !line.classList.contains('empty')) {
674       // Tabbing forward on the fresh line, try create a new empty one.
675       if (commitFreshLineIfValid(true))
676         e.preventDefault();
677     }
678   });
679
680   var lineDelete = document.createElement('div');
681   lineDelete.className = 'close-button';
682   lineDelete.addEventListener('click', function() {
683     var newSelection = line.nextElementSibling;
684     line.parentNode.removeChild(line);
685     selectLine(newSelection);
686   });
687   line.appendChild(lineDelete);
688
689   line.addEventListener('click', selectLine.bind(null, line));
690   line.addEventListener('focus', selectLine.bind(null, line));
691
692   checkEmptyLine(line);
693
694   return line;
695 }
696
697 function validatePort(input) {
698   var match = input.value.match(/^(\d+)$/);
699   if (!match)
700     return false;
701   var port = parseInt(match[1]);
702   if (port < 1024 || 65535 < port)
703     return false;
704
705   var inputs = document.querySelectorAll('input.port:not(.invalid)');
706   for (var i = 0; i != inputs.length; ++i) {
707     if (inputs[i] == input)
708       break;
709     if (parseInt(inputs[i].value) == port)
710       return false;
711   }
712   return true;
713 }
714
715 function validateLocation(input) {
716   var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
717   if (!match)
718     return false;
719   var port = parseInt(match[2]);
720   return port <= 65535;
721 }
722
723 function createEmptyConfigLine() {
724   var line = createConfigLine('', '');
725   line.classList.add('fresh');
726   return line;
727 }
728
729 function createConfigField(value, className, hint, validate) {
730   var input = document.createElement('input');
731   input.className = className;
732   input.type = 'text';
733   input.placeholder = hint;
734   input.value = value;
735   input.lastValidValue = value;
736
737   function checkInput() {
738     if (validate(input))
739       input.classList.remove('invalid');
740     else
741       input.classList.add('invalid');
742     if (input.parentNode)
743       checkEmptyLine(input.parentNode);
744   }
745   checkInput();
746
747   input.addEventListener('keyup', checkInput);
748   input.addEventListener('focus', function() {
749     selectLine(input.parentNode);
750   });
751
752   input.addEventListener('blur', function() {
753     if (validate(input))
754       input.lastValidValue = input.value;
755   });
756
757   return input;
758 }
759
760 function checkEmptyLine(line) {
761   var inputs = line.querySelectorAll('input');
762   var empty = true;
763   for (var i = 0; i != inputs.length; i++) {
764     if (inputs[i].value != '')
765       empty = false;
766   }
767   if (empty)
768     line.classList.add('empty');
769   else
770     line.classList.remove('empty');
771 }
772
773 function selectLine(line) {
774   if (line.classList.contains('selected'))
775     return;
776   unselectLine();
777   line.classList.add('selected');
778 }
779
780 function unselectLine() {
781   var line = document.querySelector('.port-forwarding-pair.selected');
782   if (!line)
783     return;
784   line.classList.remove('selected');
785   commitFreshLineIfValid();
786 }
787
788 function commitFreshLineIfValid(opt_selectNew) {
789   var line = document.querySelector('.port-forwarding-pair.fresh');
790   if (line.querySelector('.invalid'))
791     return false;
792   line.classList.remove('fresh');
793   var freshLine = createEmptyConfigLine();
794   line.parentNode.appendChild(freshLine);
795   if (opt_selectNew)
796     freshLine.querySelector('.port').focus();
797   return true;
798 }
799
800 function populatePortStatus(devicesStatusMap) {
801   for (var deviceId in devicesStatusMap) {
802     if (!devicesStatusMap.hasOwnProperty(deviceId))
803       continue;
804     var deviceStatusMap = devicesStatusMap[deviceId];
805
806     var deviceSection = $(deviceId);
807     if (!deviceSection)
808       continue;
809
810     var devicePorts = deviceSection.querySelector('.device-ports');
811     devicePorts.textContent = '';
812     for (var port in deviceStatusMap) {
813       if (!deviceStatusMap.hasOwnProperty(port))
814         continue;
815
816       var status = deviceStatusMap[port];
817       var portIcon = document.createElement('div');
818       portIcon.className = 'port-icon';
819       // status === 0 is the default (connected) state.
820       // Positive values correspond to the tunnelling connection count
821       // (in DEBUG_DEVTOOLS mode).
822       if (status > 0)
823         portIcon.classList.add('connected');
824       else if (status === -1 || status === -2)
825         portIcon.classList.add('transient');
826       else if (status < 0)
827         portIcon.classList.add('error');
828       devicePorts.appendChild(portIcon);
829
830       var portNumber = document.createElement('div');
831       portNumber.className = 'port-number';
832       portNumber.textContent = ':' + port;
833       if (status > 0)
834         portNumber.textContent += '(' + status + ')';
835       devicePorts.appendChild(portNumber);
836     }
837   }
838
839   function clearPorts(deviceSection) {
840     if (deviceSection.id in devicesStatusMap)
841       return;
842     deviceSection.querySelector('.device-ports').textContent = '';
843   }
844
845   Array.prototype.forEach.call(
846       document.querySelectorAll('.device'), clearPorts);
847 }
848
849 document.addEventListener('DOMContentLoaded', onload);
850
851 window.addEventListener('hashchange', onHashChange);