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;
236 var browserSection = $(browser.id);
237 if (browserSection) {
238 pageList = browserSection.querySelector('.pages');
240 browserSection = document.createElement('div');
241 browserSection.id = browser.id;
242 browserSection.className = 'browser';
243 insertChildSortedById(browserList, browserSection);
245 var browserHeader = document.createElement('div');
246 browserHeader.className = 'browser-header';
248 var browserName = document.createElement('div');
249 browserName.className = 'browser-name';
250 browserHeader.appendChild(browserName);
251 browserName.textContent = browser.adbBrowserName;
252 if (browser.adbBrowserVersion)
253 browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
254 browserSection.appendChild(browserHeader);
256 if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
257 var newPage = document.createElement('div');
258 newPage.className = 'open';
260 var newPageUrl = document.createElement('input');
261 newPageUrl.type = 'text';
262 newPageUrl.placeholder = 'Open tab with url';
263 newPage.appendChild(newPageUrl);
265 var openHandler = function(sourceId, browserId, input) {
267 'open', sourceId, browserId, input.value || 'about:blank');
269 }.bind(null, browser.source, browser.id, newPageUrl);
270 newPageUrl.addEventListener('keyup', function(handler, event) {
271 if (event.keyIdentifier == 'Enter' && event.target.value)
273 }.bind(null, openHandler), true);
275 var newPageButton = document.createElement('button');
276 newPageButton.textContent = 'Open';
277 newPage.appendChild(newPageButton);
278 newPageButton.addEventListener('click', openHandler, true);
280 browserHeader.appendChild(newPage);
283 pageList = document.createElement('div');
284 pageList.className = 'list pages';
285 browserSection.appendChild(pageList);
288 if (alreadyDisplayed(browserSection, browser))
291 pageList.textContent = '';
292 for (var p = 0; p < browser.pages.length; p++) {
293 var page = browser.pages[p];
294 // Attached targets have no unique id until Chrome 26. For such targets
295 // it is impossible to activate existing DevTools window.
296 page.hasNoUniqueId = page.attached &&
297 (majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID);
298 var row = addTargetToList(page, pageList, ['name', 'url']);
299 if (page['description'])
300 addWebViewDetails(row, page);
302 addFavicon(row, page);
303 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
304 addActionLink(row, 'focus tab',
305 sendTargetCommand.bind(null, 'activate', page), false);
307 if (majorChromeVersion) {
308 addActionLink(row, 'reload',
309 sendTargetCommand.bind(null, 'reload', page), page.attached);
311 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
312 addActionLink(row, 'close',
313 sendTargetCommand.bind(null, 'close', page), page.attached);
320 function addToPagesList(data) {
321 var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
322 addFavicon(row, data);
324 addGuestViews(row, data.guests);
327 function addToExtensionsList(data) {
328 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
329 addFavicon(row, data);
331 addGuestViews(row, data.guests);
334 function addToAppsList(data) {
335 var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
336 addFavicon(row, data);
338 addGuestViews(row, data.guests);
341 function addGuestViews(row, guests) {
342 Array.prototype.forEach.call(guests, function(guest) {
343 var guestRow = addTargetToList(guest, row, ['name', 'url']);
344 guestRow.classList.add('guest');
345 addFavicon(guestRow, guest);
349 function addToWorkersList(data) {
351 addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
352 addActionLink(row, 'terminate',
353 sendTargetCommand.bind(null, 'close', data), data.attached);
356 function addToOthersList(data) {
357 addTargetToList(data, $('others-list'), ['url']);
360 function formatValue(data, property) {
361 var value = data[property];
363 if (property == 'name' && value == '') {
367 var text = value ? String(value) : '';
368 if (text.length > 100)
369 text = text.substring(0, 100) + '\u2026';
371 var span = document.createElement('div');
372 span.textContent = text;
373 span.className = property;
377 function addFavicon(row, data) {
378 var favicon = document.createElement('img');
379 if (data['faviconUrl'])
380 favicon.src = data['faviconUrl'];
381 row.insertBefore(favicon, row.firstChild);
384 function addWebViewDetails(row, data) {
387 webview = JSON.parse(data['description']);
391 addWebViewDescription(row, webview);
392 if (data.adbScreenWidth && data.adbScreenHeight)
394 row, webview, data.adbScreenWidth, data.adbScreenHeight);
397 function addWebViewDescription(row, webview) {
398 var viewStatus = { visibility: '', position: '', size: '' };
399 if (!webview.empty) {
400 if (webview.attached && !webview.visible)
401 viewStatus.visibility = 'hidden';
402 else if (!webview.attached)
403 viewStatus.visibility = 'detached';
404 viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
406 viewStatus.visibility = 'empty';
408 if (webview.attached) {
409 viewStatus.position =
410 'at (' + webview.screenX + ', ' + webview.screenY + ')';
413 var subRow = document.createElement('div');
414 subRow.className = 'subrow webview';
415 if (webview.empty || !webview.attached || !webview.visible)
416 subRow.className += ' invisible-view';
417 if (viewStatus.visibility)
418 subRow.appendChild(formatValue(viewStatus, 'visibility'));
419 subRow.appendChild(formatValue(viewStatus, 'position'));
420 subRow.appendChild(formatValue(viewStatus, 'size'));
421 var mainSubrow = row.querySelector('.subrow.main');
422 if (mainSubrow.nextSibling)
423 mainSubrow.parentNode.insertBefore(subRow, mainSubrow.nextSibling);
425 mainSubrow.parentNode.appendChild(subRow);
428 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
429 var maxScreenRectSize = 50;
431 var screenRectHeight;
433 var aspectRatio = screenWidth / screenHeight;
434 if (aspectRatio < 1) {
435 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
436 screenRectHeight = maxScreenRectSize;
438 screenRectWidth = maxScreenRectSize;
439 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
442 var thumbnail = document.createElement('div');
443 thumbnail.className = 'webview-thumbnail';
444 var thumbnailWidth = 3 * screenRectWidth;
445 var thumbnailHeight = 60;
446 thumbnail.style.width = thumbnailWidth + 'px';
447 thumbnail.style.height = thumbnailHeight + 'px';
449 var screenRect = document.createElement('div');
450 screenRect.className = 'screen-rect';
451 screenRect.style.left = screenRectWidth + 'px';
452 screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
453 screenRect.style.width = screenRectWidth + 'px';
454 screenRect.style.height = screenRectHeight + 'px';
455 thumbnail.appendChild(screenRect);
457 if (!webview.empty && webview.attached) {
458 var viewRect = document.createElement('div');
459 viewRect.className = 'view-rect';
460 if (!webview.visible)
461 viewRect.classList.add('hidden');
462 function percent(ratio) {
463 return ratio * 100 + '%';
465 viewRect.style.left = percent(webview.screenX / screenWidth);
466 viewRect.style.top = percent(webview.screenY / screenHeight);
467 viewRect.style.width = percent(webview.width / screenWidth);
468 viewRect.style.height = percent(webview.height / screenHeight);
469 screenRect.appendChild(viewRect);
472 row.insertBefore(thumbnail, row.firstChild);
475 function addTargetToList(data, list, properties) {
476 var row = document.createElement('div');
477 row.className = 'row';
479 var subrowBox = document.createElement('div');
480 subrowBox.className = 'subrow-box';
481 row.appendChild(subrowBox);
483 var subrow = document.createElement('div');
484 subrow.className = 'subrow main';
485 subrowBox.appendChild(subrow);
487 var description = null;
488 for (var j = 0; j < properties.length; j++)
489 subrow.appendChild(formatValue(data, properties[j]));
492 addWebViewDescription(description, subrowBox);
494 var actionBox = document.createElement('div');
495 actionBox.className = 'actions';
496 subrowBox.appendChild(actionBox);
498 addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data),
499 data.hasNoUniqueId || data.adbAttachedForeign);
501 list.appendChild(row);
505 function addActionLink(row, text, handler, opt_disabled) {
506 var link = document.createElement('span');
507 link.classList.add('action');
509 link.classList.add('disabled');
511 link.classList.remove('disabled');
513 link.textContent = text;
514 link.addEventListener('click', handler, true);
515 row.querySelector('.actions').appendChild(link);
519 function initSettings() {
520 $('discover-usb-devices-enable').addEventListener('change',
521 enableDiscoverUsbDevices);
523 $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
524 $('port-forwarding-config-open').addEventListener(
525 'click', openPortForwardingConfig);
526 $('port-forwarding-config-close').addEventListener(
527 'click', closePortForwardingConfig);
528 $('port-forwarding-config-done').addEventListener(
529 'click', commitPortForwardingConfig.bind(true));
532 function enableDiscoverUsbDevices(event) {
533 sendCommand('set-discover-usb-devices-enabled', event.target.checked);
536 function enablePortForwarding(event) {
537 sendCommand('set-port-forwarding-enabled', event.target.checked);
540 function handleKey(event) {
541 switch (event.keyCode) {
543 if (event.target.nodeName == 'INPUT') {
544 var line = event.target.parentNode;
545 if (!line.classList.contains('fresh') ||
546 line.classList.contains('empty')) {
547 commitPortForwardingConfig(true);
549 commitFreshLineIfValid(true /* select new line */);
550 commitPortForwardingConfig(false);
553 commitPortForwardingConfig(true);
558 commitPortForwardingConfig(true);
563 function setModal(dialog) {
564 dialog.deactivatedNodes = Array.prototype.filter.call(
565 document.querySelectorAll('*'),
567 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0;
570 dialog.tabIndexes = dialog.deactivatedNodes.map(
571 function(n) { return n.getAttribute('tabindex'); });
573 dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; });
574 window.modal = dialog;
577 function unsetModal(dialog) {
578 for (var i = 0; i < dialog.deactivatedNodes.length; i++) {
579 var node = dialog.deactivatedNodes[i];
580 if (dialog.tabIndexes[i] === null)
581 node.removeAttribute('tabindex');
583 node.setAttribute('tabindex', tabIndexes[i]);
586 if (window.holdDevices) {
587 populateRemoteTargets(window.holdDevices);
588 delete window.holdDevices;
591 delete dialog.deactivatedNodes;
592 delete dialog.tabIndexes;
596 function openPortForwardingConfig() {
597 loadPortForwardingConfig(window.portForwardingConfig);
599 $('port-forwarding-overlay').classList.add('open');
600 document.addEventListener('keyup', handleKey);
602 var freshPort = document.querySelector('.fresh .port');
606 $('port-forwarding-config-done').focus();
608 setModal($('port-forwarding-overlay'));
611 function closePortForwardingConfig() {
612 $('port-forwarding-overlay').classList.remove('open');
613 document.removeEventListener('keyup', handleKey);
614 unsetModal($('port-forwarding-overlay'));
617 function loadPortForwardingConfig(config) {
618 var list = $('port-forwarding-config-list');
619 list.textContent = '';
620 for (var port in config)
621 list.appendChild(createConfigLine(port, config[port]));
622 list.appendChild(createEmptyConfigLine());
625 function commitPortForwardingConfig(closeConfig) {
627 closePortForwardingConfig();
629 commitFreshLineIfValid();
630 var lines = document.querySelectorAll('.port-forwarding-pair');
632 for (var i = 0; i != lines.length; i++) {
634 var portInput = line.querySelector('.port');
635 var locationInput = line.querySelector('.location');
637 var port = portInput.classList.contains('invalid') ?
638 portInput.lastValidValue :
641 var location = locationInput.classList.contains('invalid') ?
642 locationInput.lastValidValue :
645 if (port && location)
646 config[port] = location;
648 sendCommand('set-port-forwarding-config', config);
651 function updateDiscoverUsbDevicesEnabled(enabled) {
652 var checkbox = $('discover-usb-devices-enable');
653 checkbox.checked = !!enabled;
654 checkbox.disabled = false;
657 function updatePortForwardingEnabled(enabled) {
658 var checkbox = $('port-forwarding-enable');
659 checkbox.checked = !!enabled;
660 checkbox.disabled = false;
663 function updatePortForwardingConfig(config) {
664 window.portForwardingConfig = config;
665 $('port-forwarding-config-open').disabled = !config;
668 function createConfigLine(port, location) {
669 var line = document.createElement('div');
670 line.className = 'port-forwarding-pair';
672 var portInput = createConfigField(port, 'port', 'Port', validatePort);
673 line.appendChild(portInput);
675 var locationInput = createConfigField(
676 location, 'location', 'IP address and port', validateLocation);
677 line.appendChild(locationInput);
678 locationInput.addEventListener('keydown', function(e) {
679 if (e.keyIdentifier == 'U+0009' && // Tab
680 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
681 line.classList.contains('fresh') &&
682 !line.classList.contains('empty')) {
683 // Tabbing forward on the fresh line, try create a new empty one.
684 if (commitFreshLineIfValid(true))
689 var lineDelete = document.createElement('div');
690 lineDelete.className = 'close-button';
691 lineDelete.addEventListener('click', function() {
692 var newSelection = line.nextElementSibling;
693 line.parentNode.removeChild(line);
694 selectLine(newSelection);
696 line.appendChild(lineDelete);
698 line.addEventListener('click', selectLine.bind(null, line));
699 line.addEventListener('focus', selectLine.bind(null, line));
701 checkEmptyLine(line);
706 function validatePort(input) {
707 var match = input.value.match(/^(\d+)$/);
710 var port = parseInt(match[1]);
711 if (port < 1024 || 65535 < port)
714 var inputs = document.querySelectorAll('input.port:not(.invalid)');
715 for (var i = 0; i != inputs.length; ++i) {
716 if (inputs[i] == input)
718 if (parseInt(inputs[i].value) == port)
724 function validateLocation(input) {
725 var match = input.value.match(/^([a-zA-Z0-9\.]+):(\d+)$/);
728 var port = parseInt(match[2]);
729 return port <= 65535;
732 function createEmptyConfigLine() {
733 var line = createConfigLine('', '');
734 line.classList.add('fresh');
738 function createConfigField(value, className, hint, validate) {
739 var input = document.createElement('input');
740 input.className = className;
742 input.placeholder = hint;
744 input.lastValidValue = value;
746 function checkInput() {
748 input.classList.remove('invalid');
750 input.classList.add('invalid');
751 if (input.parentNode)
752 checkEmptyLine(input.parentNode);
756 input.addEventListener('keyup', checkInput);
757 input.addEventListener('focus', function() {
758 selectLine(input.parentNode);
761 input.addEventListener('blur', function() {
763 input.lastValidValue = input.value;
769 function checkEmptyLine(line) {
770 var inputs = line.querySelectorAll('input');
772 for (var i = 0; i != inputs.length; i++) {
773 if (inputs[i].value != '')
777 line.classList.add('empty');
779 line.classList.remove('empty');
782 function selectLine(line) {
783 if (line.classList.contains('selected'))
786 line.classList.add('selected');
789 function unselectLine() {
790 var line = document.querySelector('.port-forwarding-pair.selected');
793 line.classList.remove('selected');
794 commitFreshLineIfValid();
797 function commitFreshLineIfValid(opt_selectNew) {
798 var line = document.querySelector('.port-forwarding-pair.fresh');
799 if (line.querySelector('.invalid'))
801 line.classList.remove('fresh');
802 var freshLine = createEmptyConfigLine();
803 line.parentNode.appendChild(freshLine);
805 freshLine.querySelector('.port').focus();
809 document.addEventListener('DOMContentLoaded', onload);
811 window.addEventListener('hashchange', onHashChange);