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
10function sendCommand(command, args) {
11  chrome.send(command, Array.prototype.slice.call(arguments, 1));
12}
13
14function sendTargetCommand(command, target) {
15  sendCommand(command, target.source, target.id);
16}
17
18function removeChildren(element_id) {
19  var element = $(element_id);
20  element.textContent = '';
21}
22
23function 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
43function 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
60function 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
71function 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
89function populateWorkerTargets(data) {
90  removeChildren('workers-list');
91
92  for (var i = 0; i < data.length; i++)
93    addToWorkersList(data[i]);
94}
95
96function 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
308function addToPagesList(data) {
309  var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
310  addFavicon(row, data);
311}
312
313function addToExtensionsList(data) {
314  var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
315  addFavicon(row, data);
316}
317
318function 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
330function 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
337function addToOthersList(data) {
338  addTargetToList(data, $('others-list'), ['url']);
339}
340
341function 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
358function 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
365function 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
378function 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
409function 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
456function 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
486function 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
500function 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
513function enableDiscoverUsbDevices(event) {
514  sendCommand('set-discover-usb-devices-enabled', event.target.checked);
515}
516
517function enablePortForwarding(event) {
518  sendCommand('set-port-forwarding-enabled', event.target.checked);
519}
520
521function 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
544function 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
558function 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
577function 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
592function closePortForwardingConfig() {
593  $('port-forwarding-overlay').classList.remove('open');
594  document.removeEventListener('keyup', handleKey);
595  unsetModal($('port-forwarding-overlay'));
596}
597
598function 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
606function 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
632function updateDiscoverUsbDevicesEnabled(enabled) {
633  var checkbox = $('discover-usb-devices-enable');
634  checkbox.checked = !!enabled;
635  checkbox.disabled = false;
636}
637
638function updatePortForwardingEnabled(enabled) {
639  var checkbox = $('port-forwarding-enable');
640  checkbox.checked = !!enabled;
641  checkbox.disabled = false;
642}
643
644function updatePortForwardingConfig(config) {
645  window.portForwardingConfig = config;
646  $('port-forwarding-config-open').disabled = !config;
647}
648
649function 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
687function 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
705function 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
713function createEmptyConfigLine() {
714  var line = createConfigLine('', '');
715  line.classList.add('fresh');
716  return line;
717}
718
719function 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
750function 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
763function selectLine(line) {
764  if (line.classList.contains('selected'))
765    return;
766  unselectLine();
767  line.classList.add('selected');
768}
769
770function unselectLine() {
771  var line = document.querySelector('.port-forwarding-pair.selected');
772  if (!line)
773    return;
774  line.classList.remove('selected');
775  commitFreshLineIfValid();
776}
777
778function 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
789document.addEventListener('DOMContentLoaded', onload);
790