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
) {
177 if ($('port-forwarding-config').open
) {
178 window
.holdDevices
= devices
;
182 function browserCompare(a
, b
) {
183 if (a
.adbBrowserName
!= b
.adbBrowserName
)
184 return a
.adbBrowserName
< b
.adbBrowserName
;
185 if (a
.adbBrowserVersion
!= b
.adbBrowserVersion
)
186 return a
.adbBrowserVersion
< b
.adbBrowserVersion
;
190 function insertBrowser(browserList
, browser
) {
191 for (var sibling
= browserList
.firstElementChild
; sibling
;
192 sibling
= sibling
.nextElementSibling
) {
193 if (browserCompare(browser
, sibling
)) {
194 browserList
.insertBefore(browser
, sibling
);
198 browserList
.appendChild(browser
);
201 var deviceList
= $('devices-list');
202 if (alreadyDisplayed(deviceList
, devices
))
205 function removeObsolete(validIds
, section
) {
206 if (validIds
.indexOf(section
.id
) < 0)
210 var newDeviceIds
= devices
.map(function(d
) { return d
.id
});
211 Array
.prototype.forEach
.call(
212 deviceList
.querySelectorAll('.device'),
213 removeObsolete
.bind(null, newDeviceIds
));
215 $('devices-help').hidden
= !!devices
.length
;
217 for (var d
= 0; d
< devices
.length
; d
++) {
218 var device
= devices
[d
];
220 var deviceSection
= $(device
.id
);
221 if (!deviceSection
) {
222 deviceSection
= document
.createElement('div');
223 deviceSection
.id
= device
.id
;
224 deviceSection
.className
= 'device';
225 deviceList
.appendChild(deviceSection
);
227 var deviceHeader
= document
.createElement('div');
228 deviceHeader
.className
= 'device-header';
229 deviceSection
.appendChild(deviceHeader
);
231 var deviceName
= document
.createElement('div');
232 deviceName
.className
= 'device-name';
233 deviceHeader
.appendChild(deviceName
);
235 var deviceSerial
= document
.createElement('div');
236 deviceSerial
.className
= 'device-serial';
237 var serial
= device
.adbSerial
.toUpperCase();
238 deviceSerial
.textContent
= '#' + serial
;
239 deviceHeader
.appendChild(deviceSerial
);
241 if (serial
=== WEBRTC_SERIAL
)
242 deviceHeader
.classList
.add('hidden');
244 var devicePorts
= document
.createElement('div');
245 devicePorts
.className
= 'device-ports';
246 deviceHeader
.appendChild(devicePorts
);
248 var browserList
= document
.createElement('div');
249 browserList
.className
= 'browsers';
250 deviceSection
.appendChild(browserList
);
252 var authenticating
= document
.createElement('div');
253 authenticating
.className
= 'device-auth';
254 deviceSection
.appendChild(authenticating
);
257 if (alreadyDisplayed(deviceSection
, device
))
260 deviceSection
.querySelector('.device-name').textContent
= device
.adbModel
;
261 deviceSection
.querySelector('.device-auth').textContent
=
262 device
.adbConnected
? '' : 'Pending authentication: please accept ' +
263 'debugging session on the device.';
265 var browserList
= deviceSection
.querySelector('.browsers');
267 device
.browsers
.map(function(b
) { return b
.id
});
268 Array
.prototype.forEach
.call(
269 browserList
.querySelectorAll('.browser'),
270 removeObsolete
.bind(null, newBrowserIds
));
272 for (var b
= 0; b
< device
.browsers
.length
; b
++) {
273 var browser
= device
.browsers
[b
];
274 var majorChromeVersion
= browser
.adbBrowserChromeVersion
;
276 var browserSection
= $(browser
.id
);
277 if (browserSection
) {
278 pageList
= browserSection
.querySelector('.pages');
280 browserSection
= document
.createElement('div');
281 browserSection
.id
= browser
.id
;
282 browserSection
.className
= 'browser';
283 insertBrowser(browserList
, browserSection
);
285 var browserHeader
= document
.createElement('div');
286 browserHeader
.className
= 'browser-header';
288 var browserName
= document
.createElement('div');
289 browserName
.className
= 'browser-name';
290 browserHeader
.appendChild(browserName
);
291 browserName
.textContent
= browser
.adbBrowserName
;
292 if (browser
.adbBrowserVersion
)
293 browserName
.textContent
+= ' (' + browser
.adbBrowserVersion
+ ')';
294 if (browser
.adbBrowserUser
) {
295 var browserUser
= document
.createElement('div');
296 browserUser
.className
= 'browser-user';
297 browserUser
.textContent
= browser
.adbBrowserUser
;
298 browserHeader
.appendChild(browserUser
);
300 browserSection
.appendChild(browserHeader
);
302 if (majorChromeVersion
>= MIN_VERSION_NEW_TAB
) {
303 var newPage
= document
.createElement('div');
304 newPage
.className
= 'open';
306 var newPageUrl
= document
.createElement('input');
307 newPageUrl
.type
= 'text';
308 newPageUrl
.placeholder
= 'Open tab with url';
309 newPage
.appendChild(newPageUrl
);
311 var openHandler = function(sourceId
, browserId
, input
) {
313 'open', sourceId
, browserId
, input
.value
|| 'about:blank');
315 }.bind(null, browser
.source
, browser
.id
, newPageUrl
);
316 newPageUrl
.addEventListener('keyup', function(handler
, event
) {
317 if (event
.keyIdentifier
== 'Enter' && event
.target
.value
)
319 }.bind(null, openHandler
), true);
321 var newPageButton
= document
.createElement('button');
322 newPageButton
.textContent
= 'Open';
323 newPage
.appendChild(newPageButton
);
324 newPageButton
.addEventListener('click', openHandler
, true);
326 browserHeader
.appendChild(newPage
);
329 var portForwardingInfo
= document
.createElement('div');
330 portForwardingInfo
.className
= 'used-for-port-forwarding';
331 portForwardingInfo
.hidden
= true;
332 portForwardingInfo
.title
= 'This browser is used for port ' +
333 'forwarding. Closing it will drop current connections.';
334 browserHeader
.appendChild(portForwardingInfo
);
336 if (browserInspector
) {
337 var link
= document
.createElement('span');
338 link
.classList
.add('action');
339 link
.setAttribute('tabindex', 1);
340 link
.textContent
= browserInspectorTitle
;
341 browserHeader
.appendChild(link
);
342 link
.addEventListener(
344 sendCommand
.bind(null, 'inspect-browser', browser
.source
,
345 browser
.id
, browserInspector
), false);
348 pageList
= document
.createElement('div');
349 pageList
.className
= 'list pages';
350 browserSection
.appendChild(pageList
);
353 if (!alreadyDisplayed(browserSection
, browser
)) {
354 pageList
.textContent
= '';
355 for (var p
= 0; p
< browser
.pages
.length
; p
++) {
356 var page
= browser
.pages
[p
];
357 // Attached targets have no unique id until Chrome 26. For such
358 // targets it is impossible to activate existing DevTools window.
359 page
.hasNoUniqueId
= page
.attached
&&
360 majorChromeVersion
&& majorChromeVersion
< MIN_VERSION_TARGET_ID
;
361 var row
= addTargetToList(page
, pageList
, ['name', 'url']);
362 if (page
['description'])
363 addWebViewDetails(row
, page
);
365 addFavicon(row
, page
);
366 if (majorChromeVersion
>= MIN_VERSION_TAB_ACTIVATE
) {
367 addActionLink(row
, 'focus tab',
368 sendTargetCommand
.bind(null, 'activate', page
), false);
370 if (majorChromeVersion
) {
371 addActionLink(row
, 'reload',
372 sendTargetCommand
.bind(null, 'reload', page
), page
.attached
);
374 if (majorChromeVersion
>= MIN_VERSION_TAB_CLOSE
) {
375 addActionLink(row
, 'close',
376 sendTargetCommand
.bind(null, 'close', page
), false);
380 updateBrowserVisibility(browserSection
);
382 updateUsernameVisibility(deviceSection
);
386 function addToPagesList(data
) {
387 var row
= addTargetToList(data
, $('pages-list'), ['name', 'url']);
388 addFavicon(row
, data
);
390 addGuestViews(row
, data
.guests
);
393 function addToExtensionsList(data
) {
394 var row
= addTargetToList(data
, $('extensions-list'), ['name', 'url']);
395 addFavicon(row
, data
);
397 addGuestViews(row
, data
.guests
);
400 function addToAppsList(data
) {
401 var row
= addTargetToList(data
, $('apps-list'), ['name', 'url']);
402 addFavicon(row
, data
);
404 addGuestViews(row
, data
.guests
);
407 function addGuestViews(row
, guests
) {
408 Array
.prototype.forEach
.call(guests
, function(guest
) {
409 var guestRow
= addTargetToList(guest
, row
, ['name', 'url']);
410 guestRow
.classList
.add('guest');
411 addFavicon(guestRow
, guest
);
415 function addToWorkersList(data
) {
417 addTargetToList(data
, $('workers-list'), ['name', 'description', 'url']);
418 addActionLink(row
, 'terminate',
419 sendTargetCommand
.bind(null, 'close', data
), false);
422 function addToServiceWorkersList(data
) {
423 var row
= addTargetToList(
424 data
, $('service-workers-list'), ['name', 'description', 'url']);
425 addActionLink(row
, 'terminate',
426 sendTargetCommand
.bind(null, 'close', data
), false);
429 function addToOthersList(data
) {
430 addTargetToList(data
, $('others-list'), ['url']);
433 function formatValue(data
, property
) {
434 var value
= data
[property
];
436 if (property
== 'name' && value
== '') {
440 var text
= value
? String(value
) : '';
441 if (text
.length
> 100)
442 text
= text
.substring(0, 100) + '\u2026';
444 var div
= document
.createElement('div');
445 div
.textContent
= text
;
446 div
.className
= property
;
450 function addFavicon(row
, data
) {
451 var favicon
= document
.createElement('img');
452 if (data
['faviconUrl'])
453 favicon
.src
= data
['faviconUrl'];
454 var propertiesBox
= row
.querySelector('.properties-box');
455 propertiesBox
.insertBefore(favicon
, propertiesBox
.firstChild
);
458 function addWebViewDetails(row
, data
) {
461 webview
= JSON
.parse(data
['description']);
465 addWebViewDescription(row
, webview
);
466 if (data
.adbScreenWidth
&& data
.adbScreenHeight
)
468 row
, webview
, data
.adbScreenWidth
, data
.adbScreenHeight
);
471 function addWebViewDescription(row
, webview
) {
472 var viewStatus
= { visibility
: '', position
: '', size
: '' };
473 if (!webview
.empty
) {
474 if (webview
.attached
&& !webview
.visible
)
475 viewStatus
.visibility
= 'hidden';
476 else if (!webview
.attached
)
477 viewStatus
.visibility
= 'detached';
478 viewStatus
.size
= 'size ' + webview
.width
+ ' \u00d7 ' + webview
.height
;
480 viewStatus
.visibility
= 'empty';
482 if (webview
.attached
) {
483 viewStatus
.position
=
484 'at (' + webview
.screenX
+ ', ' + webview
.screenY
+ ')';
487 var subRow
= document
.createElement('div');
488 subRow
.className
= 'subrow webview';
489 if (webview
.empty
|| !webview
.attached
|| !webview
.visible
)
490 subRow
.className
+= ' invisible-view';
491 if (viewStatus
.visibility
)
492 subRow
.appendChild(formatValue(viewStatus
, 'visibility'));
493 if (viewStatus
.position
)
494 subRow
.appendChild(formatValue(viewStatus
, 'position'));
495 subRow
.appendChild(formatValue(viewStatus
, 'size'));
496 var subrowBox
= row
.querySelector('.subrow-box');
497 subrowBox
.insertBefore(subRow
, row
.querySelector('.actions'));
500 function addWebViewThumbnail(row
, webview
, screenWidth
, screenHeight
) {
501 var maxScreenRectSize
= 50;
503 var screenRectHeight
;
505 var aspectRatio
= screenWidth
/ screenHeight
;
506 if (aspectRatio
< 1) {
507 screenRectWidth
= Math
.round(maxScreenRectSize
* aspectRatio
);
508 screenRectHeight
= maxScreenRectSize
;
510 screenRectWidth
= maxScreenRectSize
;
511 screenRectHeight
= Math
.round(maxScreenRectSize
/ aspectRatio
);
514 var thumbnail
= document
.createElement('div');
515 thumbnail
.className
= 'webview-thumbnail';
516 var thumbnailWidth
= 3 * screenRectWidth
;
517 var thumbnailHeight
= 60;
518 thumbnail
.style
.width
= thumbnailWidth
+ 'px';
519 thumbnail
.style
.height
= thumbnailHeight
+ 'px';
521 var screenRect
= document
.createElement('div');
522 screenRect
.className
= 'screen-rect';
523 screenRect
.style
.left
= screenRectWidth
+ 'px';
524 screenRect
.style
.top
= (thumbnailHeight
- screenRectHeight
) / 2 + 'px';
525 screenRect
.style
.width
= screenRectWidth
+ 'px';
526 screenRect
.style
.height
= screenRectHeight
+ 'px';
527 thumbnail
.appendChild(screenRect
);
529 if (!webview
.empty
&& webview
.attached
) {
530 var viewRect
= document
.createElement('div');
531 viewRect
.className
= 'view-rect';
532 if (!webview
.visible
)
533 viewRect
.classList
.add('hidden');
534 function percent(ratio
) {
535 return ratio
* 100 + '%';
537 viewRect
.style
.left
= percent(webview
.screenX
/ screenWidth
);
538 viewRect
.style
.top
= percent(webview
.screenY
/ screenHeight
);
539 viewRect
.style
.width
= percent(webview
.width
/ screenWidth
);
540 viewRect
.style
.height
= percent(webview
.height
/ screenHeight
);
541 screenRect
.appendChild(viewRect
);
544 var propertiesBox
= row
.querySelector('.properties-box');
545 propertiesBox
.insertBefore(thumbnail
, propertiesBox
.firstChild
);
548 function addTargetToList(data
, list
, properties
) {
549 var row
= document
.createElement('div');
550 row
.className
= 'row';
551 row
.targetId
= data
.id
;
553 var propertiesBox
= document
.createElement('div');
554 propertiesBox
.className
= 'properties-box';
555 row
.appendChild(propertiesBox
);
557 var subrowBox
= document
.createElement('div');
558 subrowBox
.className
= 'subrow-box';
559 propertiesBox
.appendChild(subrowBox
);
561 var subrow
= document
.createElement('div');
562 subrow
.className
= 'subrow';
563 subrowBox
.appendChild(subrow
);
565 for (var j
= 0; j
< properties
.length
; j
++)
566 subrow
.appendChild(formatValue(data
, properties
[j
]));
568 var actionBox
= document
.createElement('div');
569 actionBox
.className
= 'actions';
570 subrowBox
.appendChild(actionBox
);
572 if (!data
.hasCustomInspectAction
) {
573 addActionLink(row
, 'inspect', sendTargetCommand
.bind(null, 'inspect', data
),
574 data
.hasNoUniqueId
|| data
.adbAttachedForeign
);
577 list
.appendChild(row
);
581 function addActionLink(row
, text
, handler
, opt_disabled
) {
582 var link
= document
.createElement('span');
583 link
.classList
.add('action');
584 link
.setAttribute('tabindex', 1);
586 link
.classList
.add('disabled');
588 link
.classList
.remove('disabled');
590 link
.textContent
= text
;
591 link
.addEventListener('click', handler
, true);
592 function handleKey(e
) {
593 if (e
.keyIdentifier
== 'Enter' || e
.keyIdentifier
== 'U+0020') {
598 link
.addEventListener('keydown', handleKey
, true);
599 row
.querySelector('.actions').appendChild(link
);
603 function initSettings() {
604 $('discover-usb-devices-enable').addEventListener('change',
605 enableDiscoverUsbDevices
);
607 $('port-forwarding-enable').addEventListener('change', enablePortForwarding
);
608 $('port-forwarding-config-open').addEventListener(
609 'click', openPortForwardingConfig
);
610 $('port-forwarding-config-close').addEventListener(
611 'click', closePortForwardingConfig
);
612 $('port-forwarding-config-done').addEventListener(
613 'click', commitPortForwardingConfig
.bind(null, true));
616 function enableDiscoverUsbDevices(event
) {
617 sendCommand('set-discover-usb-devices-enabled', event
.target
.checked
);
620 function enablePortForwarding(event
) {
621 sendCommand('set-port-forwarding-enabled', event
.target
.checked
);
624 function handleKey(event
) {
625 switch (event
.keyCode
) {
627 if (event
.target
.nodeName
== 'INPUT') {
628 var line
= event
.target
.parentNode
;
629 if (!line
.classList
.contains('fresh') ||
630 line
.classList
.contains('empty')) {
631 commitPortForwardingConfig(true);
633 commitFreshLineIfValid(true /* select new line */);
634 commitPortForwardingConfig(false);
637 commitPortForwardingConfig(true);
643 function openPortForwardingConfig() {
644 loadPortForwardingConfig(window
.portForwardingConfig
);
646 $('port-forwarding-config').showModal();
647 document
.addEventListener('keyup', handleKey
);
648 $('port-forwarding-config').onclose = function() {
649 commitPortForwardingConfig(true);
652 var freshPort
= document
.querySelector('.fresh .port');
656 $('port-forwarding-config-done').focus();
659 function closePortForwardingConfig() {
660 if (!$('port-forwarding-config').open
)
663 $('port-forwarding-config').onclose
= null;
664 $('port-forwarding-config').close();
665 document
.removeEventListener('keyup', handleKey
);
667 if (window
.holdDevices
) {
668 populateRemoteTargets(window
.holdDevices
);
669 delete window
.holdDevices
;
673 function loadPortForwardingConfig(config
) {
674 var list
= $('port-forwarding-config-list');
675 list
.textContent
= '';
676 for (var port
in config
)
677 list
.appendChild(createConfigLine(port
, config
[port
]));
678 list
.appendChild(createEmptyConfigLine());
681 function commitPortForwardingConfig(closeConfig
) {
683 closePortForwardingConfig();
685 commitFreshLineIfValid();
686 var lines
= document
.querySelectorAll('.port-forwarding-pair');
688 for (var i
= 0; i
!= lines
.length
; i
++) {
690 var portInput
= line
.querySelector('.port');
691 var locationInput
= line
.querySelector('.location');
693 var port
= portInput
.classList
.contains('invalid') ?
694 portInput
.lastValidValue
:
697 var location
= locationInput
.classList
.contains('invalid') ?
698 locationInput
.lastValidValue
:
701 if (port
&& location
)
702 config
[port
] = location
;
704 sendCommand('set-port-forwarding-config', config
);
707 function updateDiscoverUsbDevicesEnabled(enabled
) {
708 var checkbox
= $('discover-usb-devices-enable');
709 checkbox
.checked
= !!enabled
;
710 checkbox
.disabled
= false;
713 function updatePortForwardingEnabled(enabled
) {
714 var checkbox
= $('port-forwarding-enable');
715 checkbox
.checked
= !!enabled
;
716 checkbox
.disabled
= false;
719 function updatePortForwardingConfig(config
) {
720 window
.portForwardingConfig
= config
;
721 $('port-forwarding-config-open').disabled
= !config
;
724 function createConfigLine(port
, location
) {
725 var line
= document
.createElement('div');
726 line
.className
= 'port-forwarding-pair';
728 var portInput
= createConfigField(port
, 'port', 'Port', validatePort
);
729 line
.appendChild(portInput
);
731 var locationInput
= createConfigField(
732 location
, 'location', 'IP address and port', validateLocation
);
733 line
.appendChild(locationInput
);
734 locationInput
.addEventListener('keydown', function(e
) {
735 if (e
.keyIdentifier
== 'U+0009' && // Tab
736 !e
.ctrlKey
&& !e
.altKey
&& !e
.shiftKey
&& !e
.metaKey
&&
737 line
.classList
.contains('fresh') &&
738 !line
.classList
.contains('empty')) {
739 // Tabbing forward on the fresh line, try create a new empty one.
740 if (commitFreshLineIfValid(true))
745 var lineDelete
= document
.createElement('div');
746 lineDelete
.className
= 'close-button';
747 lineDelete
.addEventListener('click', function() {
748 var newSelection
= line
.nextElementSibling
;
749 line
.parentNode
.removeChild(line
);
750 selectLine(newSelection
);
752 line
.appendChild(lineDelete
);
754 line
.addEventListener('click', selectLine
.bind(null, line
));
755 line
.addEventListener('focus', selectLine
.bind(null, line
));
757 checkEmptyLine(line
);
762 function validatePort(input
) {
763 var match
= input
.value
.match(/^(\d+)$/);
766 var port
= parseInt(match
[1]);
767 if (port
< 1024 || 65535 < port
)
770 var inputs
= document
.querySelectorAll('input.port:not(.invalid)');
771 for (var i
= 0; i
!= inputs
.length
; ++i
) {
772 if (inputs
[i
] == input
)
774 if (parseInt(inputs
[i
].value
) == port
)
780 function validateLocation(input
) {
781 var match
= input
.value
.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
784 var port
= parseInt(match
[2]);
785 return port
<= 65535;
788 function createEmptyConfigLine() {
789 var line
= createConfigLine('', '');
790 line
.classList
.add('fresh');
794 function createConfigField(value
, className
, hint
, validate
) {
795 var input
= document
.createElement('input');
796 input
.className
= className
;
798 input
.placeholder
= hint
;
800 input
.lastValidValue
= value
;
802 function checkInput() {
804 input
.classList
.remove('invalid');
806 input
.classList
.add('invalid');
807 if (input
.parentNode
)
808 checkEmptyLine(input
.parentNode
);
812 input
.addEventListener('keyup', checkInput
);
813 input
.addEventListener('focus', function() {
814 selectLine(input
.parentNode
);
817 input
.addEventListener('blur', function() {
819 input
.lastValidValue
= input
.value
;
825 function checkEmptyLine(line
) {
826 var inputs
= line
.querySelectorAll('input');
828 for (var i
= 0; i
!= inputs
.length
; i
++) {
829 if (inputs
[i
].value
!= '')
833 line
.classList
.add('empty');
835 line
.classList
.remove('empty');
838 function selectLine(line
) {
839 if (line
.classList
.contains('selected'))
842 line
.classList
.add('selected');
845 function unselectLine() {
846 var line
= document
.querySelector('.port-forwarding-pair.selected');
849 line
.classList
.remove('selected');
850 commitFreshLineIfValid();
853 function commitFreshLineIfValid(opt_selectNew
) {
854 var line
= document
.querySelector('.port-forwarding-pair.fresh');
855 if (line
.querySelector('.invalid'))
857 line
.classList
.remove('fresh');
858 var freshLine
= createEmptyConfigLine();
859 line
.parentNode
.appendChild(freshLine
);
861 freshLine
.querySelector('.port').focus();
865 function populatePortStatus(devicesStatusMap
) {
866 for (var deviceId
in devicesStatusMap
) {
867 if (!devicesStatusMap
.hasOwnProperty(deviceId
))
869 var deviceStatus
= devicesStatusMap
[deviceId
];
870 var deviceStatusMap
= deviceStatus
.ports
;
872 var deviceSection
= $(deviceId
);
876 var devicePorts
= deviceSection
.querySelector('.device-ports');
877 if (alreadyDisplayed(devicePorts
, deviceStatus
))
880 devicePorts
.textContent
= '';
881 for (var port
in deviceStatusMap
) {
882 if (!deviceStatusMap
.hasOwnProperty(port
))
885 var status
= deviceStatusMap
[port
];
886 var portIcon
= document
.createElement('div');
887 portIcon
.className
= 'port-icon';
888 // status === 0 is the default (connected) state.
889 if (status
=== -1 || status
=== -2)
890 portIcon
.classList
.add('transient');
892 portIcon
.classList
.add('error');
893 devicePorts
.appendChild(portIcon
);
895 var portNumber
= document
.createElement('div');
896 portNumber
.className
= 'port-number';
897 portNumber
.textContent
= ':' + port
;
898 devicePorts
.appendChild(portNumber
);
901 function updatePortForwardingInfo(browserSection
) {
902 var icon
= browserSection
.querySelector('.used-for-port-forwarding');
904 icon
.hidden
= (browserSection
.id
!== deviceStatus
.browserId
);
905 updateBrowserVisibility(browserSection
);
908 Array
.prototype.forEach
.call(
909 deviceSection
.querySelectorAll('.browser'), updatePortForwardingInfo
);
911 updateUsernameVisibility(deviceSection
);
914 function clearPorts(deviceSection
) {
915 if (deviceSection
.id
in devicesStatusMap
)
917 deviceSection
.querySelector('.device-ports').textContent
= '';
920 Array
.prototype.forEach
.call(
921 document
.querySelectorAll('.device'), clearPorts
);
924 document
.addEventListener('DOMContentLoaded', onload
);
926 window
.addEventListener('hashchange', onHashChange
);