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