Only grant permissions to new extensions from sync if they have the expected version
[chromium-blink-merge.git] / chrome / browser / resources / inspect / inspect.js
blobf456b66c0e28600c11d15833181eee6c92c2d4f7
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.
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 var WEBRTC_SERIAL = 'WEBRTC';
11 var queryParamsObject = {};
12 var browserInspector;
13 var browserInspectorTitle;
15 (function() {
16 var queryParams = window.location.search;
17 if (!queryParams)
18     return;
19 var params = queryParams.substring(1).split('&');
20 for (var i = 0; i < params.length; ++i) {
21     var pair = params[i].split('=');
22     queryParamsObject[pair[0]] = pair[1];
25 if ('trace' in queryParamsObject || 'tracing' in queryParamsObject) {
26   browserInspector = 'chrome://tracing';
27   browserInspectorTitle = 'trace';
28 } else {
29   browserInspector = queryParamsObject['browser-inspector'];
30   browserInspectorTitle = 'inspect';
32 })();
34 function sendCommand(command, args) {
35   chrome.send(command, Array.prototype.slice.call(arguments, 1));
38 function sendTargetCommand(command, target) {
39   sendCommand(command, target.source, target.id);
42 function removeChildren(element_id) {
43   var element = $(element_id);
44   element.textContent = '';
47 function onload() {
48   var tabContents = document.querySelectorAll('#content > div');
49   for (var i = 0; i != tabContents.length; i++) {
50     var tabContent = tabContents[i];
51     var tabName = tabContent.querySelector('.content-header').textContent;
53     var tabHeader = document.createElement('div');
54     tabHeader.className = 'tab-header';
55     var button = document.createElement('button');
56     button.textContent = tabName;
57     tabHeader.appendChild(button);
58     tabHeader.addEventListener('click', selectTab.bind(null, tabContent.id));
59     $('navigation').appendChild(tabHeader);
60   }
61   onHashChange();
62   initSettings();
63   sendCommand('init-ui');
66 function onHashChange() {
67   var hash = window.location.hash.slice(1).toLowerCase();
68   if (!selectTab(hash))
69     selectTab('devices');
72 /**
73  * @param {string} id Tab id.
74  * @return {boolean} True if successful.
75  */
76 function selectTab(id) {
77   closePortForwardingConfig();
79   var tabContents = document.querySelectorAll('#content > div');
80   var tabHeaders = $('navigation').querySelectorAll('.tab-header');
81   var found = false;
82   for (var i = 0; i != tabContents.length; i++) {
83     var tabContent = tabContents[i];
84     var tabHeader = tabHeaders[i];
85     if (tabContent.id == id) {
86       tabContent.classList.add('selected');
87       tabHeader.classList.add('selected');
88       found = true;
89     } else {
90       tabContent.classList.remove('selected');
91       tabHeader.classList.remove('selected');
92     }
93   }
94   if (!found)
95     return false;
96   window.location.hash = id;
97   return true;
100 function populateTargets(source, data) {
101   if (source == 'local')
102     populateLocalTargets(data);
103   else if (source == 'remote')
104     populateRemoteTargets(data);
105   else
106     console.error('Unknown source type: ' + source);
109 function populateLocalTargets(data) {
110   removeChildren('pages-list');
111   removeChildren('extensions-list');
112   removeChildren('apps-list');
113   removeChildren('others-list');
114   removeChildren('workers-list');
115   removeChildren('service-workers-list');
117     for (var i = 0; i < data.length; i++) {
118     if (data[i].type === 'page')
119       addToPagesList(data[i]);
120     else if (data[i].type === 'background_page')
121       addToExtensionsList(data[i]);
122     else if (data[i].type === 'app')
123       addToAppsList(data[i]);
124     else if (data[i].type === 'worker')
125       addToWorkersList(data[i]);
126     else if (data[i].type === 'service_worker')
127       addToServiceWorkersList(data[i]);
128     else
129       addToOthersList(data[i]);
130   }
133 function showIncognitoWarning() {
134   $('devices-incognito').hidden = false;
137 function alreadyDisplayed(element, data) {
138   var json = JSON.stringify(data);
139   if (element.cachedJSON == json)
140     return true;
141   element.cachedJSON = json;
142   return false;
145 function updateBrowserVisibility(browserSection) {
146   var icon = browserSection.querySelector('.used-for-port-forwarding');
147   browserSection.hidden = !browserSection.querySelector('.open') &&
148                           !browserSection.querySelector('.row') &&
149                           !browserInspector &&
150                           (!icon || icon.hidden);
153 function updateUsernameVisibility(deviceSection) {
154   var users = new Set();
155   var browsers = deviceSection.querySelectorAll('.browser');
157   Array.prototype.forEach.call(browsers, function(browserSection) {
158     if (!browserSection.hidden) {
159       var browserUser = browserSection.querySelector('.browser-user');
160       if (browserUser)
161         users.add(browserUser.textContent);
162     }
163   });
164   var hasSingleUser = users.size <= 1;
166   Array.prototype.forEach.call(browsers, function(browserSection) {
167     var browserUser = browserSection.querySelector('.browser-user');
168     if (browserUser)
169       browserUser.hidden = hasSingleUser;
170   });
173 function populateRemoteTargets(devices) {
174   if (!devices)
175     return;
177   if ($('port-forwarding-config').open) {
178     window.holdDevices = devices;
179     return;
180   }
182   function browserCompare(a, b) {
183     if (a.adbBrowserName != b.adbBrowserName)
184       return a.adbBrowserName < b.adbBrowserName;
185     if (a.adbBrowserVersion != b.adbBrowserVersion)
186       return a.adbBrowserVersion < b.adbBrowserVersion;
187     return a.id < b.id;
188   }
190   function insertBrowser(browserList, browser) {
191     for (var sibling = browserList.firstElementChild; sibling;
192         sibling = sibling.nextElementSibling) {
193       if (browserCompare(browser, sibling)) {
194         browserList.insertBefore(browser, sibling);
195         return;
196       }
197     }
198     browserList.appendChild(browser);
199   }
201   var deviceList = $('devices-list');
202   if (alreadyDisplayed(deviceList, devices))
203     return;
205   function removeObsolete(validIds, section) {
206     if (validIds.indexOf(section.id) < 0)
207       section.remove();
208   }
210   var newDeviceIds = devices.map(function(d) { return d.id });
211   Array.prototype.forEach.call(
212       deviceList.querySelectorAll('.device'),
213       removeObsolete.bind(null, newDeviceIds));
215   $('devices-help').hidden = !!devices.length;
217   for (var d = 0; d < devices.length; d++) {
218     var device = devices[d];
220     var deviceSection = $(device.id);
221     if (!deviceSection) {
222       deviceSection = document.createElement('div');
223       deviceSection.id = device.id;
224       deviceSection.className = 'device';
225       deviceList.appendChild(deviceSection);
227       var deviceHeader = document.createElement('div');
228       deviceHeader.className = 'device-header';
229       deviceSection.appendChild(deviceHeader);
231       var deviceName = document.createElement('div');
232       deviceName.className = 'device-name';
233       deviceHeader.appendChild(deviceName);
235       var deviceSerial = document.createElement('div');
236       deviceSerial.className = 'device-serial';
237       var serial = device.adbSerial.toUpperCase();
238       deviceSerial.textContent = '#' + serial;
239       deviceHeader.appendChild(deviceSerial);
241       if (serial === WEBRTC_SERIAL)
242         deviceHeader.classList.add('hidden');
244       var devicePorts = document.createElement('div');
245       devicePorts.className = 'device-ports';
246       deviceHeader.appendChild(devicePorts);
248       var browserList = document.createElement('div');
249       browserList.className = 'browsers';
250       deviceSection.appendChild(browserList);
252       var authenticating = document.createElement('div');
253       authenticating.className = 'device-auth';
254       deviceSection.appendChild(authenticating);
255     }
257     if (alreadyDisplayed(deviceSection, device))
258       continue;
260     deviceSection.querySelector('.device-name').textContent = device.adbModel;
261     deviceSection.querySelector('.device-auth').textContent =
262         device.adbConnected ? '' : 'Pending authentication: please accept ' +
263           'debugging session on the device.';
265     var browserList = deviceSection.querySelector('.browsers');
266     var newBrowserIds =
267         device.browsers.map(function(b) { return b.id });
268     Array.prototype.forEach.call(
269         browserList.querySelectorAll('.browser'),
270         removeObsolete.bind(null, newBrowserIds));
272     for (var b = 0; b < device.browsers.length; b++) {
273       var browser = device.browsers[b];
274       var majorChromeVersion = browser.adbBrowserChromeVersion;
275       var pageList;
276       var browserSection = $(browser.id);
277       if (browserSection) {
278         pageList = browserSection.querySelector('.pages');
279       } else {
280         browserSection = document.createElement('div');
281         browserSection.id = browser.id;
282         browserSection.className = 'browser';
283         insertBrowser(browserList, browserSection);
285         var browserHeader = document.createElement('div');
286         browserHeader.className = 'browser-header';
288         var browserName = document.createElement('div');
289         browserName.className = 'browser-name';
290         browserHeader.appendChild(browserName);
291         browserName.textContent = browser.adbBrowserName;
292         if (browser.adbBrowserVersion)
293           browserName.textContent += ' (' + browser.adbBrowserVersion + ')';
294         if (browser.adbBrowserUser) {
295           var browserUser = document.createElement('div');
296           browserUser.className = 'browser-user';
297           browserUser.textContent = browser.adbBrowserUser;
298           browserHeader.appendChild(browserUser);
299         }
300         browserSection.appendChild(browserHeader);
302         if (majorChromeVersion >= MIN_VERSION_NEW_TAB) {
303           var newPage = document.createElement('div');
304           newPage.className = 'open';
306           var newPageUrl = document.createElement('input');
307           newPageUrl.type = 'text';
308           newPageUrl.placeholder = 'Open tab with url';
309           newPage.appendChild(newPageUrl);
311           var openHandler = function(sourceId, browserId, input) {
312             sendCommand(
313                 'open', sourceId, browserId, input.value || 'about:blank');
314             input.value = '';
315           }.bind(null, browser.source, browser.id, newPageUrl);
316           newPageUrl.addEventListener('keyup', function(handler, event) {
317             if (event.keyIdentifier == 'Enter' && event.target.value)
318               handler();
319           }.bind(null, openHandler), true);
321           var newPageButton = document.createElement('button');
322           newPageButton.textContent = 'Open';
323           newPage.appendChild(newPageButton);
324           newPageButton.addEventListener('click', openHandler, true);
326           browserHeader.appendChild(newPage);
327         }
329         var portForwardingInfo = document.createElement('div');
330         portForwardingInfo.className = 'used-for-port-forwarding';
331         portForwardingInfo.hidden = true;
332         portForwardingInfo.title = 'This browser is used for port ' +
333             'forwarding. Closing it will drop current connections.';
334         browserHeader.appendChild(portForwardingInfo);
336         if (browserInspector) {
337           var link = document.createElement('span');
338           link.classList.add('action');
339           link.setAttribute('tabindex', 1);
340           link.textContent = browserInspectorTitle;
341           browserHeader.appendChild(link);
342           link.addEventListener(
343               'click',
344               sendCommand.bind(null, 'inspect-browser', browser.source,
345                   browser.id, browserInspector), false);
346         }
348         pageList = document.createElement('div');
349         pageList.className = 'list pages';
350         browserSection.appendChild(pageList);
351       }
353       if (!alreadyDisplayed(browserSection, browser)) {
354         pageList.textContent = '';
355         for (var p = 0; p < browser.pages.length; p++) {
356           var page = browser.pages[p];
357           // Attached targets have no unique id until Chrome 26. For such
358           // targets it is impossible to activate existing DevTools window.
359           page.hasNoUniqueId = page.attached &&
360               majorChromeVersion && majorChromeVersion < MIN_VERSION_TARGET_ID;
361           var row = addTargetToList(page, pageList, ['name', 'url']);
362           if (page['description'])
363             addWebViewDetails(row, page);
364           else
365             addFavicon(row, page);
366           if (majorChromeVersion >= MIN_VERSION_TAB_ACTIVATE) {
367             addActionLink(row, 'focus tab',
368                 sendTargetCommand.bind(null, 'activate', page), false);
369           }
370           if (majorChromeVersion) {
371             addActionLink(row, 'reload',
372                 sendTargetCommand.bind(null, 'reload', page), page.attached);
373           }
374           if (majorChromeVersion >= MIN_VERSION_TAB_CLOSE) {
375             addActionLink(row, 'close',
376                 sendTargetCommand.bind(null, 'close', page), false);
377           }
378         }
379       }
380       updateBrowserVisibility(browserSection);
381     }
382     updateUsernameVisibility(deviceSection);
383   }
386 function addToPagesList(data) {
387   var row = addTargetToList(data, $('pages-list'), ['name', 'url']);
388   addFavicon(row, data);
389   if (data.guests)
390     addGuestViews(row, data.guests);
393 function addToExtensionsList(data) {
394   var row = addTargetToList(data, $('extensions-list'), ['name', 'url']);
395   addFavicon(row, data);
396   if (data.guests)
397     addGuestViews(row, data.guests);
400 function addToAppsList(data) {
401   var row = addTargetToList(data, $('apps-list'), ['name', 'url']);
402   addFavicon(row, data);
403   if (data.guests)
404     addGuestViews(row, data.guests);
407 function addGuestViews(row, guests) {
408   Array.prototype.forEach.call(guests, function(guest) {
409     var guestRow = addTargetToList(guest, row, ['name', 'url']);
410     guestRow.classList.add('guest');
411     addFavicon(guestRow, guest);
412   });
415 function addToWorkersList(data) {
416   var row =
417       addTargetToList(data, $('workers-list'), ['name', 'description', 'url']);
418   addActionLink(row, 'terminate',
419       sendTargetCommand.bind(null, 'close', data), false);
422 function addToServiceWorkersList(data) {
423     var row = addTargetToList(
424         data, $('service-workers-list'), ['name', 'description', 'url']);
425     addActionLink(row, 'terminate',
426         sendTargetCommand.bind(null, 'close', data), false);
429 function addToOthersList(data) {
430   addTargetToList(data, $('others-list'), ['url']);
433 function formatValue(data, property) {
434   var value = data[property];
436   if (property == 'name' && value == '') {
437     value = 'untitled';
438   }
440   var text = value ? String(value) : '';
441   if (text.length > 100)
442     text = text.substring(0, 100) + '\u2026';
444   var div = document.createElement('div');
445   div.textContent = text;
446   div.className = property;
447   return div;
450 function addFavicon(row, data) {
451   var favicon = document.createElement('img');
452   if (data['faviconUrl'])
453     favicon.src = data['faviconUrl'];
454   var propertiesBox = row.querySelector('.properties-box');
455   propertiesBox.insertBefore(favicon, propertiesBox.firstChild);
458 function addWebViewDetails(row, data) {
459   var webview;
460   try {
461     webview = JSON.parse(data['description']);
462   } catch (e) {
463     return;
464   }
465   addWebViewDescription(row, webview);
466   if (data.adbScreenWidth && data.adbScreenHeight)
467     addWebViewThumbnail(
468         row, webview, data.adbScreenWidth, data.adbScreenHeight);
471 function addWebViewDescription(row, webview) {
472   var viewStatus = { visibility: '', position: '', size: '' };
473   if (!webview.empty) {
474     if (webview.attached && !webview.visible)
475       viewStatus.visibility = 'hidden';
476     else if (!webview.attached)
477       viewStatus.visibility = 'detached';
478     viewStatus.size = 'size ' + webview.width + ' \u00d7 ' + webview.height;
479   } else {
480     viewStatus.visibility = 'empty';
481   }
482   if (webview.attached) {
483       viewStatus.position =
484         'at (' + webview.screenX + ', ' + webview.screenY + ')';
485   }
487   var subRow = document.createElement('div');
488   subRow.className = 'subrow webview';
489   if (webview.empty || !webview.attached || !webview.visible)
490     subRow.className += ' invisible-view';
491   if (viewStatus.visibility)
492     subRow.appendChild(formatValue(viewStatus, 'visibility'));
493   if (viewStatus.position)
494     subRow.appendChild(formatValue(viewStatus, 'position'));
495   subRow.appendChild(formatValue(viewStatus, 'size'));
496   var subrowBox = row.querySelector('.subrow-box');
497   subrowBox.insertBefore(subRow, row.querySelector('.actions'));
500 function addWebViewThumbnail(row, webview, screenWidth, screenHeight) {
501   var maxScreenRectSize = 50;
502   var screenRectWidth;
503   var screenRectHeight;
505   var aspectRatio = screenWidth / screenHeight;
506   if (aspectRatio < 1) {
507     screenRectWidth = Math.round(maxScreenRectSize * aspectRatio);
508     screenRectHeight = maxScreenRectSize;
509   } else {
510     screenRectWidth = maxScreenRectSize;
511     screenRectHeight = Math.round(maxScreenRectSize / aspectRatio);
512   }
514   var thumbnail = document.createElement('div');
515   thumbnail.className = 'webview-thumbnail';
516   var thumbnailWidth = 3 * screenRectWidth;
517   var thumbnailHeight = 60;
518   thumbnail.style.width = thumbnailWidth + 'px';
519   thumbnail.style.height = thumbnailHeight + 'px';
521   var screenRect = document.createElement('div');
522   screenRect.className = 'screen-rect';
523   screenRect.style.left = screenRectWidth + 'px';
524   screenRect.style.top = (thumbnailHeight - screenRectHeight) / 2 + 'px';
525   screenRect.style.width = screenRectWidth + 'px';
526   screenRect.style.height = screenRectHeight + 'px';
527   thumbnail.appendChild(screenRect);
529   if (!webview.empty && webview.attached) {
530     var viewRect = document.createElement('div');
531     viewRect.className = 'view-rect';
532     if (!webview.visible)
533       viewRect.classList.add('hidden');
534     function percent(ratio) {
535       return ratio * 100 + '%';
536     }
537     viewRect.style.left = percent(webview.screenX / screenWidth);
538     viewRect.style.top = percent(webview.screenY / screenHeight);
539     viewRect.style.width = percent(webview.width / screenWidth);
540     viewRect.style.height = percent(webview.height / screenHeight);
541     screenRect.appendChild(viewRect);
542   }
544   var propertiesBox = row.querySelector('.properties-box');
545   propertiesBox.insertBefore(thumbnail, propertiesBox.firstChild);
548 function addTargetToList(data, list, properties) {
549   var row = document.createElement('div');
550   row.className = 'row';
551   row.targetId = data.id;
553   var propertiesBox = document.createElement('div');
554   propertiesBox.className = 'properties-box';
555   row.appendChild(propertiesBox);
557   var subrowBox = document.createElement('div');
558   subrowBox.className = 'subrow-box';
559   propertiesBox.appendChild(subrowBox);
561   var subrow = document.createElement('div');
562   subrow.className = 'subrow';
563   subrowBox.appendChild(subrow);
565   for (var j = 0; j < properties.length; j++)
566     subrow.appendChild(formatValue(data, properties[j]));
568   var actionBox = document.createElement('div');
569   actionBox.className = 'actions';
570   subrowBox.appendChild(actionBox);
572   if (!data.hasCustomInspectAction) {
573     addActionLink(row, 'inspect', sendTargetCommand.bind(null, 'inspect', data),
574         data.hasNoUniqueId || data.adbAttachedForeign);
575   }
577   list.appendChild(row);
578   return row;
581 function addActionLink(row, text, handler, opt_disabled) {
582   var link = document.createElement('span');
583   link.classList.add('action');
584   link.setAttribute('tabindex', 1);
585   if (opt_disabled)
586     link.classList.add('disabled');
587   else
588     link.classList.remove('disabled');
590   link.textContent = text;
591   link.addEventListener('click', handler, true);
592   function handleKey(e) {
593     if (e.keyIdentifier == 'Enter' || e.keyIdentifier == 'U+0020') {
594       e.preventDefault();
595       handler();
596     }
597   }
598   link.addEventListener('keydown', handleKey, true);
599   row.querySelector('.actions').appendChild(link);
603 function initSettings() {
604   $('discover-usb-devices-enable').addEventListener('change',
605                                                     enableDiscoverUsbDevices);
607   $('port-forwarding-enable').addEventListener('change', enablePortForwarding);
608   $('port-forwarding-config-open').addEventListener(
609       'click', openPortForwardingConfig);
610   $('port-forwarding-config-close').addEventListener(
611       'click', closePortForwardingConfig);
612   $('port-forwarding-config-done').addEventListener(
613       'click', commitPortForwardingConfig.bind(null, true));
616 function enableDiscoverUsbDevices(event) {
617   sendCommand('set-discover-usb-devices-enabled', event.target.checked);
620 function enablePortForwarding(event) {
621   sendCommand('set-port-forwarding-enabled', event.target.checked);
624 function handleKey(event) {
625   switch (event.keyCode) {
626     case 13:  // Enter
627       if (event.target.nodeName == 'INPUT') {
628         var line = event.target.parentNode;
629         if (!line.classList.contains('fresh') ||
630             line.classList.contains('empty')) {
631           commitPortForwardingConfig(true);
632         } else {
633           commitFreshLineIfValid(true /* select new line */);
634           commitPortForwardingConfig(false);
635         }
636       } else {
637         commitPortForwardingConfig(true);
638       }
639       break;
640   }
643 function openPortForwardingConfig() {
644   loadPortForwardingConfig(window.portForwardingConfig);
646   $('port-forwarding-config').showModal();
647   document.addEventListener('keyup', handleKey);
648   $('port-forwarding-config').onclose = function() {
649     commitPortForwardingConfig(true);
650   };
652   var freshPort = document.querySelector('.fresh .port');
653   if (freshPort)
654     freshPort.focus();
655   else
656     $('port-forwarding-config-done').focus();
659 function closePortForwardingConfig() {
660   if (!$('port-forwarding-config').open)
661     return;
663   $('port-forwarding-config').onclose = null;
664   $('port-forwarding-config').close();
665   document.removeEventListener('keyup', handleKey);
667   if (window.holdDevices) {
668     populateRemoteTargets(window.holdDevices);
669     delete window.holdDevices;
670   }
673 function loadPortForwardingConfig(config) {
674   var list = $('port-forwarding-config-list');
675   list.textContent = '';
676   for (var port in config)
677     list.appendChild(createConfigLine(port, config[port]));
678   list.appendChild(createEmptyConfigLine());
681 function commitPortForwardingConfig(closeConfig) {
682   if (closeConfig)
683     closePortForwardingConfig();
685   commitFreshLineIfValid();
686   var lines = document.querySelectorAll('.port-forwarding-pair');
687   var config = {};
688   for (var i = 0; i != lines.length; i++) {
689     var line = lines[i];
690     var portInput = line.querySelector('.port');
691     var locationInput = line.querySelector('.location');
693     var port = portInput.classList.contains('invalid') ?
694                portInput.lastValidValue :
695                portInput.value;
697     var location = locationInput.classList.contains('invalid') ?
698                    locationInput.lastValidValue :
699                    locationInput.value;
701     if (port && location)
702       config[port] = location;
703   }
704   sendCommand('set-port-forwarding-config', config);
707 function updateDiscoverUsbDevicesEnabled(enabled) {
708   var checkbox = $('discover-usb-devices-enable');
709   checkbox.checked = !!enabled;
710   checkbox.disabled = false;
713 function updatePortForwardingEnabled(enabled) {
714   var checkbox = $('port-forwarding-enable');
715   checkbox.checked = !!enabled;
716   checkbox.disabled = false;
719 function updatePortForwardingConfig(config) {
720   window.portForwardingConfig = config;
721   $('port-forwarding-config-open').disabled = !config;
724 function createConfigLine(port, location) {
725   var line = document.createElement('div');
726   line.className = 'port-forwarding-pair';
728   var portInput = createConfigField(port, 'port', 'Port', validatePort);
729   line.appendChild(portInput);
731   var locationInput = createConfigField(
732       location, 'location', 'IP address and port', validateLocation);
733   line.appendChild(locationInput);
734   locationInput.addEventListener('keydown', function(e) {
735     if (e.keyIdentifier == 'U+0009' &&  // Tab
736         !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey &&
737         line.classList.contains('fresh') &&
738         !line.classList.contains('empty')) {
739       // Tabbing forward on the fresh line, try create a new empty one.
740       if (commitFreshLineIfValid(true))
741         e.preventDefault();
742     }
743   });
745   var lineDelete = document.createElement('div');
746   lineDelete.className = 'close-button';
747   lineDelete.addEventListener('click', function() {
748     var newSelection = line.nextElementSibling;
749     line.parentNode.removeChild(line);
750     selectLine(newSelection);
751   });
752   line.appendChild(lineDelete);
754   line.addEventListener('click', selectLine.bind(null, line));
755   line.addEventListener('focus', selectLine.bind(null, line));
757   checkEmptyLine(line);
759   return line;
762 function validatePort(input) {
763   var match = input.value.match(/^(\d+)$/);
764   if (!match)
765     return false;
766   var port = parseInt(match[1]);
767   if (port < 1024 || 65535 < port)
768     return false;
770   var inputs = document.querySelectorAll('input.port:not(.invalid)');
771   for (var i = 0; i != inputs.length; ++i) {
772     if (inputs[i] == input)
773       break;
774     if (parseInt(inputs[i].value) == port)
775       return false;
776   }
777   return true;
780 function validateLocation(input) {
781   var match = input.value.match(/^([a-zA-Z0-9\.\-_]+):(\d+)$/);
782   if (!match)
783     return false;
784   var port = parseInt(match[2]);
785   return port <= 65535;
788 function createEmptyConfigLine() {
789   var line = createConfigLine('', '');
790   line.classList.add('fresh');
791   return line;
794 function createConfigField(value, className, hint, validate) {
795   var input = document.createElement('input');
796   input.className = className;
797   input.type = 'text';
798   input.placeholder = hint;
799   input.value = value;
800   input.lastValidValue = value;
802   function checkInput() {
803     if (validate(input))
804       input.classList.remove('invalid');
805     else
806       input.classList.add('invalid');
807     if (input.parentNode)
808       checkEmptyLine(input.parentNode);
809   }
810   checkInput();
812   input.addEventListener('keyup', checkInput);
813   input.addEventListener('focus', function() {
814     selectLine(input.parentNode);
815   });
817   input.addEventListener('blur', function() {
818     if (validate(input))
819       input.lastValidValue = input.value;
820   });
822   return input;
825 function checkEmptyLine(line) {
826   var inputs = line.querySelectorAll('input');
827   var empty = true;
828   for (var i = 0; i != inputs.length; i++) {
829     if (inputs[i].value != '')
830       empty = false;
831   }
832   if (empty)
833     line.classList.add('empty');
834   else
835     line.classList.remove('empty');
838 function selectLine(line) {
839   if (line.classList.contains('selected'))
840     return;
841   unselectLine();
842   line.classList.add('selected');
845 function unselectLine() {
846   var line = document.querySelector('.port-forwarding-pair.selected');
847   if (!line)
848     return;
849   line.classList.remove('selected');
850   commitFreshLineIfValid();
853 function commitFreshLineIfValid(opt_selectNew) {
854   var line = document.querySelector('.port-forwarding-pair.fresh');
855   if (line.querySelector('.invalid'))
856     return false;
857   line.classList.remove('fresh');
858   var freshLine = createEmptyConfigLine();
859   line.parentNode.appendChild(freshLine);
860   if (opt_selectNew)
861     freshLine.querySelector('.port').focus();
862   return true;
865 function populatePortStatus(devicesStatusMap) {
866   for (var deviceId in devicesStatusMap) {
867     if (!devicesStatusMap.hasOwnProperty(deviceId))
868       continue;
869     var deviceStatus = devicesStatusMap[deviceId];
870     var deviceStatusMap = deviceStatus.ports;
872     var deviceSection = $(deviceId);
873     if (!deviceSection)
874       continue;
876     var devicePorts = deviceSection.querySelector('.device-ports');
877     if (alreadyDisplayed(devicePorts, deviceStatus))
878       continue;
880     devicePorts.textContent = '';
881     for (var port in deviceStatusMap) {
882       if (!deviceStatusMap.hasOwnProperty(port))
883         continue;
885       var status = deviceStatusMap[port];
886       var portIcon = document.createElement('div');
887       portIcon.className = 'port-icon';
888       // status === 0 is the default (connected) state.
889       if (status === -1 || status === -2)
890         portIcon.classList.add('transient');
891       else if (status < 0)
892         portIcon.classList.add('error');
893       devicePorts.appendChild(portIcon);
895       var portNumber = document.createElement('div');
896       portNumber.className = 'port-number';
897       portNumber.textContent = ':' + port;
898       devicePorts.appendChild(portNumber);
899     }
901     function updatePortForwardingInfo(browserSection) {
902       var icon = browserSection.querySelector('.used-for-port-forwarding');
903       if (icon)
904         icon.hidden = (browserSection.id !== deviceStatus.browserId);
905       updateBrowserVisibility(browserSection);
906     }
908     Array.prototype.forEach.call(
909         deviceSection.querySelectorAll('.browser'), updatePortForwardingInfo);
911     updateUsernameVisibility(deviceSection);
912   }
914   function clearPorts(deviceSection) {
915     if (deviceSection.id in devicesStatusMap)
916       return;
917     deviceSection.querySelector('.device-ports').textContent = '';
918   }
920   Array.prototype.forEach.call(
921       document.querySelectorAll('.device'), clearPorts);
924 document.addEventListener('DOMContentLoaded', onload);
926 window.addEventListener('hashchange', onHashChange);