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