• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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