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 cr.define('apps_dev_tool', function() {
8 // The list of all packed/unpacked apps and extensions.
11 // The list of all packed apps.
12 var packedAppList = [];
14 // The list of all unpacked apps.
15 var unpackedAppList = [];
17 // The list of all packed extensions.
18 var packedExtensionList = [];
20 // The list of all unpacked extensions.
21 var unpackedExtensionList = [];
23 // Highlight animation is in progress in apps or extensions list.
24 var animating = false;
26 // If an update of the list of apps/extensions happened while animation was
27 // in progress, we'll need to update the list at the end of the animation.
28 var needsReloadAppDisplay = false;
30 /** const*/ var AppsDevTool = apps_dev_tool.AppsDevTool;
33 * @param {string} a first string.
34 * @param {string} b second string.
35 * @return {number} 1, 0, -1 if |a| is lexicographically greater, equal or
36 * lesser than |b| respectively.
38 function compare(a, b) {
39 return a > b ? 1 : (a == b ? 0 : -1);
43 * Returns a translated string.
45 * Wrapper function to make dealing with translated strings more concise.
46 * Equivalent to localStrings.getString(id).
48 * @param {string} id The id of the string to return.
49 * @return {string} The translated string.
52 return loadTimeData.getString(id);
56 * compares strings |app1| and |app2| (case insensitive).
57 * @param {string} app1 first app_name.
58 * @param {string} app2 second app_name.
60 function compareByName(app1, app2) {
61 return compare(app1.name.toLowerCase(), app2.name.toLowerCase());
67 function reloadAppDisplay() {
68 var extensions = new ItemsList($('extensions-tab'), packedExtensionList,
69 unpackedExtensionList);
70 var apps = new ItemsList($('apps-tab'), packedAppList, unpackedAppList);
71 extensions.showItemNodes();
76 * Applies the given |filter| to the items list.
77 * @param {string} filter Regular expression to that will be used to
78 * match the name of the items we want to show.
80 function rebuildAppList(filter) {
83 packedExtensionList = [];
84 unpackedExtensionList = [];
86 for (var i = 0; i < completeList.length; i++) {
87 var item = completeList[i];
88 if (filter && item.name.toLowerCase().search(filter) < 0)
92 unpackedAppList.push(item);
94 packedAppList.push(item);
97 unpackedExtensionList.push(item);
99 packedExtensionList.push(item);
105 * Create item nodes from the metadata.
108 function ItemsList(tabNode, packedItems, unpackedItems) {
109 this.packedItems_ = packedItems;
110 this.unpackedItems_ = unpackedItems;
111 this.tabNode_ = tabNode;
112 assert(this.tabNode_);
115 ItemsList.prototype = {
118 * |packedItems_| holds the metadata of packed apps or extensions.
119 * @type {!Array.<!Object>}
125 * |unpackedItems_| holds the metadata of unpacked apps or extensions.
126 * @type {!Array.<!Object>}
132 * |tabNode_| html element holding the tab containing the list of packed
133 * and unpacked items.
134 * @type {!HTMLElement}
140 * Creates all items from scratch.
142 showItemNodes: function() {
143 // Don't reset the content until the animation is finished.
145 needsReloadAppDisplay = true;
149 var packedItemsList = this.tabNode_.querySelector('.packed-list .items');
150 var unpackedItemsList = this.tabNode_.querySelector(
151 '.unpacked-list .items');
152 packedItemsList.innerHTML = '';
153 unpackedItemsList.innerHTML = '';
155 // Iterate over the items and add each item to the list.
156 this.tabNode_.classList.toggle('empty-item-list',
157 this.packedItems_.length == 0 &&
158 this.unpackedItems_.length == 0);
160 // Iterate over the items in the packed items and add each item to the
162 this.tabNode_.querySelector('.packed-list').classList.toggle(
163 'empty-item-list', this.packedItems_.length == 0);
164 for (var i = 0; i < this.packedItems_.length; ++i) {
165 packedItemsList.appendChild(this.createNode_(this.packedItems_[i]));
168 // Iterate over the items in the unpacked items and add each item to the
170 this.tabNode_.querySelector('.unpacked-list').classList.toggle(
171 'empty-item-list', this.unpackedItems_.length == 0);
172 for (var i = 0; i < this.unpackedItems_.length; ++i) {
173 unpackedItemsList.appendChild(this.createNode_(this.unpackedItems_[i]));
178 * Synthesizes and initializes an HTML element for the item metadata
180 * @param {!Object} item A dictionary of item metadata.
181 * @return {!Node} The newly created node.
184 createNode_: function(item) {
185 var template = $('template-collection').querySelector(
186 '.extension-list-item-wrapper');
187 var node = template.cloneNode(true);
191 node.classList.add('inactive-extension');
193 node.querySelector('.extension-disabled').hidden = item.enabled;
195 if (!item.may_disable)
196 node.classList.add('may-not-disable');
198 var itemNode = node.querySelector('.extension-list-item');
199 itemNode.style.backgroundImage = 'url(' + item.icon_url + ')';
201 var title = node.querySelector('.extension-title');
202 title.textContent = item.name;
203 title.onclick = function() {
205 ItemsList.launchApp(item.id);
208 var version = node.querySelector('.extension-version');
209 version.textContent = item.version;
211 var description = node.querySelector('.extension-description span');
212 description.textContent = item.description;
214 // The 'allow in incognito' checkbox.
215 this.setAllowIncognitoCheckbox_(item, node);
217 // The 'allow file:// access' checkbox.
218 if (item.wants_file_access)
219 this.setAllowFileAccessCheckbox_(item, node);
221 // The 'Options' checkbox.
222 if (item.enabled && item.options_url) {
223 var options = node.querySelector('.options-link');
224 options.href = item.options_url;
225 options.hidden = false;
228 // The 'Permissions' link.
229 this.setPermissionsLink_(item, node);
231 // The 'View in Web Store/View Web Site' link.
232 if (item.homepage_url)
233 this.setWebstoreLink_(item, node);
235 // The 'Reload' checkbox.
236 if (item.allow_reload)
237 this.setReloadLink_(item, node);
239 if (item.type == 'packaged_app') {
240 // The 'Launch' link.
241 var launch = node.querySelector('.launch-link');
242 launch.addEventListener('click', function(e) {
243 ItemsList.launchApp(item.id);
245 launch.hidden = false;
247 // The terminated reload link.
248 if (!item.terminated)
249 this.setEnabledCheckbox_(item, node);
251 this.setTerminatedReloadLink_(item, node);
253 // Set delete button handler.
254 this.setDeleteButton_(item, node);
256 // First get the item id.
257 var idLabel = node.querySelector('.extension-id');
258 idLabel.textContent = ' ' + item.id;
260 // Set the path and show the pack button, if provided by unpacked
262 if (item.is_unpacked) {
263 var loadPath = node.querySelector('.load-path');
264 loadPath.hidden = false;
265 loadPath.querySelector('span:nth-of-type(2)').textContent =
267 this.setPackButton_(item, node);
270 // Then the 'managed, cannot uninstall/disable' message.
271 if (!item.may_disable)
272 node.querySelector('.managed-message').hidden = false;
274 // The install warnings.
275 if (item.install_warnings.length > 0) {
276 var panel = node.querySelector('.install-warnings');
277 panel.hidden = false;
278 var list = panel.querySelector('ul');
279 item.install_warnings.forEach(function(warning) {
280 var li = document.createElement('li');
281 li.textContent = warning.message;
282 list.appendChild(li);
286 this.setActiveViews_(item, node);
292 * Sets the webstore link.
293 * @param {!Object} item A dictionary of item metadata.
294 * @param {!HTMLElement} el HTML element containing all items.
297 setWebstoreLink_: function(item, el) {
298 var siteLink = el.querySelector('.site-link');
299 siteLink.href = item.homepage_url;
300 siteLink.textContent = str(item.homepageProvided ?
301 'extensionSettingsVisitWebsite' : 'extensionSettingsVisitWebStore');
302 siteLink.hidden = false;
303 siteLink.target = '_blank';
307 * Sets the reload link handler.
308 * @param {!Object} item A dictionary of item metadata.
309 * @param {!HTMLElement} el HTML element containing all items.
312 setReloadLink_: function(item, el) {
313 var reload = el.querySelector('.reload-link');
314 reload.addEventListener('click', function(e) {
315 chrome.developerPrivate.reload(item.id, function() {
316 ItemsList.loadItemsInfo();
319 reload.hidden = false;
323 * Sets the terminated reload link handler.
324 * @param {!Object} item A dictionary of item metadata.
325 * @param {!HTMLElement} el HTML element containing all items.
328 setTerminatedReloadLink_: function(item, el) {
329 var terminatedReload = el.querySelector('.terminated-reload-link');
330 terminatedReload.addEventListener('click', function(e) {
331 chrome.developerPrivate.reload(item.id, function() {
332 ItemsList.loadItemsInfo();
335 terminatedReload.hidden = false;
339 * Sets the permissions link handler.
340 * @param {!Object} item A dictionary of item metadata.
341 * @param {!HTMLElement} el HTML element containing all items.
344 setPermissionsLink_: function(item, el) {
345 var permissions = el.querySelector('.permissions-link');
346 permissions.addEventListener('click', function(e) {
347 chrome.developerPrivate.showPermissionsDialog(item.id);
352 * Sets the pack button handler.
353 * @param {!Object} item A dictionary of item metadata.
354 * @param {!HTMLElement} el HTML element containing all items.
357 setPackButton_: function(item, el) {
358 var packButton = el.querySelector('.pack-link');
359 packButton.addEventListener('click', function(e) {
361 $('pack-heading').textContent =
362 loadTimeData.getString('packAppHeading');
363 $('pack-title').textContent =
364 loadTimeData.getString('packAppOverlay');
366 $('pack-heading').textContent =
367 loadTimeData.getString('packExtensionHeading');
368 $('pack-title').textContent =
369 loadTimeData.getString('packExtensionOverlay');
371 $('item-root-dir').value = item.path;
372 AppsDevTool.showOverlay($('packItemOverlay'));
374 packButton.hidden = false;
378 * Shows the delete confirmation dialog and will trigger the deletion
379 * if the user confirms deletion.
380 * @param {!Object} item Information about the item being deleted.
383 showDeleteConfirmationDialog: function(item) {
386 message = loadTimeData.getString('deleteConfirmationMessageApp');
388 message = loadTimeData.getString('deleteConfirmationMessageExtension');
390 alertOverlay.setValues(
391 loadTimeData.getString('deleteConfirmationTitle'),
393 loadTimeData.getString('deleteConfirmationDeleteButton'),
394 loadTimeData.getString('cancel'),
396 AppsDevTool.showOverlay(null);
397 var options = {showConfirmDialog: false};
398 chrome.management.uninstall(item.id, options);
401 AppsDevTool.showOverlay(null);
404 AppsDevTool.showOverlay($('alertOverlay'));
408 * Sets the delete button handler.
409 * @param {!Object} item A dictionary of item metadata.
410 * @param {!HTMLElement} el HTML element containing all items.
413 setDeleteButton_: function(item, el) {
414 var deleteLink = el.querySelector('.delete-link');
415 deleteLink.addEventListener('click', function(e) {
416 this.showDeleteConfirmationDialog(item);
421 * Sets the handler for enable checkbox.
422 * @param {!Object} item A dictionary of item metadata.
423 * @param {!HTMLElement} el HTML element containing all items.
426 setEnabledCheckbox_: function(item, el) {
427 var enable = el.querySelector('.enable-checkbox');
428 enable.hidden = false;
429 enable.querySelector('input').disabled = !item.may_disable;
431 if (item.may_disable) {
432 enable.addEventListener('click', function(e) {
433 chrome.developerPrivate.enable(
434 item.id, !!e.target.checked, function() {
435 ItemsList.loadItemsInfo();
440 enable.querySelector('input').checked = item.enabled;
444 * Sets the handler for the allow_file_access checkbox.
445 * @param {!Object} item A dictionary of item metadata.
446 * @param {!HTMLElement} el HTML element containing all items.
449 setAllowFileAccessCheckbox_: function(item, el) {
450 var fileAccess = el.querySelector('.file-access-control');
451 fileAccess.addEventListener('click', function(e) {
452 chrome.developerPrivate.allowFileAccess(item.id, !!e.target.checked);
454 fileAccess.querySelector('input').checked = item.allow_file_access;
455 fileAccess.hidden = false;
459 * Sets the handler for the allow_incognito checkbox.
460 * @param {!Object} item A dictionary of item metadata.
461 * @param {!HTMLElement} el HTML element containing all items.
464 setAllowIncognitoCheckbox_: function(item, el) {
465 if (item.allow_incognito) {
466 var incognito = el.querySelector('.incognito-control');
467 incognito.addEventListener('change', function(e) {
468 chrome.developerPrivate.allowIncognito(
469 item.id, !!e.target.checked, function() {
470 ItemsList.loadItemsInfo();
473 incognito.querySelector('input').checked = item.incognito_enabled;
474 incognito.hidden = false;
479 * Sets the active views link of an item. Clicking on the link
480 * opens devtools window to inspect.
481 * @param {!Object} item A dictionary of item metadata.
482 * @param {!HTMLElement} el HTML element containing all items.
485 setActiveViews_: function(item, el) {
486 if (!item.views.length)
489 var activeViews = el.querySelector('.active-views');
490 activeViews.hidden = false;
491 var link = activeViews.querySelector('a');
493 item.views.forEach(function(view, i) {
494 var displayName = view.generatedBackgroundPage ?
495 str('backgroundPage') : view.path;
497 displayName + (view.incognito ? ' ' + str('viewIncognito') : '') +
498 (view.render_process_id == -1 ? ' ' + str('viewInactive') : '');
499 link.textContent = label;
500 link.addEventListener('click', function(e) {
501 // Opens the devtools inspect window for the page.
502 chrome.developerPrivate.inspect({
503 extension_id: String(item.id),
504 render_process_id: String(view.render_process_id),
505 render_view_id: String(view.render_view_id),
506 incognito: view.incognito,
510 if (i < item.views.length - 1) {
511 link = link.cloneNode(true);
512 activeViews.appendChild(link);
519 * Rebuilds the item list and reloads the app on every search input.
521 ItemsList.onSearchInput = function() {
522 // Escape regexp special chars (e.g. ^, $, etc.).
523 rebuildAppList($('search').value.toLowerCase().replace(
524 /[-\/\\^$*+?.()|[\]{}]/g, '\\$&'));
529 * Fetches items info and reloads the app.
530 * @param {Function=} opt_callback An optional callback to be run when
531 * reloading is finished.
533 ItemsList.loadItemsInfo = function(callback) {
534 chrome.developerPrivate.getItemsInfo(true, true, function(info) {
535 completeList = info.sort(compareByName);
536 ItemsList.onSearchInput();
537 assert(/undefined|function/.test(typeof callback));
544 * Launches the item with id |id|.
545 * @param {string} id Item ID.
547 ItemsList.launchApp = function(id) {
548 chrome.management.launchApp(id, function() {
549 ItemsList.loadItemsInfo();
554 * Selects the unpacked apps / extensions tab, scrolls to the app /extension
555 * with the given |id| and expand its details.
556 * @param {string} id Identifier of the app / extension.
558 ItemsList.makeUnpackedExtensionVisible = function(id) {
559 // Find which tab contains the item.
560 var tabbox = document.querySelector('tabbox');
562 // Select the correct tab.
567 var tabNode = findAncestor(node, function(el) {
568 return el.tagName == 'TABPANEL';
570 tabbox.selectedIndex = tabNode == $('apps-tab') ? 0 : 1;
572 // Highlights the item.
574 needsReloadAppDisplay = true;
575 node.classList.add('highlighted');
576 // Show highlighted item for 1 sec.
577 setTimeout(function() {
578 node.style.backgroundColor = 'rgba(255, 255, 128, 0)';
579 // Wait for fade animation to happen.
580 node.addEventListener('webkitTransitionEnd', function f(e) {
581 assert(e.propertyName == 'background-color');
584 if (needsReloadAppDisplay)
587 node.removeEventListener('webkitTransitionEnd', f);
591 // Scroll relatively to the position of the first item.
592 var header = tabNode.querySelector('.unpacked-list .list-header');
593 if (node.offsetTop - header.offsetTop < tabNode.scrollTop) {
594 // Some padding between the top edge and the node is already provided
595 // by the HTML layout.
596 tabNode.scrollTop = node.offsetTop - header.offsetTop;
597 } else if (node.offsetTop + node.offsetHeight > tabNode.scrollTop +
598 tabNode.offsetHeight + 20) {
599 // Adds padding of 20px between the bottom edge and the bottom of the
601 tabNode.scrollTop = node.offsetTop + node.offsetHeight -
602 tabNode.offsetHeight + 20;
607 ItemsList: ItemsList,