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