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