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;
10 function sendCommand(command, args) {
11 chrome.send(command, Array.prototype.slice.call(arguments, 1));
14 function sendTargetCommand(command, target) {
15 sendCommand(command, target.source, target.id);
18 function removeChildren(element_id) {
19 var element = $(element_id);
20 element.textContent = '';
24 var tabContents = document.querySelectorAll('#content > div');
25 for (var i = 0; i != tabContents.length; i++) {
26 var tabContent = tabContents[i];
27 var tabName = tabContent.querySelector('.content-header').textContent;
29 var tabHeader = document.createElement('div');
30 tabHeader.className = 'tab-header';
31 var button = document.createElement('button');
32 button.textContent = tabName;
33 tabHeader.appendChild(button);
34 tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
35 $('navigation').appendChild(tabHeader);
39 sendCommand('init-ui');
42 function onHashChange() {
43 var hash = window.location.hash.slice(1).toLowerCase();
49 * @param {string} id Tab id.
50 * @return {boolean} True if successful.
52 function selectTab(id) {
53 closePortForwardingConfig();
55 var tabContents = document.querySelectorAll('#content > div');
56 var tabHeaders = $('navigation').querySelectorAll('.tab-header');
58 for (var i = 0; i != tabContents.length; i++) {
59 var tabContent = tabContents[i];
60 var tabHeader = tabHeaders[i];
61 if (tabContent.id == id) {
62 tabContent.classList.add('selected');
63 tabHeader.classList.add('selected');
66 tabContent.classList.remove('selected');
67 tabHeader.classList.remove('selected');
72 window.location.hash = id;
76 function populateTargets(source, data) {
77 if (source == 'renderers')
78 populateWebContentsTargets(data);
79 else if (source == 'workers')
80 populateWorkerTargets(data);
81 else if (source == 'adb')
82 populateRemoteTargets(data);
84 console.error('Unknown source type: ' + source);
87 function populateWebContentsTargets(data) {
88 removeChildren('pages-list');
89 removeChildren('extensions-list');
90 removeChildren('apps-list');
91 removeChildren('others-list');
93 for (var i = 0; i < data.length; i++) {
94 if (data[i].type === 'page')
95 addToPagesList(data[i]);
96 else if (data[i].type === 'background_page')
97 addToExtensionsList(data[i]);
98 else if (data[i].type === 'app')
99 addToAppsList(data[i]);
101 addToOthersList(data[i]);
105 function populateWorkerTargets(data) {
106 removeChildren('workers-list');
108 for (var i = 0; i < data.length; i++)
109 addToWorkersList(data[i]);
112 function populateRemoteTargets(devices) {
117 window.holdDevices = devices;
121 function alreadyDisplayed(element, data) {
122 var json = JSON.stringify(data);
123 if (element.cachedJSON == json)
125 element.cachedJSON = json;
129 function insertChildSortedById(parent, child) {
130 for (var sibling = parent.firstElementChild;
132 sibling = sibling.nextElementSibling) {
133 if (sibling.id > child.id) {
134 parent.insertBefore(child, sibling);
138 parent.appendChild(child);
141 var deviceList = $('devices-list');
142 if (alreadyDisplayed(deviceList, devices))
145 function removeObsolete(validIds, section) {
146 if (validIds.indexOf(section.id) < 0)
150 var newDeviceIds = devices.map(function(d) { return d.id });
151 Array.prototype.forEach.call(
152 deviceList.querySelectorAll('.device'),
153 removeObsolete.bind(null, newDeviceIds));
155 $('devices-help').hidden = !!devices.length;
157 for (var d = 0; d < devices.length; d++) {
158 var device = devices[d];
160 var deviceSection = $(device.id);
161 if (!deviceSection) {
162 deviceSection = document.createElement('div');
163 deviceSection.id = device.id;
164 deviceSection.className = 'device';
165 deviceList.appendChild(deviceSection);
167 var deviceHeader = document.createElement('div');
168 deviceHeader.className = 'device-header';
169 deviceSection.appendChild(deviceHeader);
171 var deviceName = document.createElement('div');
172 deviceName.className = 'device-name';
173 deviceHeader.appendChild(deviceName);
175 var deviceSerial = document.createElement('div');
176 deviceSerial.className = 'device-serial';
177 deviceSerial.textContent = '#' + device.adbSerial.toUpperCase();
178 deviceHeader.appendChild(deviceSerial);
180 var devicePorts = document.createElement('div');
181 devicePorts.className = 'device-ports';
182 deviceHeader.appendChild(devicePorts);
184 var browserList = document.createElement('div');
185 browserList.className = 'browsers';
186 deviceSection.appendChild(browserList);
188 var authenticating = document.createElement('div');
189 authenticating.className = 'device-auth';
190 deviceSection.appendChild(authenticating);
193 if (alreadyDisplayed(deviceSection, device))
196 deviceSection.querySelector('.device-name').textContent = device.adbModel;
197 deviceSection.querySelector('.device-auth').textContent =
198 device.adbConnected ? '' : 'Pending authentication: please accept ' +
199 'debugging session on the device.';
201 var browserList = deviceSection.querySelector('.browsers');
203 device.browsers.map(function(b) { return b.id });
204 Array.prototype.forEach.call(
205 browserList.querySelectorAll('.browser'),
206 removeObsolete.bind(null, newBrowserIds));
208 for (var b = 0; b < device.browsers.length; b++) {
209 var browser = device.browsers[b];
211 var majorChromeVersion = browser.adbBrowserChromeVersion;
213 var incompatibleVersion = browser.hasOwnProperty('compatibleVersion') &&
214 !browser.compatibleVersion;
216 var browserSection = $(browser.id);
217 if (browserSection) {
218 pageList = browserSection.querySelector('.pages');
220 browserSection = document.createElement('div');
221 browserSection.id = browser.id;
222 browserSection.className = 'browser';
223 insertChildSortedById(browserList, browserSection);
225 var browserHeader = document.createElement('div');
226 browserHeader.className = 'browser-header';
228 var browserName = document.createElement('div');
229 browserName.className = 'browser-name';
230 browserHeader.appendChild(browserName);
231 browserName.textContent = browser.adbBrowserName;
232 if (browser.adbBrowserVersion)
233 browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
234 browserSection.appendChild(browserHeader);
236 if (incompatibleVersion) {
237 var warningSection = document.createElement('div');
238 warningSection.className = 'warning';
239 warningSection.textContent =
240 'You may need a newer version of desktop Chrome. ' +
241 'Please try Chrome ' + browser.adbBrowserVersion + ' or later.';
242 browserHeader.appendChild(warningSection);
243 } else if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
244 var newPage = document.createElement('div');
245 newPage.className = 'open';
247 var newPageUrl = document.createElement('input');
248 newPageUrl.type = 'text';
249 newPageUrl.placeholder = 'Open tab with url';
250 newPage.appendChild(newPageUrl);
252 var openHandler = function(sourceId, browserId, input) {
254 'open', sourceId, browserId, input.value || 'about:blank');
256 }.bind(null, browser.source, browser.id, newPageUrl);
257 newPageUrl.addEventListener('keyup', function(handler, event) {
258 if (event.keyIdentifier == 'Enter' && event.target.value)
260 }.bind(null, openHandler), true);
262 var newPageButton = document.createElement('button');
263 newPageButton.textContent = 'Open';
264 newPage.appendChild(newPageButton);
265 newPageButton.addEventListener('click', openHandler, true);
267 browserHeader.appendChild(newPage);
270 pageList = document.createElement('div');
271 pageList.className = 'list pages';
272 browserSection.appendChild(pageList);
275 if (incompatibleVersion || alreadyDisplayed(browserSection, browser))
278 pageList.textContent = '';
279 for (var p = 0; p < browser.pages.length; p++) {
280 var page = browser.pages[p];
281 // Attached targets have no unique id until Chrome 26. For such targets
282 // it is impossible to activate existing DevTools window.
283 page.hasNoUniqueId = page.attached &&
284 (majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID);
285 var row = addTargetToList(page, pageList, ['name', 'url']);
286 if (page['description'])
287 addWebViewDetails(row, page);
289 addFavicon(row, page);
290 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
291 addActionLink(row, 'focus tab',
292 sendTargetCommand.bind(null, 'activate', page), false);
294 if (majorChromeVersion) {
295 addActionLink(row, 'reload',
296 sendTargetCommand.bind(null, 'reload', page), page.attached);
298 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
299 addActionLink(row, 'close',
300 sendTargetCommand.bind(null, 'close', page), page.attached);
307 function addToPagesList(data) {
308 var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
309 addFavicon(row, data);
311 addGuestViews(row, data.guests);
314 function addToExtensionsList(data) {
315 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
316 addFavicon(row, data);
318 addGuestViews(row, data.guests);
321 function addToAppsList(data) {
322 var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
323 addFavicon(row, data);
325 addGuestViews(row, data.guests);
328 function addGuestViews(row, guests) {
329 Array.prototype.forEach.call(guests, function(guest) {
330 var guestRow = addTargetToList(guest, row, ['name', 'url']);
331 guestRow.classList.add('guest');
332 addFavicon(guestRow, guest);
336 function addToWorkersList(data) {
338 addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
339 addActionLink(row, 'terminate',
340 sendTargetCommand.bind(null, 'close', data), data.attached);
343 function addToOthersList(data) {
344 addTargetToList(data, $('others-list'), ['url']);
347 function formatValue(data, property) {
348 var value = data[property];
350 if (property == 'name' && value == '') {
354 var text = value ? String(value) : '';
355 if (text.length > 100)
356 text = text.substring(0, 100) + '\u2026';
358 var div = document.createElement('div');
359 div.textContent = text;
360 div.className = property;
364 function addFavicon(row, data) {
365 var favicon = document.createElement('img');
366 if (data['faviconUrl'])
367 favicon.src = data['faviconUrl'];
368 var propertiesBox = row.querySelector('.properties-box');
369 propertiesBox.insertBefore(favicon, propertiesBox.firstChild);
372 function addWebViewDetails(row, data) {
375 webview = JSON.parse(data['description']);
379 addWebViewDescription(row, webview);
380 if (data.adbScreenWidth && data.adbScreenHeight)
382 row, webview, data.adbScreenWidth, data.adbScreenHeight);
385 function addWebViewDescription(row, webview) {
386 var viewStatus = { visibility: '', position: '', size: '' };
387 if (!webview.empty) {
388 if (webview.attached && !webview.visible)
389 viewStatus.visibility = 'hidden';
390 else if (!webview.attached)
391 viewStatus.visibility = 'detached';
392 viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
394 viewStatus.visibility = 'empty';
396 if (webview.attached) {
397 viewStatus.position =
398 'at (' + webview.screenX + ', ' + webview.screenY + ')';
401 var subRow = document.createElement('div');
402 subRow.className = 'subrow webview';
403 if (webview.empty || !webview.attached || !webview.visible)
404 subRow.className += ' invisible-view';
405 if (viewStatus.visibility)
406 subRow.appendChild(formatValue(viewStatus, 'visibility'));
407 if (viewStatus.position)
408 subRow.appendChild(formatValue(viewStatus, 'position'));
409 subRow.appendChild(formatValue(viewStatus, 'size'));
410 var subrowBox = row.querySelector('.subrow-box');
411 subrowBox.insertBefore(subRow, row.querySelector('.actions'));
414 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
415 var maxScreenRectSize = 50;
417 var screenRectHeight;
419 var aspectRatio = screenWidth / screenHeight;
420 if (aspectRatio < 1) {
421 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
422 screenRectHeight = maxScreenRectSize;
424 screenRectWidth = maxScreenRectSize;
425 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
428 var thumbnail = document.createElement('div');
429 thumbnail.className = 'webview-thumbnail';
430 var thumbnailWidth = 3 * screenRectWidth;
431 var thumbnailHeight = 60;
432 thumbnail.style.width = thumbnailWidth + 'px';
433 thumbnail.style.height = thumbnailHeight + 'px';
435 var screenRect = document.createElement('div');
436 screenRect.className = 'screen-rect';
437 screenRect.style.left = screenRectWidth + 'px';
438 screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
439 screenRect.style.width = screenRectWidth + 'px';
440 screenRect.style.height = screenRectHeight + 'px';
441 thumbnail.appendChild(screenRect);
443 if (!webview.empty && webview.attached) {
444 var viewRect = document.createElement('div');
445 viewRect.className = 'view-rect';
446 if (!webview.visible)
447 viewRect.classList.add('hidden');
448 function percent(ratio) {
449 return ratio * 100 + '%';
451 viewRect.style.left = percent(webview.screenX / screenWidth);
452 viewRect.style.top = percent(webview.screenY / screenHeight);
453 viewRect.style.width = percent(webview.width / screenWidth);
454 viewRect.style.height = percent(webview.height / screenHeight);
455 screenRect.appendChild(viewRect);
458 var propertiesBox = row.querySelector('.properties-box');
459 propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild);
462 function addTargetToList(data, list, properties) {
463 var row = document.createElement('div');
464 row.className = 'row';
466 var propertiesBox = document.createElement('div');
467 propertiesBox.className = 'properties-box';
468 row.appendChild(propertiesBox);
470 var subrowBox = document.createElement('div');
471 subrowBox.className = 'subrow-box';
472 propertiesBox.appendChild(subrowBox);
474 var subrow = document.createElement('div');
475 subrow.className = 'subrow';
476 subrowBox.appendChild(subrow);
478 for (var j = 0; j < properties.length; j++)
479 subrow.appendChild(formatValue(data, properties[j]));
481 var actionBox = document.createElement('div');
482 actionBox.className = 'actions';
483 subrowBox.appendChild(actionBox);
485 addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data),
486 data.hasNoUniqueId || data.adbAttachedForeign);
488 list.appendChild(row);
492 function addActionLink(row, text, handler, opt_disabled) {
493 var link = document.createElement('span');
494 link.classList.add('action');
495 link.setAttribute('tabindex', 1);
497 link.classList.add('disabled');
499 link.classList.remove('disabled');
501 link.textContent = text;
502 link.addEventListener('click', handler, true);
503 row.querySelector('.actions').appendChild(link);
507 function initSettings() {
508 $('discover-usb-devices-enable').addEventListener('change',
509 enableDiscoverUsbDevices);
511 $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
512 $('port-forwarding-config-open').addEventListener(
513 'click', openPortForwardingConfig);
514 $('port-forwarding-config-close').addEventListener(
515 'click', closePortForwardingConfig);
516 $('port-forwarding-config-done').addEventListener(
517 'click', commitPortForwardingConfig.bind(true));
520 function enableDiscoverUsbDevices(event) {
521 sendCommand('set-discover-usb-devices-enabled', event.target.checked);
524 function enablePortForwarding(event) {
525 sendCommand('set-port-forwarding-enabled', event.target.checked);
528 function handleKey(event) {
529 switch (event.keyCode) {
531 if (event.target.nodeName == 'INPUT') {
532 var line = event.target.parentNode;
533 if (!line.classList.contains('fresh') ||
534 line.classList.contains('empty')) {
535 commitPortForwardingConfig(true);
537 commitFreshLineIfValid(true /* select new line */);
538 commitPortForwardingConfig(false);
541 commitPortForwardingConfig(true);
546 commitPortForwardingConfig(true);
551 function setModal(dialog) {
552 dialog.deactivatedNodes = Array.prototype.filter.call(
553 document.querySelectorAll('*'),
555 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
558 dialog.tabIndexes = dialog.deactivatedNodes.map(
559 function(n) { return n.getAttribute('tabindex'); });
561 dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; });
562 window.modal = dialog;
565 function unsetModal(dialog) {
566 for (var i = 0; i < dialog.deactivatedNodes.length; i++) {
567 var node = dialog.deactivatedNodes[i];
568 if (dialog.tabIndexes[i] === null)
569 node.removeAttribute('tabindex');
571 node.setAttribute('tabindex', tabIndexes[i]);
574 if (window.holdDevices) {
575 populateRemoteTargets(window.holdDevices);
576 delete window.holdDevices;
579 delete dialog.deactivatedNodes;
580 delete dialog.tabIndexes;
584 function openPortForwardingConfig() {
585 loadPortForwardingConfig(window.portForwardingConfig);
587 $('port-forwarding-overlay').classList.add('open');
588 document.addEventListener('keyup', handleKey);
590 var freshPort = document.querySelector('.fresh .port');
594 $('port-forwarding-config-done').focus();
596 setModal($('port-forwarding-overlay'));
599 function closePortForwardingConfig() {
600 if (!$('port-forwarding-overlay').classList.contains('open'))
603 $('port-forwarding-overlay').classList.remove('open');
604 document.removeEventListener('keyup', handleKey);
605 unsetModal($('port-forwarding-overlay'));
608 function loadPortForwardingConfig(config) {
609 var list = $('port-forwarding-config-list');
610 list.textContent = '';
611 for (var port in config)
612 list.appendChild(createConfigLine(port, config[port]));
613 list.appendChild(createEmptyConfigLine());
616 function commitPortForwardingConfig(closeConfig) {
618 closePortForwardingConfig();
620 commitFreshLineIfValid();
621 var lines = document.querySelectorAll('.port-forwarding-pair');
623 for (var i = 0; i != lines.length; i++) {
625 var portInput = line.querySelector('.port');
626 var locationInput = line.querySelector('.location');
628 var port = portInput.classList.contains('invalid') ?
629 portInput.lastValidValue :
632 var location = locationInput.classList.contains('invalid') ?
633 locationInput.lastValidValue :
636 if (port && location)
637 config[port] = location;
639 sendCommand('set-port-forwarding-config', config);
642 function updateDiscoverUsbDevicesEnabled(enabled) {
643 var checkbox = $('discover-usb-devices-enable');
644 checkbox.checked = !!enabled;
645 checkbox.disabled = false;
648 function updatePortForwardingEnabled(enabled) {
649 var checkbox = $('port-forwarding-enable');
650 checkbox.checked = !!enabled;
651 checkbox.disabled = false;
654 function updatePortForwardingConfig(config) {
655 window.portForwardingConfig = config;
656 $('port-forwarding-config-open').disabled = !config;
659 function createConfigLine(port, location) {
660 var line = document.createElement('div');
661 line.className = 'port-forwarding-pair';
663 var portInput = createConfigField(port, 'port', 'Port', validatePort);
664 line.appendChild(portInput);
666 var locationInput = createConfigField(
667 location, 'location', 'IP address and port', validateLocation);
668 line.appendChild(locationInput);
669 locationInput.addEventListener('keydown', function(e) {
670 if (e.keyIdentifier == 'U+0009' && // Tab
671 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
672 line.classList.contains('fresh') &&
673 !line.classList.contains('empty')) {
674 // Tabbing forward on the fresh line, try create a new empty one.
675 if (commitFreshLineIfValid(true))
680 var lineDelete = document.createElement('div');
681 lineDelete.className = 'close-button';
682 lineDelete.addEventListener('click', function() {
683 var newSelection = line.nextElementSibling;
684 line.parentNode.removeChild(line);
685 selectLine(newSelection);
687 line.appendChild(lineDelete);
689 line.addEventListener('click', selectLine.bind(null, line));
690 line.addEventListener('focus', selectLine.bind(null, line));
692 checkEmptyLine(line);
697 function validatePort(input) {
698 var match = input.value.match(/^(\d+)$/);
701 var port = parseInt(match[1]);
702 if (port < 1024 || 65535 < port)
705 var inputs = document.querySelectorAll('input.port:not(.invalid)');
706 for (var i = 0; i != inputs.length; ++i) {
707 if (inputs[i] == input)
709 if (parseInt(inputs[i].value) == port)
715 function validateLocation(input) {
716 var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
719 var port = parseInt(match[2]);
720 return port <= 65535;
723 function createEmptyConfigLine() {
724 var line = createConfigLine('', '');
725 line.classList.add('fresh');
729 function createConfigField(value, className, hint, validate) {
730 var input = document.createElement('input');
731 input.className = className;
733 input.placeholder = hint;
735 input.lastValidValue = value;
737 function checkInput() {
739 input.classList.remove('invalid');
741 input.classList.add('invalid');
742 if (input.parentNode)
743 checkEmptyLine(input.parentNode);
747 input.addEventListener('keyup', checkInput);
748 input.addEventListener('focus', function() {
749 selectLine(input.parentNode);
752 input.addEventListener('blur', function() {
754 input.lastValidValue = input.value;
760 function checkEmptyLine(line) {
761 var inputs = line.querySelectorAll('input');
763 for (var i = 0; i != inputs.length; i++) {
764 if (inputs[i].value != '')
768 line.classList.add('empty');
770 line.classList.remove('empty');
773 function selectLine(line) {
774 if (line.classList.contains('selected'))
777 line.classList.add('selected');
780 function unselectLine() {
781 var line = document.querySelector('.port-forwarding-pair.selected');
784 line.classList.remove('selected');
785 commitFreshLineIfValid();
788 function commitFreshLineIfValid(opt_selectNew) {
789 var line = document.querySelector('.port-forwarding-pair.fresh');
790 if (line.querySelector('.invalid'))
792 line.classList.remove('fresh');
793 var freshLine = createEmptyConfigLine();
794 line.parentNode.appendChild(freshLine);
796 freshLine.querySelector('.port').focus();
800 function populatePortStatus(devicesStatusMap) {
801 for (var deviceId in devicesStatusMap) {
802 if (!devicesStatusMap.hasOwnProperty(deviceId))
804 var deviceStatusMap = devicesStatusMap[deviceId];
806 var deviceSection = $(deviceId);
810 var devicePorts = deviceSection.querySelector('.device-ports');
811 devicePorts.textContent = '';
812 for (var port in deviceStatusMap) {
813 if (!deviceStatusMap.hasOwnProperty(port))
816 var status = deviceStatusMap[port];
817 var portIcon = document.createElement('div');
818 portIcon.className = 'port-icon';
819 // status === 0 is the default (connected) state.
820 // Positive values correspond to the tunnelling connection count
821 // (in DEBUG_DEVTOOLS mode).
823 portIcon.classList.add('connected');
824 else if (status === -1 || status === -2)
825 portIcon.classList.add('transient');
827 portIcon.classList.add('error');
828 devicePorts.appendChild(portIcon);
830 var portNumber = document.createElement('div');
831 portNumber.className = 'port-number';
832 portNumber.textContent = ':' + port;
834 portNumber.textContent += '(' + status + ')';
835 devicePorts.appendChild(portNumber);
839 function clearPorts(deviceSection) {
840 if (deviceSection.id in devicesStatusMap)
842 deviceSection.querySelector('.device-ports').textContent = '';
845 Array.prototype.forEach.call(
846 document.querySelectorAll('.device'), clearPorts);
849 document.addEventListener('DOMContentLoaded', onload);
851 window.addEventListener('hashchange', onHashChange);