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 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 10 var queryParamsObject = {}; 11 12 (function() { 13 var queryParams = window.location.search; 14 if (!queryParams) 15 return; 16 var params = queryParams.substring(1).split('&'); 17 for (var i = 0; i < params.length; ++i) { 18 var pair = params[i].split('='); 19 queryParamsObject[pair[0]] = pair[1]; 20 } 21 22 })(); 23 24 function sendCommand(command, args) { 25 chrome.send(command, Array.prototype.slice.call(arguments, 1)); 26 } 27 28 function sendTargetCommand(command, target) { 29 sendCommand(command, target.source, target.id); 30 } 31 32 function sendServiceWorkerCommand(action, worker) { 33 $('serviceworker-internals').contentWindow.postMessage({ 34 'action': action, 35 'worker': worker 36 },'chrome://serviceworker-internals'); 37 } 38 39 function removeChildren(element_id) { 40 var element = $(element_id); 41 element.textContent = ''; 42 } 43 44 function onload() { 45 var tabContents = document.querySelectorAll('#content > div'); 46 for (var i = 0; i != tabContents.length; i++) { 47 var tabContent = tabContents[i]; 48 var tabName = tabContent.querySelector('.content-header').textContent; 49 50 var tabHeader = document.createElement('div'); 51 tabHeader.className = 'tab-header'; 52 var button = document.createElement('button'); 53 button.textContent = tabName; 54 tabHeader.appendChild(button); 55 tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id)); 56 $('navigation').appendChild(tabHeader); 57 } 58 onHashChange(); 59 initSettings(); 60 sendCommand('init-ui'); 61 window.addEventListener('message', onMessage.bind(this), false); 62 } 63 64 function onMessage(event) { 65 if (event.origin != 'chrome://serviceworker-internals') { 66 return; 67 } 68 populateServiceWorkers(event.data.partition_id, 69 event.data.workers); 70 } 71 72 function onHashChange() { 73 var hash = window.location.hash.slice(1).toLowerCase(); 74 if (!selectTab(hash)) 75 selectTab('devices'); 76 } 77 78 /** 79 * @param {string} id Tab id. 80 * @return {boolean} True if successful. 81 */ 82 function selectTab(id) { 83 closePortForwardingConfig(); 84 85 var tabContents = document.querySelectorAll('#content > div'); 86 var tabHeaders = $('navigation').querySelectorAll('.tab-header'); 87 var found = false; 88 for (var i = 0; i != tabContents.length; i++) { 89 var tabContent = tabContents[i]; 90 var tabHeader = tabHeaders[i]; 91 if (tabContent.id == id) { 92 tabContent.classList.add('selected'); 93 tabHeader.classList.add('selected'); 94 found = true; 95 } else { 96 tabContent.classList.remove('selected'); 97 tabHeader.classList.remove('selected'); 98 } 99 } 100 if (!found) 101 return false; 102 window.location.hash = id; 103 return true; 104 } 105 106 function populateServiceWorkers(partition_id, workers) { 107 var list = $('service-workers-list-' + partition_id); 108 if (workers.length == 0) { 109 if (list) { 110 list.parentNode.removeChild(list); 111 } 112 return; 113 } 114 if (list) { 115 list.textContent = ''; 116 } else { 117 list = document.createElement('div'); 118 list.id = 'service-workers-list-' + partition_id; 119 list.className = 'list'; 120 $('service-workers-list').appendChild(list); 121 } 122 for (var i = 0; i < workers.length; i++) { 123 var worker = workers[i]; 124 worker.hasCustomInspectAction = true; 125 var row = addTargetToList(worker, list, ['scope', 'url']); 126 addActionLink( 127 row, 128 'inspect', 129 sendServiceWorkerCommand.bind(null, 'inspect', worker), 130 false); 131 addActionLink( 132 row, 133 'terminate', 134 sendServiceWorkerCommand.bind(null, 'stop', worker), 135 false); 136 } 137 } 138 139 function populateTargets(source, data) { 140 if (source == 'renderers') 141 populateWebContentsTargets(data); 142 else if (source == 'workers') 143 populateWorkerTargets(data); 144 else if (source == 'adb') 145 populateRemoteTargets(data); 146 else 147 console.error('Unknown source type: ' + source); 148 } 149 150 function populateWebContentsTargets(data) { 151 removeChildren('pages-list'); 152 removeChildren('extensions-list'); 153 removeChildren('apps-list'); 154 removeChildren('others-list'); 155 156 for (var i = 0; i < data.length; i++) { 157 if (data[i].type === 'page') 158 addToPagesList(data[i]); 159 else if (data[i].type === 'background_page') 160 addToExtensionsList(data[i]); 161 else if (data[i].type === 'app') 162 addToAppsList(data[i]); 163 else 164 addToOthersList(data[i]); 165 } 166 } 167 168 function populateWorkerTargets(data) { 169 removeChildren('workers-list'); 170 171 for (var i = 0; i < data.length; i++) 172 addToWorkersList(data[i]); 173 } 174 175 function showIncognitoWarning() { 176 $('devices-incognito').hidden = false; 177 } 178 179 function populateRemoteTargets(devices) { 180 if (!devices) 181 return; 182 183 if (window.modal) { 184 window.holdDevices = devices; 185 return; 186 } 187 188 function alreadyDisplayed(element, data) { 189 var json = JSON.stringify(data); 190 if (element.cachedJSON == json) 191 return true; 192 element.cachedJSON = json; 193 return false; 194 } 195 196 function insertChildSortedById(parent, child) { 197 for (var sibling = parent.firstElementChild; 198 sibling; 199 sibling = sibling.nextElementSibling) { 200 if (sibling.id > child.id) { 201 parent.insertBefore(child, sibling); 202 return; 203 } 204 } 205 parent.appendChild(child); 206 } 207 208 var deviceList = $('devices-list'); 209 if (alreadyDisplayed(deviceList, devices)) 210 return; 211 212 function removeObsolete(validIds, section) { 213 if (validIds.indexOf(section.id) < 0) 214 section.remove(); 215 } 216 217 var newDeviceIds = devices.map(function(d) { return d.id }); 218 Array.prototype.forEach.call( 219 deviceList.querySelectorAll('.device'), 220 removeObsolete.bind(null, newDeviceIds)); 221 222 $('devices-help').hidden = !!devices.length; 223 224 for (var d = 0; d < devices.length; d++) { 225 var device = devices[d]; 226 227 var deviceSection = $(device.id); 228 if (!deviceSection) { 229 deviceSection = document.createElement('div'); 230 deviceSection.id = device.id; 231 deviceSection.className = 'device'; 232 deviceList.appendChild(deviceSection); 233 234 var deviceHeader = document.createElement('div'); 235 deviceHeader.className = 'device-header'; 236 deviceSection.appendChild(deviceHeader); 237 238 var deviceName = document.createElement('div'); 239 deviceName.className = 'device-name'; 240 deviceHeader.appendChild(deviceName); 241 242 var deviceSerial = document.createElement('div'); 243 deviceSerial.className = 'device-serial'; 244 deviceSerial.textContent = '#' + device.adbSerial.toUpperCase(); 245 deviceHeader.appendChild(deviceSerial); 246 247 var devicePorts = document.createElement('div'); 248 devicePorts.className = 'device-ports'; 249 deviceHeader.appendChild(devicePorts); 250 251 var browserList = document.createElement('div'); 252 browserList.className = 'browsers'; 253 deviceSection.appendChild(browserList); 254 255 var authenticating = document.createElement('div'); 256 authenticating.className = 'device-auth'; 257 deviceSection.appendChild(authenticating); 258 } 259 260 if (alreadyDisplayed(deviceSection, device)) 261 continue; 262 263 deviceSection.querySelector('.device-name').textContent = device.adbModel; 264 deviceSection.querySelector('.device-auth').textContent = 265 device.adbConnected ? '' : 'Pending authentication: please accept ' + 266 'debugging session on the device.'; 267 268 var browserList = deviceSection.querySelector('.browsers'); 269 var newBrowserIds = 270 device.browsers.map(function(b) { return b.id }); 271 Array.prototype.forEach.call( 272 browserList.querySelectorAll('.browser'), 273 removeObsolete.bind(null, newBrowserIds)); 274 275 for (var b = 0; b < device.browsers.length; b++) { 276 var browser = device.browsers[b]; 277 278 var majorChromeVersion = browser.adbBrowserChromeVersion; 279 280 var incompatibleVersion = browser.hasOwnProperty('compatibleVersion') && 281 !browser.compatibleVersion; 282 var pageList; 283 var browserSection = $(browser.id); 284 if (browserSection) { 285 pageList = browserSection.querySelector('.pages'); 286 } else { 287 browserSection = document.createElement('div'); 288 browserSection.id = browser.id; 289 browserSection.className = 'browser'; 290 insertChildSortedById(browserList, browserSection); 291 292 var browserHeader = document.createElement('div'); 293 browserHeader.className = 'browser-header'; 294 295 var browserName = document.createElement('div'); 296 browserName.className = 'browser-name'; 297 browserHeader.appendChild(browserName); 298 browserName.textContent = browser.adbBrowserName; 299 if (browser.adbBrowserVersion) 300 browserName.textContent += ' (' + browser.adbBrowserVersion + ')'; 301 browserSection.appendChild(browserHeader); 302 303 if (incompatibleVersion) { 304 var warningSection = document.createElement('div'); 305 warningSection.className = 'warning'; 306 warningSection.textContent = 307 'You may need a newer version of desktop Chrome. ' + 308 'Please try Chrome ' + browser.adbBrowserVersion + ' or later.'; 309 browserHeader.appendChild(warningSection); 310 } else if (majorChromeVersion >= MIN_VERSION_NEW_TAB) { 311 var newPage = document.createElement('div'); 312 newPage.className = 'open'; 313 314 var newPageUrl = document.createElement('input'); 315 newPageUrl.type = 'text'; 316 newPageUrl.placeholder = 'Open tab with url'; 317 newPage.appendChild(newPageUrl); 318 319 var openHandler = function(sourceId, browserId, input) { 320 sendCommand( 321 'open', sourceId, browserId, input.value || 'about:blank'); 322 input.value = ''; 323 }.bind(null, browser.source, browser.id, newPageUrl); 324 newPageUrl.addEventListener('keyup', function(handler, event) { 325 if (event.keyIdentifier == 'Enter' && event.target.value) 326 handler(); 327 }.bind(null, openHandler), true); 328 329 var newPageButton = document.createElement('button'); 330 newPageButton.textContent = 'Open'; 331 newPage.appendChild(newPageButton); 332 newPageButton.addEventListener('click', openHandler, true); 333 334 browserHeader.appendChild(newPage); 335 } 336 337 var browserInspector; 338 var browserInspectorTitle; 339 if ('trace' in queryParamsObject || 'tracing' in queryParamsObject) { 340 browserInspector = 'chrome://tracing'; 341 browserInspectorTitle = 'trace'; 342 } else { 343 browserInspector = queryParamsObject['browser-inspector']; 344 browserInspectorTitle = 'inspect'; 345 } 346 if (browserInspector) { 347 var link = document.createElement('span'); 348 link.classList.add('action'); 349 link.setAttribute('tabindex', 1); 350 link.textContent = browserInspectorTitle; 351 browserHeader.appendChild(link); 352 link.addEventListener( 353 'click', 354 sendCommand.bind(null, 'inspect-browser', browser.source, 355 browser.id, browserInspector), false); 356 } 357 358 pageList = document.createElement('div'); 359 pageList.className = 'list pages'; 360 browserSection.appendChild(pageList); 361 } 362 363 if (incompatibleVersion || alreadyDisplayed(browserSection, browser)) 364 continue; 365 366 pageList.textContent = ''; 367 for (var p = 0; p < browser.pages.length; p++) { 368 var page = browser.pages[p]; 369 // Attached targets have no unique id until Chrome 26. For such targets 370 // it is impossible to activate existing DevTools window. 371 page.hasNoUniqueId = page.attached && 372 (majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID); 373 var row = addTargetToList(page, pageList, ['name', 'url']); 374 if (page['description']) 375 addWebViewDetails(row, page); 376 else 377 addFavicon(row, page); 378 if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) { 379 addActionLink(row, 'focus tab', 380 sendTargetCommand.bind(null, 'activate', page), false); 381 } 382 if (majorChromeVersion) { 383 addActionLink(row, 'reload', 384 sendTargetCommand.bind(null, 'reload', page), page.attached); 385 } 386 if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) { 387 addActionLink(row, 'close', 388 sendTargetCommand.bind(null, 'close', page), false); 389 } 390 } 391 } 392 } 393 } 394 395 function addToPagesList(data) { 396 var row = addTargetToList(data, $('pages-list'), ['name', 'url']); 397 addFavicon(row, data); 398 if (data.guests) 399 addGuestViews(row, data.guests); 400 } 401 402 function addToExtensionsList(data) { 403 var row = addTargetToList(data, $('extensions-list'), ['name', 'url']); 404 addFavicon(row, data); 405 if (data.guests) 406 addGuestViews(row, data.guests); 407 } 408 409 function addToAppsList(data) { 410 var row = addTargetToList(data, $('apps-list'), ['name', 'url']); 411 addFavicon(row, data); 412 if (data.guests) 413 addGuestViews(row, data.guests); 414 } 415 416 function addGuestViews(row, guests) { 417 Array.prototype.forEach.call(guests, function(guest) { 418 var guestRow = addTargetToList(guest, row, ['name', 'url']); 419 guestRow.classList.add('guest'); 420 addFavicon(guestRow, guest); 421 }); 422 } 423 424 function addToWorkersList(data) { 425 var row = 426 addTargetToList(data, $('workers-list'), ['name', 'description', 'url']); 427 addActionLink(row, 'terminate', 428 sendTargetCommand.bind(null, 'close', data), false); 429 } 430 431 function addToOthersList(data) { 432 addTargetToList(data, $('others-list'), ['url']); 433 } 434 435 function formatValue(data, property) { 436 var value = data[property]; 437 438 if (property == 'name' && value == '') { 439 value = 'untitled'; 440 } 441 442 var text = value ? String(value) : ''; 443 if (text.length > 100) 444 text = text.substring(0, 100) + '\u2026'; 445 446 var div = document.createElement('div'); 447 div.textContent = text; 448 div.className = property; 449 return div; 450 } 451 452 function addFavicon(row, data) { 453 var favicon = document.createElement('img'); 454 if (data['faviconUrl']) 455 favicon.src = data['faviconUrl']; 456 var propertiesBox = row.querySelector('.properties-box'); 457 propertiesBox.insertBefore(favicon, propertiesBox.firstChild); 458 } 459 460 function addWebViewDetails(row, data) { 461 var webview; 462 try { 463 webview = JSON.parse(data['description']); 464 } catch (e) { 465 return; 466 } 467 addWebViewDescription(row, webview); 468 if (data.adbScreenWidth && data.adbScreenHeight) 469 addWebViewThumbnail( 470 row, webview, data.adbScreenWidth, data.adbScreenHeight); 471 } 472 473 function addWebViewDescription(row, webview) { 474 var viewStatus = { visibility: '', position: '', size: '' }; 475 if (!webview.empty) { 476 if (webview.attached && !webview.visible) 477 viewStatus.visibility = 'hidden'; 478 else if (!webview.attached) 479 viewStatus.visibility = 'detached'; 480 viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height; 481 } else { 482 viewStatus.visibility = 'empty'; 483 } 484 if (webview.attached) { 485 viewStatus.position = 486 'at (' + webview.screenX + ', ' + webview.screenY + ')'; 487 } 488 489 var subRow = document.createElement('div'); 490 subRow.className = 'subrow webview'; 491 if (webview.empty || !webview.attached || !webview.visible) 492 subRow.className += ' invisible-view'; 493 if (viewStatus.visibility) 494 subRow.appendChild(formatValue(viewStatus, 'visibility')); 495 if (viewStatus.position) 496 subRow.appendChild(formatValue(viewStatus, 'position')); 497 subRow.appendChild(formatValue(viewStatus, 'size')); 498 var subrowBox = row.querySelector('.subrow-box'); 499 subrowBox.insertBefore(subRow, row.querySelector('.actions')); 500 } 501 502 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) { 503 var maxScreenRectSize = 50; 504 var screenRectWidth; 505 var screenRectHeight; 506 507 var aspectRatio = screenWidth / screenHeight; 508 if (aspectRatio < 1) { 509 screenRectWidth = Math.round(maxScreenRectSize * aspectRatio); 510 screenRectHeight = maxScreenRectSize; 511 } else { 512 screenRectWidth = maxScreenRectSize; 513 screenRectHeight = Math.round(maxScreenRectSize / aspectRatio); 514 } 515 516 var thumbnail = document.createElement('div'); 517 thumbnail.className = 'webview-thumbnail'; 518 var thumbnailWidth = 3 * screenRectWidth; 519 var thumbnailHeight = 60; 520 thumbnail.style.width = thumbnailWidth + 'px'; 521 thumbnail.style.height = thumbnailHeight + 'px'; 522 523 var screenRect = document.createElement('div'); 524 screenRect.className = 'screen-rect'; 525 screenRect.style.left = screenRectWidth + 'px'; 526 screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px'; 527 screenRect.style.width = screenRectWidth + 'px'; 528 screenRect.style.height = screenRectHeight + 'px'; 529 thumbnail.appendChild(screenRect); 530 531 if (!webview.empty && webview.attached) { 532 var viewRect = document.createElement('div'); 533 viewRect.className = 'view-rect'; 534 if (!webview.visible) 535 viewRect.classList.add('hidden'); 536 function percent(ratio) { 537 return ratio * 100 + '%'; 538 } 539 viewRect.style.left = percent(webview.screenX / screenWidth); 540 viewRect.style.top = percent(webview.screenY / screenHeight); 541 viewRect.style.width = percent(webview.width / screenWidth); 542 viewRect.style.height = percent(webview.height / screenHeight); 543 screenRect.appendChild(viewRect); 544 } 545 546 var propertiesBox = row.querySelector('.properties-box'); 547 propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild); 548 } 549 550 function addTargetToList(data, list, properties) { 551 var row = document.createElement('div'); 552 row.className = 'row'; 553 554 var propertiesBox = document.createElement('div'); 555 propertiesBox.className = 'properties-box'; 556 row.appendChild(propertiesBox); 557 558 var subrowBox = document.createElement('div'); 559 subrowBox.className = 'subrow-box'; 560 propertiesBox.appendChild(subrowBox); 561 562 var subrow = document.createElement('div'); 563 subrow.className = 'subrow'; 564 subrowBox.appendChild(subrow); 565 566 for (var j = 0; j < properties.length; j++) 567 subrow.appendChild(formatValue(data, properties[j])); 568 569 var actionBox = document.createElement('div'); 570 actionBox.className = 'actions'; 571 subrowBox.appendChild(actionBox); 572 573 if (!data.hasCustomInspectAction) { 574 addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data), 575 data.hasNoUniqueId || data.adbAttachedForeign); 576 } 577 578 list.appendChild(row); 579 return row; 580 } 581 582 function addActionLink(row, text, handler, opt_disabled) { 583 var link = document.createElement('span'); 584 link.classList.add('action'); 585 link.setAttribute('tabindex', 1); 586 if (opt_disabled) 587 link.classList.add('disabled'); 588 else 589 link.classList.remove('disabled'); 590 591 link.textContent = text; 592 link.addEventListener('click', handler, true); 593 function handleKey(e) { 594 if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') { 595 e.preventDefault(); 596 handler(); 597 } 598 } 599 link.addEventListener('keydown', handleKey, true); 600 row.querySelector('.actions').appendChild(link); 601 } 602 603 604 function initSettings() { 605 $('discover-usb-devices-enable').addEventListener('change', 606 enableDiscoverUsbDevices); 607 608 $('port-forwarding-enable').addEventListener('change', enablePortForwarding); 609 $('port-forwarding-config-open').addEventListener( 610 'click', openPortForwardingConfig); 611 $('port-forwarding-config-close').addEventListener( 612 'click', closePortForwardingConfig); 613 $('port-forwarding-config-done').addEventListener( 614 'click', commitPortForwardingConfig.bind(true)); 615 } 616 617 function enableDiscoverUsbDevices(event) { 618 sendCommand('set-discover-usb-devices-enabled', event.target.checked); 619 } 620 621 function enablePortForwarding(event) { 622 sendCommand('set-port-forwarding-enabled', event.target.checked); 623 } 624 625 function handleKey(event) { 626 switch (event.keyCode) { 627 case 13: // Enter 628 if (event.target.nodeName == 'INPUT') { 629 var line = event.target.parentNode; 630 if (!line.classList.contains('fresh') || 631 line.classList.contains('empty')) { 632 commitPortForwardingConfig(true); 633 } else { 634 commitFreshLineIfValid(true /* select new line */); 635 commitPortForwardingConfig(false); 636 } 637 } else { 638 commitPortForwardingConfig(true); 639 } 640 break; 641 642 case 27: 643 commitPortForwardingConfig(true); 644 break; 645 } 646 } 647 648 function setModal(dialog) { 649 dialog.deactivatedNodes = Array.prototype.filter.call( 650 document.querySelectorAll('*'), 651 function(n) { 652 return n != dialog && !dialog.contains(n) && n.tabIndex >= 0; 653 }); 654 655 dialog.tabIndexes = dialog.deactivatedNodes.map( 656 function(n) { return n.getAttribute('tabindex'); }); 657 658 dialog.deactivatedNodes.forEach(function(n) { n.tabIndex = -1; }); 659 window.modal = dialog; 660 } 661 662 function unsetModal(dialog) { 663 for (var i = 0; i < dialog.deactivatedNodes.length; i++) { 664 var node = dialog.deactivatedNodes[i]; 665 if (dialog.tabIndexes[i] === null) 666 node.removeAttribute('tabindex'); 667 else 668 node.setAttribute('tabindex', dialog.tabIndexes[i]); 669 } 670 671 if (window.holdDevices) { 672 populateRemoteTargets(window.holdDevices); 673 delete window.holdDevices; 674 } 675 676 delete dialog.deactivatedNodes; 677 delete dialog.tabIndexes; 678 delete window.modal; 679 } 680 681 function openPortForwardingConfig() { 682 loadPortForwardingConfig(window.portForwardingConfig); 683 684 $('port-forwarding-overlay').classList.add('open'); 685 document.addEventListener('keyup', handleKey); 686 687 var freshPort = document.querySelector('.fresh .port'); 688 if (freshPort) 689 freshPort.focus(); 690 else 691 $('port-forwarding-config-done').focus(); 692 693 setModal($('port-forwarding-overlay')); 694 } 695 696 function closePortForwardingConfig() { 697 if (!$('port-forwarding-overlay').classList.contains('open')) 698 return; 699 700 $('port-forwarding-overlay').classList.remove('open'); 701 document.removeEventListener('keyup', handleKey); 702 unsetModal($('port-forwarding-overlay')); 703 } 704 705 function loadPortForwardingConfig(config) { 706 var list = $('port-forwarding-config-list'); 707 list.textContent = ''; 708 for (var port in config) 709 list.appendChild(createConfigLine(port, config[port])); 710 list.appendChild(createEmptyConfigLine()); 711 } 712 713 function commitPortForwardingConfig(closeConfig) { 714 if (closeConfig) 715 closePortForwardingConfig(); 716 717 commitFreshLineIfValid(); 718 var lines = document.querySelectorAll('.port-forwarding-pair'); 719 var config = {}; 720 for (var i = 0; i != lines.length; i++) { 721 var line = lines[i]; 722 var portInput = line.querySelector('.port'); 723 var locationInput = line.querySelector('.location'); 724 725 var port = portInput.classList.contains('invalid') ? 726 portInput.lastValidValue : 727 portInput.value; 728 729 var location = locationInput.classList.contains('invalid') ? 730 locationInput.lastValidValue : 731 locationInput.value; 732 733 if (port && location) 734 config[port] = location; 735 } 736 sendCommand('set-port-forwarding-config', config); 737 } 738 739 function updateDiscoverUsbDevicesEnabled(enabled) { 740 var checkbox = $('discover-usb-devices-enable'); 741 checkbox.checked = !!enabled; 742 checkbox.disabled = false; 743 } 744 745 function updatePortForwardingEnabled(enabled) { 746 var checkbox = $('port-forwarding-enable'); 747 checkbox.checked = !!enabled; 748 checkbox.disabled = false; 749 } 750 751 function updatePortForwardingConfig(config) { 752 window.portForwardingConfig = config; 753 $('port-forwarding-config-open').disabled = !config; 754 } 755 756 function createConfigLine(port, location) { 757 var line = document.createElement('div'); 758 line.className = 'port-forwarding-pair'; 759 760 var portInput = createConfigField(port, 'port', 'Port', validatePort); 761 line.appendChild(portInput); 762 763 var locationInput = createConfigField( 764 location, 'location', 'IP address and port', validateLocation); 765 line.appendChild(locationInput); 766 locationInput.addEventListener('keydown', function(e) { 767 if (e.keyIdentifier == 'U+0009' && // Tab 768 !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey && 769 line.classList.contains('fresh') && 770 !line.classList.contains('empty')) { 771 // Tabbing forward on the fresh line, try create a new empty one. 772 if (commitFreshLineIfValid(true)) 773 e.preventDefault(); 774 } 775 }); 776 777 var lineDelete = document.createElement('div'); 778 lineDelete.className = 'close-button'; 779 lineDelete.addEventListener('click', function() { 780 var newSelection = line.nextElementSibling; 781 line.parentNode.removeChild(line); 782 selectLine(newSelection); 783 }); 784 line.appendChild(lineDelete); 785 786 line.addEventListener('click', selectLine.bind(null, line)); 787 line.addEventListener('focus', selectLine.bind(null, line)); 788 789 checkEmptyLine(line); 790 791 return line; 792 } 793 794 function validatePort(input) { 795 var match = input.value.match(/^(\d+)$/); 796 if (!match) 797 return false; 798 var port = parseInt(match[1]); 799 if (port < 1024 || 65535 < port) 800 return false; 801 802 var inputs = document.querySelectorAll('input.port:not(.invalid)'); 803 for (var i = 0; i != inputs.length; ++i) { 804 if (inputs[i] == input) 805 break; 806 if (parseInt(inputs[i].value) == port) 807 return false; 808 } 809 return true; 810 } 811 812 function validateLocation(input) { 813 var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/); 814 if (!match) 815 return false; 816 var port = parseInt(match[2]); 817 return port <= 65535; 818 } 819 820 function createEmptyConfigLine() { 821 var line = createConfigLine('', ''); 822 line.classList.add('fresh'); 823 return line; 824 } 825 826 function createConfigField(value, className, hint, validate) { 827 var input = document.createElement('input'); 828 input.className = className; 829 input.type = 'text'; 830 input.placeholder = hint; 831 input.value = value; 832 input.lastValidValue = value; 833 834 function checkInput() { 835 if (validate(input)) 836 input.classList.remove('invalid'); 837 else 838 input.classList.add('invalid'); 839 if (input.parentNode) 840 checkEmptyLine(input.parentNode); 841 } 842 checkInput(); 843 844 input.addEventListener('keyup', checkInput); 845 input.addEventListener('focus', function() { 846 selectLine(input.parentNode); 847 }); 848 849 input.addEventListener('blur', function() { 850 if (validate(input)) 851 input.lastValidValue = input.value; 852 }); 853 854 return input; 855 } 856 857 function checkEmptyLine(line) { 858 var inputs = line.querySelectorAll('input'); 859 var empty = true; 860 for (var i = 0; i != inputs.length; i++) { 861 if (inputs[i].value != '') 862 empty = false; 863 } 864 if (empty) 865 line.classList.add('empty'); 866 else 867 line.classList.remove('empty'); 868 } 869 870 function selectLine(line) { 871 if (line.classList.contains('selected')) 872 return; 873 unselectLine(); 874 line.classList.add('selected'); 875 } 876 877 function unselectLine() { 878 var line = document.querySelector('.port-forwarding-pair.selected'); 879 if (!line) 880 return; 881 line.classList.remove('selected'); 882 commitFreshLineIfValid(); 883 } 884 885 function commitFreshLineIfValid(opt_selectNew) { 886 var line = document.querySelector('.port-forwarding-pair.fresh'); 887 if (line.querySelector('.invalid')) 888 return false; 889 line.classList.remove('fresh'); 890 var freshLine = createEmptyConfigLine(); 891 line.parentNode.appendChild(freshLine); 892 if (opt_selectNew) 893 freshLine.querySelector('.port').focus(); 894 return true; 895 } 896 897 function populatePortStatus(devicesStatusMap) { 898 for (var deviceId in devicesStatusMap) { 899 if (!devicesStatusMap.hasOwnProperty(deviceId)) 900 continue; 901 var deviceStatusMap = devicesStatusMap[deviceId]; 902 903 var deviceSection = $(deviceId); 904 if (!deviceSection) 905 continue; 906 907 var devicePorts = deviceSection.querySelector('.device-ports'); 908 devicePorts.textContent = ''; 909 for (var port in deviceStatusMap) { 910 if (!deviceStatusMap.hasOwnProperty(port)) 911 continue; 912 913 var status = deviceStatusMap[port]; 914 var portIcon = document.createElement('div'); 915 portIcon.className = 'port-icon'; 916 // status === 0 is the default (connected) state. 917 // Positive values correspond to the tunnelling connection count 918 // (in DEBUG_DEVTOOLS mode). 919 if (status > 0) 920 portIcon.classList.add('connected'); 921 else if (status === -1 || status === -2) 922 portIcon.classList.add('transient'); 923 else if (status < 0) 924 portIcon.classList.add('error'); 925 devicePorts.appendChild(portIcon); 926 927 var portNumber = document.createElement('div'); 928 portNumber.className = 'port-number'; 929 portNumber.textContent = ':' + port; 930 if (status > 0) 931 portNumber.textContent += '(' + status + ')'; 932 devicePorts.appendChild(portNumber); 933 } 934 } 935 936 function clearPorts(deviceSection) { 937 if (deviceSection.id in devicesStatusMap) 938 return; 939 deviceSection.querySelector('.device-ports').textContent = ''; 940 } 941 942 Array.prototype.forEach.call( 943 document.querySelectorAll('.device'), clearPorts); 944 } 945 946 document.addEventListener('DOMContentLoaded', onload); 947 948 window.addEventListener('hashchange', onHashChange); 949