blob: 1fc42a70c685d5be5d35f6ac6765b6021f29f75a [file] [log] [blame]
tsergeant77365182017-05-05 04:02:331// Copyright 2017 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
tsergeant2db36262017-05-15 02:47:535/**
6 * @fileoverview Element which shows context menus and handles keyboard
7 * shortcuts.
8 */
9cr.define('bookmarks', function() {
tsergeant77365182017-05-05 04:02:3310
tsergeant2db36262017-05-15 02:47:5311 var CommandManager = Polymer({
12 is: 'bookmarks-command-manager',
tsergeant77365182017-05-05 04:02:3313
tsergeant2db36262017-05-15 02:47:5314 behaviors: [
15 bookmarks.StoreClient,
16 ],
17
18 properties: {
19 /** @private {!Array<Command>} */
20 menuCommands_: {
21 type: Array,
22 value: function() {
23 return [
24 Command.EDIT,
25 Command.COPY,
26 Command.DELETE,
27 // <hr>
28 Command.OPEN_NEW_TAB,
29 Command.OPEN_NEW_WINDOW,
30 Command.OPEN_INCOGNITO,
31 ];
32 },
tsergeant13a466462017-05-15 01:21:0333 },
tsergeant2db36262017-05-15 02:47:5334
tsergeant2437f992017-06-13 23:54:2935 /** @private {Set<string>} */
calamity64e2012a2017-06-21 09:57:1436 menuIds_: {
37 type: Object,
38 observer: 'onMenuIdsChanged_',
39 },
40
41 /** @private */
42 hasAnySublabel_: {
43 type: Boolean,
44 reflectToAttribute: true,
45 },
tsergeant2437f992017-06-13 23:54:2946
47 /** @private */
48 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0349 },
50
tsergeant2db36262017-05-15 02:47:5351 attached: function() {
52 assert(CommandManager.instance_ == null);
53 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3354
tsergeant2437f992017-06-13 23:54:2955 this.watch('globalCanEdit_', function(state) {
56 return state.prefs.canEdit;
57 });
58 this.updateFromStore();
59
tsergeant2db36262017-05-15 02:47:5360 /** @private {function(!Event)} */
61 this.boundOnOpenItemMenu_ = this.onOpenItemMenu_.bind(this);
62 document.addEventListener('open-item-menu', this.boundOnOpenItemMenu_);
tsergeantfad224f2017-05-05 04:42:0963
calamityefe477352017-06-07 06:44:5864 /** @private {function()} */
65 this.boundOnCommandUndo_ = function() {
66 this.handle(Command.UNDO, new Set());
67 }.bind(this);
68 document.addEventListener('command-undo', this.boundOnCommandUndo_);
69
tsergeant2db36262017-05-15 02:47:5370 /** @private {function(!Event)} */
71 this.boundOnKeydown_ = this.onKeydown_.bind(this);
72 document.addEventListener('keydown', this.boundOnKeydown_);
tsergeantfad224f2017-05-05 04:42:0973
tsergeant0292e51a2017-06-16 03:44:3574 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */
tsergeant2db36262017-05-15 02:47:5375 this.shortcuts_ = {};
tsergeant0292e51a2017-06-16 03:44:3576
77 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
78 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
79 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
80
81 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o');
82 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
83 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
84
85 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
86 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4187
88 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
89 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeant2db36262017-05-15 02:47:5390 },
tsergeant77365182017-05-05 04:02:3391
tsergeant2db36262017-05-15 02:47:5392 detached: function() {
93 CommandManager.instance_ = null;
94 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_);
calamityefe477352017-06-07 06:44:5895 document.removeEventListener('command-undo', this.boundOnCommandUndo_);
tsergeant2db36262017-05-15 02:47:5396 document.removeEventListener('keydown', this.boundOnKeydown_);
97 },
tsergeant77365182017-05-05 04:02:3398
tsergeant2db36262017-05-15 02:47:5399 /**
100 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28101 * Commands will execute on |items| if given, or on the currently selected
102 * items.
tsergeant2db36262017-05-15 02:47:53103 * @param {number} x
104 * @param {number} y
tsergeanta274e0412017-06-16 05:22:28105 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53106 */
tsergeanta274e0412017-06-16 05:22:28107 openCommandMenuAtPosition: function(x, y, items) {
108 this.menuIds_ = items || this.getState().selection.items;
tsergeant2db36262017-05-15 02:47:53109 /** @type {!CrActionMenuElement} */ (this.$.dropdown)
110 .showAtPosition({top: y, left: x});
111 },
tsergeant77365182017-05-05 04:02:33112
tsergeant2db36262017-05-15 02:47:53113 /**
114 * Display the command context menu positioned to cover the |target|
115 * element. Commands will execute on the currently selected items.
116 * @param {!Element} target
117 */
118 openCommandMenuAtElement: function(target) {
119 this.menuIds_ = this.getState().selection.items;
120 /** @type {!CrActionMenuElement} */ (this.$.dropdown).showAt(target);
121 },
tsergeant77365182017-05-05 04:02:33122
tsergeant2db36262017-05-15 02:47:53123 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02124 this.menuIds_ = new Set();
tsergeant2db36262017-05-15 02:47:53125 /** @type {!CrActionMenuElement} */ (this.$.dropdown).close();
126 },
tsergeant77365182017-05-05 04:02:33127
tsergeant2db36262017-05-15 02:47:53128 ////////////////////////////////////////////////////////////////////////////
129 // Command handlers:
tsergeant77365182017-05-05 04:02:33130
tsergeant2db36262017-05-15 02:47:53131 /**
132 * Determine if the |command| can be executed with the given |itemIds|.
133 * Commands which appear in the context menu should be implemented
134 * separately using `isCommandVisible_` and `isCommandEnabled_`.
135 * @param {Command} command
136 * @param {!Set<string>} itemIds
137 * @return {boolean}
138 */
139 canExecute: function(command, itemIds) {
tsergeant6c5ad90a2017-05-19 14:12:34140 switch (command) {
141 case Command.OPEN:
142 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58143 case Command.UNDO:
144 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29145 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41146 case Command.SELECT_ALL:
147 case Command.DESELECT_ALL:
148 return true;
tsergeant6c5ad90a2017-05-19 14:12:34149 default:
150 return this.isCommandVisible_(command, itemIds) &&
151 this.isCommandEnabled_(command, itemIds);
152 }
tsergeant2db36262017-05-15 02:47:53153 },
tsergeant13a466462017-05-15 01:21:03154
tsergeant2db36262017-05-15 02:47:53155 /**
156 * @param {Command} command
157 * @param {!Set<string>} itemIds
158 * @return {boolean} True if the command should be visible in the context
159 * menu.
160 */
161 isCommandVisible_: function(command, itemIds) {
162 switch (command) {
163 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29164 return itemIds.size == 1 && this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53165 case Command.COPY:
tsergeant2437f992017-06-13 23:54:29166 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53167 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29168 return itemIds.size > 0 && this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53169 case Command.OPEN_NEW_TAB:
170 case Command.OPEN_NEW_WINDOW:
171 case Command.OPEN_INCOGNITO:
172 return itemIds.size > 0;
173 default:
174 return false;
tsergeantf1ffc892017-05-05 07:43:43175 }
tsergeant2db36262017-05-15 02:47:53176 },
tsergeantf1ffc892017-05-05 07:43:43177
tsergeant2db36262017-05-15 02:47:53178 /**
179 * @param {Command} command
180 * @param {!Set<string>} itemIds
181 * @return {boolean} True if the command should be clickable in the context
182 * menu.
183 */
184 isCommandEnabled_: function(command, itemIds) {
185 switch (command) {
tsergeant2437f992017-06-13 23:54:29186 case Command.EDIT:
187 case Command.DELETE:
188 var state = this.getState();
189 return !this.containsMatchingNode_(itemIds, function(node) {
190 return !bookmarks.util.canEditNode(state, node.id);
191 });
tsergeant2db36262017-05-15 02:47:53192 case Command.OPEN_NEW_TAB:
193 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53194 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02195 case Command.OPEN_INCOGNITO:
196 return this.expandUrls_(itemIds).length > 0 &&
197 this.getState().prefs.incognitoAvailability !=
198 IncognitoAvailability.DISABLED;
tsergeant2db36262017-05-15 02:47:53199 default:
200 return true;
201 }
202 },
tsergeant13a466462017-05-15 01:21:03203
tsergeant2db36262017-05-15 02:47:53204 /**
205 * @param {Command} command
206 * @param {!Set<string>} itemIds
207 */
208 handle: function(command, itemIds) {
tsergeant0292e51a2017-06-16 03:44:35209 var state = this.getState();
tsergeant2db36262017-05-15 02:47:53210 switch (command) {
211 case Command.EDIT:
212 var id = Array.from(itemIds)[0];
213 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35214 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53215 break;
216 case Command.COPY:
217 var idList = Array.from(itemIds);
218 chrome.bookmarkManagerPrivate.copy(idList, function() {
calamityefe477352017-06-07 06:44:58219 bookmarks.ToastManager.getInstance().show(
220 loadTimeData.getString('toastUrlCopied'), false);
tsergeant2db36262017-05-15 02:47:53221 });
222 break;
223 case Command.DELETE:
calamityefe477352017-06-07 06:44:58224 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
tsergeant0292e51a2017-06-16 03:44:35225 var title = state.nodes[idList[0]].title;
calamitye0917642017-06-09 07:34:35226 var labelPromise = cr.sendWithPromise(
227 'getPluralString', 'toastItemsDeleted', idList.length);
calamityefe477352017-06-07 06:44:58228 chrome.bookmarkManagerPrivate.removeTrees(idList, function() {
229 labelPromise.then(function(label) {
calamitye0917642017-06-09 07:34:35230 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
231 .map(function(p) {
232 // Make the bookmark name collapsible.
233 p.collapsible = !!p.arg;
234 return p;
235 });
236 bookmarks.ToastManager.getInstance().showForStringPieces(
237 pieces, true);
238 }.bind(this));
calamityefe477352017-06-07 06:44:58239 }.bind(this));
tsergeant2db36262017-05-15 02:47:53240 break;
calamity2d4b5502017-05-29 03:57:58241 case Command.UNDO:
242 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58243 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58244 break;
245 case Command.REDO:
246 chrome.bookmarkManagerPrivate.redo();
247 break;
tsergeant2db36262017-05-15 02:47:53248 case Command.OPEN_NEW_TAB:
249 case Command.OPEN_NEW_WINDOW:
250 case Command.OPEN_INCOGNITO:
251 this.openUrls_(this.expandUrls_(itemIds), command);
252 break;
tsergeant6c5ad90a2017-05-19 14:12:34253 case Command.OPEN:
254 var isFolder = itemIds.size == 1 &&
255 this.containsMatchingNode_(itemIds, function(node) {
256 return !node.url;
257 });
258 if (isFolder) {
259 var folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35260 this.dispatch(
261 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34262 } else {
263 this.openUrls_(this.expandUrls_(itemIds), command);
264 }
265 break;
tsergeant679159f2017-06-16 06:58:41266 case Command.SELECT_ALL:
267 var displayedIds = bookmarks.util.getDisplayedList(state);
268 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
269 break;
270 case Command.DESELECT_ALL:
271 this.dispatch(bookmarks.actions.deselectItems());
272 break;
tsergeant6c5ad90a2017-05-19 14:12:34273 default:
274 assert(false);
tsergeant2db36262017-05-15 02:47:53275 }
276 },
tsergeant13a466462017-05-15 01:21:03277
tsergeant6c3a6df2017-06-06 23:53:02278 /**
tsergeant0292e51a2017-06-16 03:44:35279 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02280 * @param {!Set<string>} itemIds
281 * @return {boolean} True if the event was handled, triggering a keyboard
282 * shortcut.
283 */
284 handleKeyEvent: function(e, itemIds) {
285 for (var commandName in this.shortcuts_) {
286 var shortcut = this.shortcuts_[commandName];
tsergeant0292e51a2017-06-16 03:44:35287 if (shortcut.matchesEvent(e) && this.canExecute(commandName, itemIds)) {
tsergeant6c3a6df2017-06-06 23:53:02288 this.handle(commandName, itemIds);
289
290 e.stopPropagation();
291 e.preventDefault();
292 return true;
293 }
294 }
295
296 return false;
297 },
298
tsergeant2db36262017-05-15 02:47:53299 ////////////////////////////////////////////////////////////////////////////
300 // Private functions:
301
302 /**
tsergeant0292e51a2017-06-16 03:44:35303 * Register a keyboard shortcut for a command.
304 * @param {Command} command Command that the shortcut will trigger.
305 * @param {string} shortcut Keyboard shortcut, using the syntax of
306 * cr/ui/command.js.
307 * @param {string=} macShortcut If set, enables a replacement shortcut for
308 * Mac.
309 */
310 addShortcut_: function(command, shortcut, macShortcut) {
311 var shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
312 this.shortcuts_[command] = new cr.ui.KeyboardShortcutList(shortcut);
313 },
314
315 /**
tsergeant2db36262017-05-15 02:47:53316 * Minimize the set of |itemIds| by removing any node which has an ancestor
317 * node already in the set. This ensures that instead of trying to delete
318 * both a node and its descendant, we will only try to delete the topmost
319 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
320 * call.
321 * @param {!Set<string>} itemIds
322 * @return {!Set<string>}
323 */
324 minimizeDeletionSet_: function(itemIds) {
325 var minimizedSet = new Set();
326 var nodes = this.getState().nodes;
327 itemIds.forEach(function(itemId) {
328 var currentId = itemId;
329 while (currentId != ROOT_NODE_ID) {
330 currentId = assert(nodes[currentId].parentId);
331 if (itemIds.has(currentId))
332 return;
333 }
334 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03335 });
tsergeant2db36262017-05-15 02:47:53336 return minimizedSet;
337 },
tsergeant13a466462017-05-15 01:21:03338
tsergeant2db36262017-05-15 02:47:53339 /**
340 * @param {!Array<string>} urls
341 * @param {Command} command
342 * @private
343 */
344 openUrls_: function(urls, command) {
345 assert(
tsergeant6c5ad90a2017-05-19 14:12:34346 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53347 command == Command.OPEN_NEW_WINDOW ||
348 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03349
tsergeant2db36262017-05-15 02:47:53350 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09351 return;
tsergeantfad224f2017-05-05 04:42:09352
tsergeant2db36262017-05-15 02:47:53353 var incognito = command == Command.OPEN_INCOGNITO;
354 if (command == Command.OPEN_NEW_WINDOW || incognito) {
355 chrome.windows.create({url: urls, incognito: incognito});
356 } else {
tsergeant6c5ad90a2017-05-19 14:12:34357 if (command == Command.OPEN)
358 chrome.tabs.create({url: urls.shift(), active: true});
tsergeant2db36262017-05-15 02:47:53359 urls.forEach(function(url) {
360 chrome.tabs.create({url: url, active: false});
tsergeant13a466462017-05-15 01:21:03361 });
tsergeant2db36262017-05-15 02:47:53362 }
363 },
tsergeant77365182017-05-05 04:02:33364
tsergeant2db36262017-05-15 02:47:53365 /**
366 * Returns all URLs in the given set of nodes and their immediate children.
367 * Note that these will be ordered by insertion order into the |itemIds|
368 * set, and that it is possible to duplicate a URL by passing in both the
369 * parent ID and child ID.
370 * @param {!Set<string>} itemIds
371 * @return {!Array<string>}
372 * @private
373 */
374 expandUrls_: function(itemIds) {
375 var urls = [];
376 var nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03377
tsergeant2db36262017-05-15 02:47:53378 itemIds.forEach(function(id) {
379 var node = nodes[id];
380 if (node.url) {
381 urls.push(node.url);
382 } else {
383 node.children.forEach(function(childId) {
384 var childNode = nodes[childId];
385 if (childNode.url)
386 urls.push(childNode.url);
387 });
388 }
389 });
tsergeant13a466462017-05-15 01:21:03390
tsergeant2db36262017-05-15 02:47:53391 return urls;
392 },
393
394 /**
395 * @param {!Set<string>} itemIds
396 * @param {function(BookmarkNode):boolean} predicate
397 * @return {boolean} True if any node in |itemIds| returns true for
398 * |predicate|.
399 */
400 containsMatchingNode_: function(itemIds, predicate) {
401 var nodes = this.getState().nodes;
402
403 return Array.from(itemIds).some(function(id) {
404 return predicate(nodes[id]);
405 });
406 },
407
408 /**
tsergeant2437f992017-06-13 23:54:29409 * @param {!Set<string>} itemIds
410 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
411 * node.
412 */
413 isSingleBookmark_: function(itemIds) {
414 return itemIds.size == 1 &&
415 this.containsMatchingNode_(itemIds, function(node) {
416 return !!node.url;
417 });
418 },
419
420 /**
tsergeant2db36262017-05-15 02:47:53421 * @param {Event} e
422 * @private
423 */
424 onOpenItemMenu_: function(e) {
425 if (e.detail.targetElement) {
426 this.openCommandMenuAtElement(e.detail.targetElement);
427 } else {
428 this.openCommandMenuAtPosition(e.detail.x, e.detail.y);
429 }
430 },
431
432 /**
433 * @param {Event} e
434 * @private
435 */
436 onCommandClick_: function(e) {
calamity64e2012a2017-06-21 09:57:14437 this.handle(
438 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
tsergeant4707d172017-06-05 05:47:02439 this.closeCommandMenu();
tsergeant2db36262017-05-15 02:47:53440 },
441
442 /**
443 * @param {!Event} e
444 * @private
445 */
446 onKeydown_: function(e) {
447 var selection = this.getState().selection.items;
tsergeant6c3a6df2017-06-06 23:53:02448 if (e.target == document.body)
449 this.handleKeyEvent(e, selection);
tsergeant2db36262017-05-15 02:47:53450 },
451
452 /**
453 * Close the menu on mousedown so clicks can propagate to the underlying UI.
454 * This allows the user to right click the list while a context menu is
455 * showing and get another context menu.
456 * @param {Event} e
457 * @private
458 */
459 onMenuMousedown_: function(e) {
460 if (e.path[0] != this.$.dropdown)
461 return;
462
tsergeant4707d172017-06-05 05:47:02463 this.closeCommandMenu();
tsergeant2db36262017-05-15 02:47:53464 },
465
466 /**
467 * @param {Command} command
468 * @return {string}
469 * @private
470 */
471 getCommandLabel_: function(command) {
472 var multipleNodes = this.menuIds_.size > 1 ||
473 this.containsMatchingNode_(this.menuIds_, function(node) {
474 return !node.url;
475 });
476 var label;
477 switch (command) {
478 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02479 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53480 return '';
481
482 var id = Array.from(this.menuIds_)[0];
483 var itemUrl = this.getState().nodes[id].url;
484 label = itemUrl ? 'menuEdit' : 'menuRename';
485 break;
486 case Command.COPY:
487 label = 'menuCopyURL';
488 break;
489 case Command.DELETE:
490 label = 'menuDelete';
491 break;
492 case Command.OPEN_NEW_TAB:
493 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
494 break;
495 case Command.OPEN_NEW_WINDOW:
496 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
497 break;
498 case Command.OPEN_INCOGNITO:
499 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
500 break;
501 }
502
503 return loadTimeData.getString(assert(label));
504 },
505
506 /**
507 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14508 * @return {string}
509 * @private
510 */
511 getCommandSublabel_: function(command) {
512 var multipleNodes = this.menuIds_.size > 1 ||
513 this.containsMatchingNode_(this.menuIds_, function(node) {
514 return !node.url;
515 });
516 switch (command) {
517 case Command.OPEN_NEW_TAB:
518 var urls = this.expandUrls_(this.menuIds_);
519 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
520 default:
521 return '';
522 }
523 },
524
525 /** @private */
526 onMenuIdsChanged_: function() {
527 if (!this.menuIds_)
528 return;
529
530 this.hasAnySublabel_ = this.menuCommands_.some(function(command) {
531 return this.getCommandSublabel_(command) != '';
532 }.bind(this));
533 },
534
535 /**
536 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53537 * @return {boolean}
538 * @private
539 */
tsergeant2437f992017-06-13 23:54:29540 showDividerAfter_: function(command, itemIds) {
541 return command == Command.DELETE &&
542 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
tsergeant2db36262017-05-15 02:47:53543 },
544 });
545
546 /** @private {bookmarks.CommandManager} */
547 CommandManager.instance_ = null;
548
549 /** @return {!bookmarks.CommandManager} */
550 CommandManager.getInstance = function() {
551 return assert(CommandManager.instance_);
552 };
553
554 return {
555 CommandManager: CommandManager,
556 };
tsergeant77365182017-05-05 04:02:33557});