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 5var MIN_VERSION_TAB_CLOSE = 25; 6var MIN_VERSION_TARGET_ID = 26; 7var MIN_VERSION_NEW_TAB = 29; 8var MIN_VERSION_TAB_ACTIVATE = 30; 9 10var queryParamsObject = {}; 11 12(function() { 13var queryParams = window.location.search; 14if (!queryParams) 15 return; 16var params = queryParams.substring(1).split('&'); 17for (var i = 0; i < params.length; ++i) { 18 var pair = params[i].split('='); 19 queryParamsObject[pair[0]] = pair[1]; 20} 21 22})(); 23 24function sendCommand(command, args) { 25 chrome.send(command, Array.prototype.slice.call(arguments, 1)); 26} 27 28function sendTargetCommand(command, target) { 29 sendCommand(command, target.source, target.id); 30} 31 32function sendServiceWorkerCommand(action, worker) { 33 $('serviceworker-internals').contentWindow.postMessage({ 34 'action': action, 35 'worker': worker 36 },'chrome://serviceworker-internals'); 37} 38 39function removeChildren(element_id) { 40 var element = $(element_id); 41 element.textContent = ''; 42} 43 44function 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 64function onMessage(event) { 65 if (event.origin != 'chrome://serviceworker-internals') { 66 return; 67 } 68 populateServiceWorkers(event.data.partition_id, 69 event.data.workers); 70} 71 72function 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 */ 82function 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 106function 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 139function 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 150function 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 168function populateWorkerTargets(data) { 169 removeChildren('workers-list'); 170 171 for (var i = 0; i < data.length; i++) 172 addToWorkersList(data[i]); 173} 174 175function showIncognitoWarning() { 176 $('devices-incognito').hidden = false; 177} 178 179function 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 395function 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 402function 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 409function 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 416function 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 424function 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 431function addToOthersList(data) { 432 addTargetToList(data, $('others-list'), ['url']); 433} 434 435function 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 452function 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 460function 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 473function 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 502function 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 550function 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 582function 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 604function 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 617function enableDiscoverUsbDevices(event) { 618 sendCommand('set-discover-usb-devices-enabled', event.target.checked); 619} 620 621function enablePortForwarding(event) { 622 sendCommand('set-port-forwarding-enabled', event.target.checked); 623} 624 625function 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 648function 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 662function 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 681function 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 696function 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 705function 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 713function 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 739function updateDiscoverUsbDevicesEnabled(enabled) { 740 var checkbox = $('discover-usb-devices-enable'); 741 checkbox.checked = !!enabled; 742 checkbox.disabled = false; 743} 744 745function updatePortForwardingEnabled(enabled) { 746 var checkbox = $('port-forwarding-enable'); 747 checkbox.checked = !!enabled; 748 checkbox.disabled = false; 749} 750 751function updatePortForwardingConfig(config) { 752 window.portForwardingConfig = config; 753 $('port-forwarding-config-open').disabled = !config; 754} 755 756function 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 794function 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 812function 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 820function createEmptyConfigLine() { 821 var line = createConfigLine('', ''); 822 line.classList.add('fresh'); 823 return line; 824} 825 826function 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 857function 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 870function selectLine(line) { 871 if (line.classList.contains('selected')) 872 return; 873 unselectLine(); 874 line.classList.add('selected'); 875} 876 877function unselectLine() { 878 var line = document.querySelector('.port-forwarding-pair.selected'); 879 if (!line) 880 return; 881 line.classList.remove('selected'); 882 commitFreshLineIfValid(); 883} 884 885function 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 897function 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 946document.addEventListener('DOMContentLoaded', onload); 947 948window.addEventListener('hashchange', onHashChange); 949