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. 4 5 function inspect(data) { 6 chrome.send('inspect', [data]); 7 } 8 9 function terminate(data) { 10 chrome.send('terminate', [data]); 11 } 12 13 function reload(data) { 14 chrome.send('reload', [data]); 15 } 16 17 function open(browserId, url) { 18 chrome.send('open', [browserId, url]); 19 } 20 21 function removeChildren(element_id) { 22 var element = $(element_id); 23 element.textContent = ''; 24 } 25 26 function onload() { 27 var tabContents = document.querySelectorAll('#content > div'); 28 for (var i = 0; i != tabContents.length; i++) { 29 var tabContent = tabContents[i]; 30 var tabName = tabContent.querySelector('.content-header').textContent; 31 32 var tabHeader = document.createElement('div'); 33 tabHeader.className = 'tab-header'; 34 var button = document.createElement('button'); 35 button.textContent = tabName; 36 tabHeader.appendChild(button); 37 tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id)); 38 $('navigation').appendChild(tabHeader); 39 } 40 var selectedTabName = window.location.hash.slice(1) || 'devices'; 41 selectTab(selectedTabName + '-tab'); 42 initPortForwarding(); 43 chrome.send('init-ui'); 44 } 45 46 function selectTab(id) { 47 var tabContents = document.querySelectorAll('#content > div'); 48 var tabHeaders = $('navigation').querySelectorAll('.tab-header'); 49 for (var i = 0; i != tabContents.length; i++) { 50 var tabContent = tabContents[i]; 51 var tabHeader = tabHeaders[i]; 52 if (tabContent.id == id) { 53 tabContent.classList.add('selected'); 54 tabHeader.classList.add('selected'); 55 } else { 56 tabContent.classList.remove('selected'); 57 tabHeader.classList.remove('selected'); 58 } 59 } 60 } 61 62 function populateLists(data) { 63 removeChildren('pages'); 64 removeChildren('extensions'); 65 removeChildren('apps'); 66 removeChildren('others'); 67 68 for (var i = 0; i < data.length; i++) { 69 if (data[i].type === 'page') 70 addToPagesList(data[i]); 71 else if (data[i].type === 'extension') 72 addToExtensionsList(data[i]); 73 else if (data[i].type === 'app') 74 addToAppsList(data[i]); 75 else 76 addToOthersList(data[i]); 77 } 78 } 79 80 function populateWorkersList(data) { 81 removeChildren('workers'); 82 83 for (var i = 0; i < data.length; i++) 84 addToWorkersList(data[i]); 85 } 86 87 function populateDeviceLists(devices) { 88 if (!devices) 89 return; 90 91 function alreadyDisplayed(element, data) { 92 var json = JSON.stringify(data); 93 if (element.cachedJSON == json) 94 return true; 95 element.cachedJSON = json; 96 return false; 97 } 98 99 function insertChildSortedById(parent, child) { 100 for (var sibling = parent.firstElementChild; 101 sibling; 102 sibling = sibling.nextElementSibling) { 103 if (sibling.id > child.id) { 104 parent.insertBefore(child, sibling); 105 return; 106 } 107 } 108 parent.appendChild(child); 109 } 110 111 var deviceList = $('devices'); 112 if (alreadyDisplayed(deviceList, devices)) 113 return; 114 115 function removeObsolete(validIds, section) { 116 if (validIds.indexOf(section.id) < 0) 117 section.remove(); 118 } 119 120 var newDeviceIds = devices.map(function(d) { return d.adbGlobalId }); 121 Array.prototype.forEach.call( 122 deviceList.querySelectorAll('.device'), 123 removeObsolete.bind(null, newDeviceIds)); 124 125 for (var d = 0; d < devices.length; d++) { 126 var device = devices[d]; 127 128 var devicePorts; 129 var browserList; 130 var deviceSection = $(device.adbGlobalId); 131 if (deviceSection) { 132 devicePorts = deviceSection.querySelector('.device-ports'); 133 browserList = deviceSection.querySelector('.browsers'); 134 } else { 135 deviceSection = document.createElement('div'); 136 deviceSection.id = device.adbGlobalId; 137 deviceSection.className = 'device list'; 138 deviceList.appendChild(deviceSection); 139 140 var deviceHeader = document.createElement('div'); 141 deviceHeader.className = 'device-header'; 142 deviceSection.appendChild(deviceHeader); 143 144 var deviceName = document.createElement('div'); 145 deviceName.className = 'device-name'; 146 deviceName.textContent = device.adbModel; 147 deviceHeader.appendChild(deviceName); 148 149 if (device.adbSerial) { 150 var deviceSerial = document.createElement('div'); 151 deviceSerial.className = 'device-serial'; 152 deviceSerial.textContent = '#' + device.adbSerial.toUpperCase(); 153 deviceHeader.appendChild(deviceSerial); 154 } 155 156 devicePorts = document.createElement('div'); 157 devicePorts.className = 'device-ports'; 158 deviceHeader.appendChild(devicePorts); 159 160 browserList = document.createElement('div'); 161 browserList.className = 'browsers'; 162 deviceSection.appendChild(browserList); 163 } 164 165 if (alreadyDisplayed(deviceSection, device)) 166 continue; 167 168 devicePorts.textContent = ''; 169 if (device.adbPortStatus) { 170 for (var port in device.adbPortStatus) { 171 var status = device.adbPortStatus[port]; 172 var portIcon = document.createElement('div'); 173 portIcon.className = 'port-icon'; 174 if (status > 0) 175 portIcon.classList.add('connected'); 176 else if (status == -1 || status == -2) 177 portIcon.classList.add('transient'); 178 else if (status < 0) 179 portIcon.classList.add('error'); 180 devicePorts.appendChild(portIcon); 181 182 var portNumber = document.createElement('div'); 183 portNumber.className = 'port-number'; 184 portNumber.textContent = ':' + port; 185 if (status > 0) 186 portNumber.textContent += '(' + status + ')'; 187 devicePorts.appendChild(portNumber); 188 } 189 } 190 191 var newBrowserIds = 192 device.browsers.map(function(b) { return b.adbGlobalId }); 193 Array.prototype.forEach.call( 194 browserList.querySelectorAll('.browser'), 195 removeObsolete.bind(null, newBrowserIds)); 196 197 for (var b = 0; b < device.browsers.length; b++) { 198 var browser = device.browsers[b]; 199 200 var isChrome = browser.adbBrowserProduct && 201 browser.adbBrowserProduct.match(/^Chrome/); 202 203 var pageList; 204 var browserSection = $(browser.adbGlobalId); 205 if (browserSection) { 206 pageList = browserSection.querySelector('.pages'); 207 } else { 208 browserSection = document.createElement('div'); 209 browserSection.id = browser.adbGlobalId; 210 browserSection.className = 'browser'; 211 insertChildSortedById(browserList, browserSection); 212 213 var browserHeader = document.createElement('div'); 214 browserHeader.className = 'browser-header'; 215 browserHeader.textContent = browser.adbBrowserProduct; 216 var majorChromeVersion = 0; 217 if (browser.adbBrowserVersion) { 218 browserHeader.textContent += ' (' + browser.adbBrowserVersion + ')'; 219 if (isChrome) { 220 var match = browser.adbBrowserVersion.match(/^(\d+)/); 221 if (match) 222 majorChromeVersion = parseInt(match[1]); 223 } 224 } 225 browserSection.appendChild(browserHeader); 226 227 if (majorChromeVersion >= 29) { 228 var newPage = document.createElement('div'); 229 newPage.className = 'open'; 230 231 var newPageUrl = document.createElement('input'); 232 newPageUrl.type = 'text'; 233 newPageUrl.placeholder = 'Open tab with url'; 234 newPage.appendChild(newPageUrl); 235 236 var openHandler = function(browserId, input) { 237 open(browserId, input.value || 'about:blank'); 238 input.value = ''; 239 }.bind(null, browser.adbGlobalId, newPageUrl); 240 newPageUrl.addEventListener('keyup', function(handler, event) { 241 if (event.keyIdentifier == 'Enter' && event.target.value) 242 handler(); 243 }.bind(null, openHandler), true); 244 245 var newPageButton = document.createElement('button'); 246 newPageButton.textContent = 'Open'; 247 newPage.appendChild(newPageButton); 248 newPageButton.addEventListener('click', openHandler, true); 249 250 browserSection.appendChild(newPage); 251 } 252 253 pageList = document.createElement('div'); 254 pageList.className = 'list pages'; 255 browserSection.appendChild(pageList); 256 } 257 258 if (alreadyDisplayed(browserSection, browser)) 259 continue; 260 261 pageList.textContent = ''; 262 for (var p = 0; p < browser.pages.length; p++) { 263 var page = browser.pages[p]; 264 var row = addTargetToList( 265 page, pageList, ['faviconUrl', 'name', 'url']); 266 if (isChrome) { 267 row.appendChild(createActionLink( 268 'reload', reload.bind(null, page), page.attached)); 269 row.appendChild(createActionLink( 270 'close', terminate.bind(null, page), page.attached)); 271 } 272 } 273 } 274 } 275 } 276 277 function addToPagesList(data) { 278 addTargetToList(data, $('pages'), ['faviconUrl', 'name', 'url']); 279 } 280 281 function addToExtensionsList(data) { 282 addTargetToList(data, $('extensions'), ['name', 'url']); 283 } 284 285 function addToAppsList(data) { 286 addTargetToList(data, $('apps'), ['name', 'url']); 287 } 288 289 function addToWorkersList(data) { 290 var row = addTargetToList(data, $('workers'), ['name', 'url', 'pid']); 291 row.appendChild(createActionLink( 292 'terminate', terminate.bind(null, data), data.attached)); 293 } 294 295 function addToOthersList(data) { 296 addTargetToList(data, $('others'), ['url']); 297 } 298 299 function formatValue(data, property) { 300 var value = data[property]; 301 302 if (property == 'name' && value == '') { 303 value = 'untitled'; 304 } 305 306 if (property == 'faviconUrl') { 307 var faviconElement = document.createElement('img'); 308 if (value) 309 faviconElement.src = value; 310 return faviconElement; 311 } 312 313 var text = value ? String(value) : ''; 314 if (text.length > 100) 315 text = text.substring(0, 100) + '\u2026'; 316 317 if (property == 'pid') 318 text = 'Pid:' + text; 319 320 var span = document.createElement('span'); 321 span.textContent = ' ' + text + ' '; 322 span.className = property; 323 return span; 324 } 325 326 function addTargetToList(data, list, properties) { 327 var row = document.createElement('div'); 328 row.className = 'row'; 329 for (var j = 0; j < properties.length; j++) 330 row.appendChild(formatValue(data, properties[j])); 331 332 row.appendChild(createActionLink('inspect', inspect.bind(null, data))); 333 334 row.processId = data.processId; 335 row.routeId = data.routeId; 336 337 list.appendChild(row); 338 return row; 339 } 340 341 function createActionLink(text, handler, opt_disabled) { 342 var link = document.createElement('a'); 343 if (opt_disabled) 344 link.classList.add('disabled'); 345 else 346 link.classList.remove('disabled'); 347 348 link.setAttribute('href', '#'); 349 link.textContent = text; 350 link.addEventListener('click', handler, true); 351 return link; 352 } 353 354 355 function initPortForwarding() { 356 $('port-forwarding-enable').addEventListener('change', enablePortForwarding); 357 358 $('port-forwarding-config-open').addEventListener( 359 'click', openPortForwardingConfig); 360 $('port-forwarding-config-close').addEventListener( 361 'click', closePortForwardingConfig); 362 $('port-forwarding-config-done').addEventListener( 363 'click', commitPortForwardingConfig); 364 } 365 366 function enablePortForwarding(event) { 367 chrome.send('set-port-forwarding-enabled', [event.target.checked]); 368 } 369 370 function handleKey(event) { 371 switch (event.keyCode) { 372 case 13: // Enter 373 if (event.target.nodeName == 'INPUT') { 374 var line = event.target.parentNode; 375 if (!line.classList.contains('fresh') || 376 line.classList.contains('empty')) 377 commitPortForwardingConfig(); 378 else 379 commitFreshLineIfValid(true /* select new line */); 380 } else { 381 commitPortForwardingConfig(); 382 } 383 break; 384 385 case 27: 386 closePortForwardingConfig(); 387 break; 388 } 389 } 390 391 function openPortForwardingConfig() { 392 loadPortForwardingConfig(window.portForwardingConfig); 393 394 $('port-forwarding-overlay').classList.add('open'); 395 document.addEventListener('keyup', handleKey); 396 397 var freshPort = document.querySelector('.fresh .port'); 398 if (freshPort) 399 freshPort.focus(); 400 else 401 $('port-forwarding-config-done').focus(); 402 } 403 404 function closePortForwardingConfig() { 405 $('port-forwarding-overlay').classList.remove('open'); 406 document.removeEventListener('keyup', handleKey); 407 } 408 409 function loadPortForwardingConfig(config) { 410 var list = $('port-forwarding-config-list'); 411 list.textContent = ''; 412 for (var port in config) 413 list.appendChild(createConfigLine(port, config[port])); 414 list.appendChild(createEmptyConfigLine()); 415 } 416 417 function commitPortForwardingConfig() { 418 if (document.querySelector( 419 '.port-forwarding-pair:not(.fresh) input.invalid')) 420 return; 421 422 if (document.querySelector( 423 '.port-forwarding-pair.fresh:not(.empty) input.invalid')) 424 return; 425 426 closePortForwardingConfig(); 427 commitFreshLineIfValid(); 428 var lines = document.querySelectorAll('.port-forwarding-pair'); 429 var config = {}; 430 for (var i = 0; i != lines.length; i++) { 431 var line = lines[i]; 432 var portInput = line.querySelector('.port:not(.invalid)'); 433 var locationInput = line.querySelector('.location:not(.invalid)'); 434 if (portInput && locationInput) 435 config[portInput.value] = locationInput.value; 436 } 437 chrome.send('set-port-forwarding-config', [config]); 438 } 439 440 function updatePortForwardingEnabled(enabled) { 441 var checkbox = $('port-forwarding-enable'); 442 checkbox.checked = !!enabled; 443 checkbox.disabled = false; 444 } 445 446 function updatePortForwardingConfig(config) { 447 window.portForwardingConfig = config; 448 $('port-forwarding-config-open').disabled = !config; 449 } 450 451 function createConfigLine(port, location) { 452 var line = document.createElement('div'); 453 line.className = 'port-forwarding-pair'; 454 455 var portInput = createConfigField(port, 'port', 'Port', validatePort); 456 line.appendChild(portInput); 457 458 var locationInput = createConfigField( 459 location, 'location', 'IP address and port', validateLocation); 460 line.appendChild(locationInput); 461 locationInput.addEventListener('keydown', function(e) { 462 if (e.keyIdentifier == 'U+0009' && // Tab 463 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && 464 line.classList.contains('fresh') && 465 !line.classList.contains('empty')) { 466 // Tabbing forward on the fresh line, try create a new empty one. 467 commitFreshLineIfValid(true); 468 e.preventDefault(); 469 } 470 }); 471 472 var lineDelete = document.createElement('div'); 473 lineDelete.className = 'close-button'; 474 lineDelete.addEventListener('click', function() { 475 var newSelection = line.nextElementSibling; 476 line.parentNode.removeChild(line); 477 selectLine(newSelection); 478 }); 479 line.appendChild(lineDelete); 480 481 line.addEventListener('click', selectLine.bind(null, line)); 482 line.addEventListener('focus', selectLine.bind(null, line)); 483 484 checkEmptyLine(line); 485 486 return line; 487 } 488 489 function validatePort(input) { 490 var match = input.value.match(/^(\d+)$/); 491 if (!match) 492 return false; 493 var port = parseInt(match[1]); 494 if (port < 5000 || 10000 < port) 495 return false; 496 497 var inputs = document.querySelectorAll('input.port:not(.invalid)'); 498 for (var i = 0; i != inputs.length; ++i) { 499 if (inputs[i] == input) 500 break; 501 if (parseInt(inputs[i].value) == port) 502 return false; 503 } 504 return true; 505 } 506 507 function validateLocation(input) { 508 var match = input.value.match(/^([a-zA-Z0-9\.]+):(\d+)$/); 509 if (!match) 510 return false; 511 var port = parseInt(match[2]); 512 return port <= 10000; 513 } 514 515 function createEmptyConfigLine() { 516 var line = createConfigLine('', ''); 517 line.classList.add('fresh'); 518 return line; 519 } 520 521 function createConfigField(value, className, hint, validate) { 522 var input = document.createElement('input'); 523 input.className = className; 524 input.type = 'text'; 525 input.placeholder = hint; 526 input.value = value; 527 528 function checkInput() { 529 if (validate(input)) 530 input.classList.remove('invalid'); 531 else 532 input.classList.add('invalid'); 533 if (input.parentNode) 534 checkEmptyLine(input.parentNode); 535 } 536 checkInput(); 537 538 input.addEventListener('keyup', checkInput); 539 input.addEventListener('focus', function() { 540 selectLine(input.parentNode); 541 }); 542 543 return input; 544 } 545 546 function checkEmptyLine(line) { 547 var inputs = line.querySelectorAll('input'); 548 var empty = true; 549 for (var i = 0; i != inputs.length; i++) { 550 if (inputs[i].value != '') 551 empty = false; 552 } 553 if (empty) 554 line.classList.add('empty'); 555 else 556 line.classList.remove('empty'); 557 } 558 559 function selectLine(line) { 560 if (line.classList.contains('selected')) 561 return; 562 unselectLine(); 563 line.classList.add('selected'); 564 } 565 566 function unselectLine() { 567 var line = document.querySelector('.port-forwarding-pair.selected'); 568 if (!line) 569 return; 570 line.classList.remove('selected'); 571 commitFreshLineIfValid(); 572 } 573 574 function commitFreshLineIfValid(opt_selectNew) { 575 var line = document.querySelector('.port-forwarding-pair.fresh'); 576 if (line.querySelector('.invalid')) 577 return; 578 line.classList.remove('fresh'); 579 var freshLine = createEmptyConfigLine(); 580 line.parentNode.appendChild(freshLine); 581 if (opt_selectNew) 582 freshLine.querySelector('.port').focus(); 583 } 584 585 document.addEventListener('DOMContentLoaded', onload); 586