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