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 = {};
13 var browserInspectorTitle;
16 var queryParams = window.location.search;
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';
29 browserInspector = queryParamsObject['browser-inspector'];
30 browserInspectorTitle = 'inspect';
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 = '';
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);
63 sendCommand('init-ui');
66 function onHashChange() {
67 var hash = window.location.hash.slice(1).toLowerCase();
73 * @param {string} id Tab id.
74 * @return {boolean} True if successful.
76 function selectTab(id) {
77 closePortForwardingConfig();
79 var tabContents = document.querySelectorAll('#content > div');
80 var tabHeaders = $('navigation').querySelectorAll('.tab-header');
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');
90 tabContent.classList.remove('selected');
91 tabHeader.classList.remove('selected');
96 window.location.hash = id;
100 function populateTargets(source, data) {
101 if (source == 'local')
102 populateLocalTargets(data);
103 else if (source == 'remote')
104 populateRemoteTargets(data);
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]);
129 addToOthersList(data[i]);
133 function showIncognitoWarning() {
134 $('devices-incognito').hidden = false;
137 function alreadyDisplayed(element, data) {
138 var json = JSON.stringify(data);
139 if (element.cachedJSON == json)
141 element.cachedJSON = json;
145 function updateBrowserVisibility(browserSection) {
146 var icon = browserSection.querySelector('.used-for-port-forwarding');
147 browserSection.hidden = !browserSection.querySelector('.open') &&
148 !browserSection.querySelector('.row') &&
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');
161 users.add(browserUser.textContent);
164 var hasSingleUser = users.size <= 1;
166 Array.prototype.forEach.call(browsers, function(browserSection) {
167 var browserUser = browserSection.querySelector('.browser-user');
169 browserUser.hidden = hasSingleUser;
173 function populateRemoteTargets(devices) {
178 window.holdDevices = devices;
182 function insertChildSortedById(parent, child) {
183 for (var sibling = parent.firstElementChild;
185 sibling = sibling.nextElementSibling) {
186 if (sibling.id > child.id) {
187 parent.insertBefore(child, sibling);
191 parent.appendChild(child);
194 var deviceList = $('devices-list');
195 if (alreadyDisplayed(deviceList, devices))
198 function removeObsolete(validIds, section) {
199 if (validIds.indexOf(section.id) < 0)
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);
250 if (alreadyDisplayed(deviceSection, device))
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');
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;
269 var browserSection = $(browser.id);
270 if (browserSection) {
271 pageList = browserSection.querySelector('.pages');
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);
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) {
306 'open', sourceId, browserId, input.value || 'about:blank');
308 }.bind(null, browser.source, browser.id, newPageUrl);
309 newPageUrl.addEventListener('keyup', function(handler, event) {
310 if (event.keyIdentifier == 'Enter' && event.target.value)
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);
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(
337 sendCommand.bind(null, 'inspect-browser', browser.source,
338 browser.id, browserInspector), false);
341 pageList = document.createElement('div');
342 pageList.className = 'list pages';
343 browserSection.appendChild(pageList);
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);
358 addFavicon(row, page);
359 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
360 addActionLink(row, 'focus tab',
361 sendTargetCommand.bind(null, 'activate', page), false);
363 if (majorChromeVersion) {
364 addActionLink(row, 'reload',
365 sendTargetCommand.bind(null, 'reload', page), page.attached);
367 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
368 addActionLink(row, 'close',
369 sendTargetCommand.bind(null, 'close', page), false);
373 updateBrowserVisibility(browserSection);
375 updateUsernameVisibility(deviceSection);
379 function addToPagesList(data) {
380 var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
381 addFavicon(row, data);
383 addGuestViews(row, data.guests);
386 function addToExtensionsList(data) {
387 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
388 addFavicon(row, data);
390 addGuestViews(row, data.guests);
393 function addToAppsList(data) {
394 var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
395 addFavicon(row, data);
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);
408 function addToWorkersList(data) {
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 == '') {
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;
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) {
454 webview = JSON.parse(data['description']);
458 addWebViewDescription(row, webview);
459 if (data.adbScreenWidth && data.adbScreenHeight)
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;
473 viewStatus.visibility = 'empty';
475 if (webview.attached) {
476 viewStatus.position =
477 'at (' + webview.screenX + ', ' + webview.screenY + ')';
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;
496 var screenRectHeight;
498 var aspectRatio = screenWidth / screenHeight;
499 if (aspectRatio < 1) {
500 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
501 screenRectHeight = maxScreenRectSize;
503 screenRectWidth = maxScreenRectSize;
504 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
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 + '%';
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);
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);
570 list.appendChild(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);
579 link.classList.add('disabled');
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') {
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) {
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);
626 commitFreshLineIfValid(true /* select new line */);
627 commitPortForwardingConfig(false);
630 commitPortForwardingConfig(true);
635 commitPortForwardingConfig(true);
640 function setModal(dialog) {
641 dialog.deactivatedNodes = Array.prototype.filter.call(
642 document.querySelectorAll('*'),
644 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
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');
660 node.setAttribute('tabindex', dialog.tabIndexes[i]);
663 if (window.holdDevices) {
664 populateRemoteTargets(window.holdDevices);
665 delete window.holdDevices;
668 delete dialog.deactivatedNodes;
669 delete dialog.tabIndexes;
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');
683 $('port-forwarding-config-done').focus();
685 setModal($('port-forwarding-overlay'));
688 function closePortForwardingConfig() {
689 if (!$('port-forwarding-overlay').classList.contains('open'))
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) {
707 closePortForwardingConfig();
709 commitFreshLineIfValid();
710 var lines = document.querySelectorAll('.port-forwarding-pair');
712 for (var i = 0; i != lines.length; i++) {
714 var portInput = line.querySelector('.port');
715 var locationInput = line.querySelector('.location');
717 var port = portInput.classList.contains('invalid') ?
718 portInput.lastValidValue :
721 var location = locationInput.classList.contains('invalid') ?
722 locationInput.lastValidValue :
725 if (port && location)
726 config[port] = location;
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))
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);
776 line.appendChild(lineDelete);
778 line.addEventListener('click', selectLine.bind(null, line));
779 line.addEventListener('focus', selectLine.bind(null, line));
781 checkEmptyLine(line);
786 function validatePort(input) {
787 var match = input.value.match(/^(\d+)$/);
790 var port = parseInt(match[1]);
791 if (port < 1024 || 65535 < port)
794 var inputs = document.querySelectorAll('input.port:not(.invalid)');
795 for (var i = 0; i != inputs.length; ++i) {
796 if (inputs[i] == input)
798 if (parseInt(inputs[i].value) == port)
804 function validateLocation(input) {
805 var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
808 var port = parseInt(match[2]);
809 return port <= 65535;
812 function createEmptyConfigLine() {
813 var line = createConfigLine('', '');
814 line.classList.add('fresh');
818 function createConfigField(value, className, hint, validate) {
819 var input = document.createElement('input');
820 input.className = className;
822 input.placeholder = hint;
824 input.lastValidValue = value;
826 function checkInput() {
828 input.classList.remove('invalid');
830 input.classList.add('invalid');
831 if (input.parentNode)
832 checkEmptyLine(input.parentNode);
836 input.addEventListener('keyup', checkInput);
837 input.addEventListener('focus', function() {
838 selectLine(input.parentNode);
841 input.addEventListener('blur', function() {
843 input.lastValidValue = input.value;
849 function checkEmptyLine(line) {
850 var inputs = line.querySelectorAll('input');
852 for (var i = 0; i != inputs.length; i++) {
853 if (inputs[i].value != '')
857 line.classList.add('empty');
859 line.classList.remove('empty');
862 function selectLine(line) {
863 if (line.classList.contains('selected'))
866 line.classList.add('selected');
869 function unselectLine() {
870 var line = document.querySelector('.port-forwarding-pair.selected');
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'))
881 line.classList.remove('fresh');
882 var freshLine = createEmptyConfigLine();
883 line.parentNode.appendChild(freshLine);
885 freshLine.querySelector('.port').focus();
889 function populatePortStatus(devicesStatusMap) {
890 for (var deviceId in devicesStatusMap) {
891 if (!devicesStatusMap.hasOwnProperty(deviceId))
893 var deviceStatus = devicesStatusMap[deviceId];
894 var deviceStatusMap = deviceStatus.ports;
896 var deviceSection = $(deviceId);
900 var devicePorts = deviceSection.querySelector('.device-ports');
901 if (alreadyDisplayed(devicePorts, deviceStatus))
904 devicePorts.textContent = '';
905 for (var port in deviceStatusMap) {
906 if (!deviceStatusMap.hasOwnProperty(port))
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).
916 portIcon.classList.add('connected');
917 else if (status === -1 || status === -2)
918 portIcon.classList.add('transient');
920 portIcon.classList.add('error');
921 devicePorts.appendChild(portIcon);
923 var portNumber = document.createElement('div');
924 portNumber.className = 'port-number';
925 portNumber.textContent = ':' + port;
927 portNumber.textContent += '(' + status + ')';
928 devicePorts.appendChild(portNumber);
931 function updatePortForwardingInfo(browserSection) {
932 var icon = browserSection.querySelector('.used-for-port-forwarding');
934 icon.hidden = (browserSection.id !== deviceStatus.browserId);
935 updateBrowserVisibility(browserSection);
938 Array.prototype.forEach.call(
939 deviceSection.querySelectorAll('.browser'), updatePortForwardingInfo);
941 updateUsernameVisibility(deviceSection);
944 function clearPorts(deviceSection) {
945 if (deviceSection.id in devicesStatusMap)
947 deviceSection.querySelector('.device-ports').textContent = '';
950 Array.prototype.forEach.call(
951 document.querySelectorAll('.device'), clearPorts);
954 document.addEventListener('DOMContentLoaded', onload);
956 window.addEventListener('hashchange', onHashChange);