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 var tabContents = document.querySelectorAll('#content > div');
54 var tabHeaders = $('navigation').querySelectorAll('.tab-header');
56 for (var i = 0; i != tabContents.length; i++) {
57 var tabContent = tabContents[i];
58 var tabHeader = tabHeaders[i];
59 if (tabContent.id == id) {
60 tabContent.classList.add('selected');
61 tabHeader.classList.add('selected');
64 tabContent.classList.remove('selected');
65 tabHeader.classList.remove('selected');
70 window.location.hash = id;
74 function populateTargets(source, data) {
75 if (source == 'renderers')
76 populateWebContentsTargets(data);
77 else if (source == 'workers')
78 populateWorkerTargets(data);
79 else if (source == 'adb')
80 populateRemoteTargets(data);
82 console.error('Unknown source type: ' + source);
85 function populateWebContentsTargets(data) {
86 removeChildren('pages-list');
87 removeChildren('extensions-list');
88 removeChildren('apps-list');
89 removeChildren('others-list');
91 for (var i = 0; i < data.length; i++) {
92 if (data[i].type === 'page')
93 addToPagesList(data[i]);
94 else if (data[i].type === 'background_page')
95 addToExtensionsList(data[i]);
96 else if (data[i].type === 'app')
97 addToAppsList(data[i]);
99 addToOthersList(data[i]);
103 function populateWorkerTargets(data) {
104 removeChildren('workers-list');
106 for (var i = 0; i < data.length; i++)
107 addToWorkersList(data[i]);
110 function populateRemoteTargets(devices) {
115 window.holdDevices = devices;
119 function alreadyDisplayed(element, data) {
120 var json = JSON.stringify(data);
121 if (element.cachedJSON == json)
123 element.cachedJSON = json;
127 function insertChildSortedById(parent, child) {
128 for (var sibling = parent.firstElementChild;
130 sibling = sibling.nextElementSibling) {
131 if (sibling.id > child.id) {
132 parent.insertBefore(child, sibling);
136 parent.appendChild(child);
139 var deviceList = $('devices-list');
140 if (alreadyDisplayed(deviceList, devices))
143 function removeObsolete(validIds, section) {
144 if (validIds.indexOf(section.id) < 0)
148 var newDeviceIds = devices.map(function(d) { return d.id });
149 Array.prototype.forEach.call(
150 deviceList.querySelectorAll('.device'),
151 removeObsolete.bind(null, newDeviceIds));
153 $('devices-help').hidden = !!devices.length;
155 for (var d = 0; d < devices.length; d++) {
156 var device = devices[d];
158 var deviceSection = $(device.id);
159 if (!deviceSection) {
160 deviceSection = document.createElement('div');
161 deviceSection.id = device.id;
162 deviceSection.className = 'device';
163 deviceList.appendChild(deviceSection);
165 var deviceHeader = document.createElement('div');
166 deviceHeader.className = 'device-header';
167 deviceSection.appendChild(deviceHeader);
169 var deviceName = document.createElement('div');
170 deviceName.className = 'device-name';
171 deviceHeader.appendChild(deviceName);
173 var deviceSerial = document.createElement('div');
174 deviceSerial.className = 'device-serial';
175 deviceSerial.textContent = '#' + device.adbSerial.toUpperCase();
176 deviceHeader.appendChild(deviceSerial);
178 var devicePorts = document.createElement('div');
179 devicePorts.className = 'device-ports';
180 deviceHeader.appendChild(devicePorts);
182 var browserList = document.createElement('div');
183 browserList.className = 'browsers';
184 deviceSection.appendChild(browserList);
186 var authenticating = document.createElement('div');
187 authenticating.className = 'device-auth';
188 deviceSection.appendChild(authenticating);
191 if (alreadyDisplayed(deviceSection, device))
194 deviceSection.querySelector('.device-name').textContent = device.adbModel;
195 deviceSection.querySelector('.device-auth').textContent =
196 device.adbConnected ? '' : 'Pending authentication: please accept ' +
197 'debugging session on the device.';
199 var devicePorts = deviceSection.querySelector('.device-ports');
200 devicePorts.textContent = '';
201 if (device.adbPortStatus) {
202 for (var port in device.adbPortStatus) {
203 var status = device.adbPortStatus[port];
204 var portIcon = document.createElement('div');
205 portIcon.className = 'port-icon';
207 portIcon.classList.add('connected');
208 else if (status == -1 || status == -2)
209 portIcon.classList.add('transient');
211 portIcon.classList.add('error');
212 devicePorts.appendChild(portIcon);
214 var portNumber = document.createElement('div');
215 portNumber.className = 'port-number';
216 portNumber.textContent = ':' + port;
218 portNumber.textContent += '(' + status + ')';
219 devicePorts.appendChild(portNumber);
223 var browserList = deviceSection.querySelector('.browsers');
225 device.browsers.map(function(b) { return b.id });
226 Array.prototype.forEach.call(
227 browserList.querySelectorAll('.browser'),
228 removeObsolete.bind(null, newBrowserIds));
230 for (var b = 0; b < device.browsers.length; b++) {
231 var browser = device.browsers[b];
233 var majorChromeVersion = browser.adbBrowserChromeVersion;
235 var incompatibleVersion = browser.hasOwnProperty('compatibleVersion') &&
236 !browser.compatibleVersion;
238 var browserSection = $(browser.id);
239 if (browserSection) {
240 pageList = browserSection.querySelector('.pages');
242 browserSection = document.createElement('div');
243 browserSection.id = browser.id;
244 browserSection.className = 'browser';
245 insertChildSortedById(browserList, browserSection);
247 var browserHeader = document.createElement('div');
248 browserHeader.className = 'browser-header';
250 var browserName = document.createElement('div');
251 browserName.className = 'browser-name';
252 browserHeader.appendChild(browserName);
253 browserName.textContent = browser.adbBrowserName;
254 if (browser.adbBrowserVersion)
255 browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
256 browserSection.appendChild(browserHeader);
258 if (incompatibleVersion) {
259 var warningSection = document.createElement('div');
260 warningSection.className = 'warning';
261 warningSection.textContent =
262 'You may need a newer version of desktop Chrome. ' +
263 'Please try Chrome ' + browser.adbBrowserVersion + ' or later.';
264 browserHeader.appendChild(warningSection);
265 } else if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
266 var newPage = document.createElement('div');
267 newPage.className = 'open';
269 var newPageUrl = document.createElement('input');
270 newPageUrl.type = 'text';
271 newPageUrl.placeholder = 'Open tab with url';
272 newPage.appendChild(newPageUrl);
274 var openHandler = function(sourceId, browserId, input) {
276 'open', sourceId, browserId, input.value || 'about:blank');
278 }.bind(null, browser.source, browser.id, newPageUrl);
279 newPageUrl.addEventListener('keyup', function(handler, event) {
280 if (event.keyIdentifier == 'Enter' && event.target.value)
282 }.bind(null, openHandler), true);
284 var newPageButton = document.createElement('button');
285 newPageButton.textContent = 'Open';
286 newPage.appendChild(newPageButton);
287 newPageButton.addEventListener('click', openHandler, true);
289 browserHeader.appendChild(newPage);
292 pageList = document.createElement('div');
293 pageList.className = 'list pages';
294 browserSection.appendChild(pageList);
297 if (incompatibleVersion || alreadyDisplayed(browserSection, browser))
300 pageList.textContent = '';
301 for (var p = 0; p < browser.pages.length; p++) {
302 var page = browser.pages[p];
303 // Attached targets have no unique id until Chrome 26. For such targets
304 // it is impossible to activate existing DevTools window.
305 page.hasNoUniqueId = page.attached &&
306 (majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID);
307 var row = addTargetToList(page, pageList, ['name', 'url']);
308 if (page['description'])
309 addWebViewDetails(row, page);
311 addFavicon(row, page);
312 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
313 addActionLink(row, 'focus tab',
314 sendTargetCommand.bind(null, 'activate', page), false);
316 if (majorChromeVersion) {
317 addActionLink(row, 'reload',
318 sendTargetCommand.bind(null, 'reload', page), page.attached);
320 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
321 addActionLink(row, 'close',
322 sendTargetCommand.bind(null, 'close', page), page.attached);
329 function addToPagesList(data) {
330 var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
331 addFavicon(row, data);
333 addGuestViews(row, data.guests);
336 function addToExtensionsList(data) {
337 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
338 addFavicon(row, data);
340 addGuestViews(row, data.guests);
343 function addToAppsList(data) {
344 var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
345 addFavicon(row, data);
347 addGuestViews(row, data.guests);
350 function addGuestViews(row, guests) {
351 Array.prototype.forEach.call(guests, function(guest) {
352 var guestRow = addTargetToList(guest, row, ['name', 'url']);
353 guestRow.classList.add('guest');
354 addFavicon(guestRow, guest);
358 function addToWorkersList(data) {
360 addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
361 addActionLink(row, 'terminate',
362 sendTargetCommand.bind(null, 'close', data), data.attached);
365 function addToOthersList(data) {
366 addTargetToList(data, $('others-list'), ['url']);
369 function formatValue(data, property) {
370 var value = data[property];
372 if (property == 'name' && value == '') {
376 var text = value ? String(value) : '';
377 if (text.length > 100)
378 text = text.substring(0, 100) + '\u2026';
380 var div = document.createElement('div');
381 div.textContent = text;
382 div.className = property;
386 function addFavicon(row, data) {
387 var favicon = document.createElement('img');
388 if (data['faviconUrl'])
389 favicon.src = data['faviconUrl'];
390 var propertiesBox = row.querySelector('.properties-box');
391 propertiesBox.insertBefore(favicon, propertiesBox.firstChild);
394 function addWebViewDetails(row, data) {
397 webview = JSON.parse(data['description']);
401 addWebViewDescription(row, webview);
402 if (data.adbScreenWidth && data.adbScreenHeight)
404 row, webview, data.adbScreenWidth, data.adbScreenHeight);
407 function addWebViewDescription(row, webview) {
408 var viewStatus = { visibility: '', position: '', size: '' };
409 if (!webview.empty) {
410 if (webview.attached && !webview.visible)
411 viewStatus.visibility = 'hidden';
412 else if (!webview.attached)
413 viewStatus.visibility = 'detached';
414 viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
416 viewStatus.visibility = 'empty';
418 if (webview.attached) {
419 viewStatus.position =
420 'at (' + webview.screenX + ', ' + webview.screenY + ')';
423 var subRow = document.createElement('div');
424 subRow.className = 'subrow webview';
425 if (webview.empty || !webview.attached || !webview.visible)
426 subRow.className += ' invisible-view';
427 if (viewStatus.visibility)
428 subRow.appendChild(formatValue(viewStatus, 'visibility'));
429 if (viewStatus.position)
430 subRow.appendChild(formatValue(viewStatus, 'position'));
431 subRow.appendChild(formatValue(viewStatus, 'size'));
432 var subrowBox = row.querySelector('.subrow-box');
433 subrowBox.insertBefore(subRow, row.querySelector('.actions'));
436 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
437 var maxScreenRectSize = 50;
439 var screenRectHeight;
441 var aspectRatio = screenWidth / screenHeight;
442 if (aspectRatio < 1) {
443 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
444 screenRectHeight = maxScreenRectSize;
446 screenRectWidth = maxScreenRectSize;
447 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
450 var thumbnail = document.createElement('div');
451 thumbnail.className = 'webview-thumbnail';
452 var thumbnailWidth = 3 * screenRectWidth;
453 var thumbnailHeight = 60;
454 thumbnail.style.width = thumbnailWidth + 'px';
455 thumbnail.style.height = thumbnailHeight + 'px';
457 var screenRect = document.createElement('div');
458 screenRect.className = 'screen-rect';
459 screenRect.style.left = screenRectWidth + 'px';
460 screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
461 screenRect.style.width = screenRectWidth + 'px';
462 screenRect.style.height = screenRectHeight + 'px';
463 thumbnail.appendChild(screenRect);
465 if (!webview.empty && webview.attached) {
466 var viewRect = document.createElement('div');
467 viewRect.className = 'view-rect';
468 if (!webview.visible)
469 viewRect.classList.add('hidden');
470 function percent(ratio) {
471 return ratio * 100 + '%';
473 viewRect.style.left = percent(webview.screenX / screenWidth);
474 viewRect.style.top = percent(webview.screenY / screenHeight);
475 viewRect.style.width = percent(webview.width / screenWidth);
476 viewRect.style.height = percent(webview.height / screenHeight);
477 screenRect.appendChild(viewRect);
480 var propertiesBox = row.querySelector('.properties-box');
481 propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild);
484 function addTargetToList(data, list, properties) {
485 var row = document.createElement('div');
486 row.className = 'row';
488 var propertiesBox = document.createElement('div');
489 propertiesBox.className = 'properties-box';
490 row.appendChild(propertiesBox);
492 var subrowBox = document.createElement('div');
493 subrowBox.className = 'subrow-box';
494 propertiesBox.appendChild(subrowBox);
496 var subrow = document.createElement('div');
497 subrow.className = 'subrow';
498 subrowBox.appendChild(subrow);
500 for (var j = 0; j < properties.length; j++)
501 subrow.appendChild(formatValue(data, properties[j]));
503 var actionBox = document.createElement('div');
504 actionBox.className = 'actions';
505 subrowBox.appendChild(actionBox);
507 addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data),
508 data.hasNoUniqueId || data.adbAttachedForeign);
510 list.appendChild(row);
514 function addActionLink(row, text, handler, opt_disabled) {
515 var link = document.createElement('span');
516 link.classList.add('action');
518 link.classList.add('disabled');
520 link.classList.remove('disabled');
522 link.textContent = text;
523 link.addEventListener('click', handler, true);
524 row.querySelector('.actions').appendChild(link);
528 function initSettings() {
529 $('discover-usb-devices-enable').addEventListener('change',
530 enableDiscoverUsbDevices);
532 $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
533 $('port-forwarding-config-open').addEventListener(
534 'click', openPortForwardingConfig);
535 $('port-forwarding-config-close').addEventListener(
536 'click', closePortForwardingConfig);
537 $('port-forwarding-config-done').addEventListener(
538 'click', commitPortForwardingConfig.bind(true));
541 function enableDiscoverUsbDevices(event) {
542 sendCommand('set-discover-usb-devices-enabled', event.target.checked);
545 function enablePortForwarding(event) {
546 sendCommand('set-port-forwarding-enabled', event.target.checked);
549 function handleKey(event) {
550 switch (event.keyCode) {
552 if (event.target.nodeName == 'INPUT') {
553 var line = event.target.parentNode;
554 if (!line.classList.contains('fresh') ||
555 line.classList.contains('empty')) {
556 commitPortForwardingConfig(true);
558 commitFreshLineIfValid(true /* select new line */);
559 commitPortForwardingConfig(false);
562 commitPortForwardingConfig(true);
567 commitPortForwardingConfig(true);
572 function setModal(dialog) {
573 dialog.deactivatedNodes = Array.prototype.filter.call(
574 document.querySelectorAll('*'),
576 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
579 dialog.tabIndexes = dialog.deactivatedNodes.map(
580 function(n) { return n.getAttribute('tabindex'); });
582 dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; });
583 window.modal = dialog;
586 function unsetModal(dialog) {
587 for (var i = 0; i < dialog.deactivatedNodes.length; i++) {
588 var node = dialog.deactivatedNodes[i];
589 if (dialog.tabIndexes[i] === null)
590 node.removeAttribute('tabindex');
592 node.setAttribute('tabindex', tabIndexes[i]);
595 if (window.holdDevices) {
596 populateRemoteTargets(window.holdDevices);
597 delete window.holdDevices;
600 delete dialog.deactivatedNodes;
601 delete dialog.tabIndexes;
605 function openPortForwardingConfig() {
606 loadPortForwardingConfig(window.portForwardingConfig);
608 $('port-forwarding-overlay').classList.add('open');
609 document.addEventListener('keyup', handleKey);
611 var freshPort = document.querySelector('.fresh .port');
615 $('port-forwarding-config-done').focus();
617 setModal($('port-forwarding-overlay'));
620 function closePortForwardingConfig() {
621 $('port-forwarding-overlay').classList.remove('open');
622 document.removeEventListener('keyup', handleKey);
623 unsetModal($('port-forwarding-overlay'));
626 function loadPortForwardingConfig(config) {
627 var list = $('port-forwarding-config-list');
628 list.textContent = '';
629 for (var port in config)
630 list.appendChild(createConfigLine(port, config[port]));
631 list.appendChild(createEmptyConfigLine());
634 function commitPortForwardingConfig(closeConfig) {
636 closePortForwardingConfig();
638 commitFreshLineIfValid();
639 var lines = document.querySelectorAll('.port-forwarding-pair');
641 for (var i = 0; i != lines.length; i++) {
643 var portInput = line.querySelector('.port');
644 var locationInput = line.querySelector('.location');
646 var port = portInput.classList.contains('invalid') ?
647 portInput.lastValidValue :
650 var location = locationInput.classList.contains('invalid') ?
651 locationInput.lastValidValue :
654 if (port && location)
655 config[port] = location;
657 sendCommand('set-port-forwarding-config', config);
660 function updateDiscoverUsbDevicesEnabled(enabled) {
661 var checkbox = $('discover-usb-devices-enable');
662 checkbox.checked = !!enabled;
663 checkbox.disabled = false;
666 function updatePortForwardingEnabled(enabled) {
667 var checkbox = $('port-forwarding-enable');
668 checkbox.checked = !!enabled;
669 checkbox.disabled = false;
672 function updatePortForwardingConfig(config) {
673 window.portForwardingConfig = config;
674 $('port-forwarding-config-open').disabled = !config;
677 function createConfigLine(port, location) {
678 var line = document.createElement('div');
679 line.className = 'port-forwarding-pair';
681 var portInput = createConfigField(port, 'port', 'Port', validatePort);
682 line.appendChild(portInput);
684 var locationInput = createConfigField(
685 location, 'location', 'IP address and port', validateLocation);
686 line.appendChild(locationInput);
687 locationInput.addEventListener('keydown', function(e) {
688 if (e.keyIdentifier == 'U+0009' && // Tab
689 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
690 line.classList.contains('fresh') &&
691 !line.classList.contains('empty')) {
692 // Tabbing forward on the fresh line, try create a new empty one.
693 if (commitFreshLineIfValid(true))
698 var lineDelete = document.createElement('div');
699 lineDelete.className = 'close-button';
700 lineDelete.addEventListener('click', function() {
701 var newSelection = line.nextElementSibling;
702 line.parentNode.removeChild(line);
703 selectLine(newSelection);
705 line.appendChild(lineDelete);
707 line.addEventListener('click', selectLine.bind(null, line));
708 line.addEventListener('focus', selectLine.bind(null, line));
710 checkEmptyLine(line);
715 function validatePort(input) {
716 var match = input.value.match(/^(\d+)$/);
719 var port = parseInt(match[1]);
720 if (port < 1024 || 65535 < port)
723 var inputs = document.querySelectorAll('input.port:not(.invalid)');
724 for (var i = 0; i != inputs.length; ++i) {
725 if (inputs[i] == input)
727 if (parseInt(inputs[i].value) == port)
733 function validateLocation(input) {
734 var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
737 var port = parseInt(match[2]);
738 return port <= 65535;
741 function createEmptyConfigLine() {
742 var line = createConfigLine('', '');
743 line.classList.add('fresh');
747 function createConfigField(value, className, hint, validate) {
748 var input = document.createElement('input');
749 input.className = className;
751 input.placeholder = hint;
753 input.lastValidValue = value;
755 function checkInput() {
757 input.classList.remove('invalid');
759 input.classList.add('invalid');
760 if (input.parentNode)
761 checkEmptyLine(input.parentNode);
765 input.addEventListener('keyup', checkInput);
766 input.addEventListener('focus', function() {
767 selectLine(input.parentNode);
770 input.addEventListener('blur', function() {
772 input.lastValidValue = input.value;
778 function checkEmptyLine(line) {
779 var inputs = line.querySelectorAll('input');
781 for (var i = 0; i != inputs.length; i++) {
782 if (inputs[i].value != '')
786 line.classList.add('empty');
788 line.classList.remove('empty');
791 function selectLine(line) {
792 if (line.classList.contains('selected'))
795 line.classList.add('selected');
798 function unselectLine() {
799 var line = document.querySelector('.port-forwarding-pair.selected');
802 line.classList.remove('selected');
803 commitFreshLineIfValid();
806 function commitFreshLineIfValid(opt_selectNew) {
807 var line = document.querySelector('.port-forwarding-pair.fresh');
808 if (line.querySelector('.invalid'))
810 line.classList.remove('fresh');
811 var freshLine = createEmptyConfigLine();
812 line.parentNode.appendChild(freshLine);
814 freshLine.querySelector('.port').focus();
818 document.addEventListener('DOMContentLoaded', onload);
820 window.addEventListener('hashchange', onHashChange);