blob: 6123d0106b559e4e66c3b83c152bac9efaf54cc6 [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 */
rbpotterf0aa38c2019-11-19 01:21:319import {Polymer, html, flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
10import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.m.js';
11import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
12import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.m.js';
13import 'chrome://resources/cr_elements/shared_vars_css.m.js';
14import {assert} from 'chrome://resources/js/assert.m.js';
15import {isMac} from 'chrome://resources/js/cr.m.js';
16import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
17import {getInstance} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.m.js';
18import {KeyboardShortcutList} from 'chrome://resources/js/cr/ui/keyboard_shortcut_list.m.js';
19import 'chrome://resources/polymer/v3_0/iron-a11y-keys-behavior/iron-a11y-keys-behavior.js';
20import {trackUpdatedItems, highlightUpdatedItems} from './api_listener.js';
21import {BrowserProxy} from './browser_proxy.js';
22import {Command, MenuSource, IncognitoAvailability, OPEN_CONFIRMATION_LIMIT, ROOT_NODE_ID} from './constants.js';
23import {DialogFocusManager} from './dialog_focus_manager.js';
24import './edit_dialog.js';
25import './shared_style.js';
26import {StoreClient} from './store_client.js';
27import './strings.m.js';
28import {BookmarkNode} from './types.js';
29import {canEditNode, canReorderChildren, getDisplayedList} from './util.js';
30import {deselectItems, selectAll, selectFolder} from './actions.js';
31
32 export const CommandManager = Polymer({
tsergeant2db36262017-05-15 02:47:5333 is: 'bookmarks-command-manager',
tsergeant77365182017-05-05 04:02:3334
rbpotterf0aa38c2019-11-19 01:21:3135 _template: html`{__html_template__}`,
36
tsergeant2db36262017-05-15 02:47:5337 behaviors: [
rbpotterf0aa38c2019-11-19 01:21:3138 StoreClient,
tsergeant2db36262017-05-15 02:47:5339 ],
40
41 properties: {
42 /** @private {!Array<Command>} */
43 menuCommands_: {
44 type: Array,
Christopher Lam043c7cb2018-01-09 04:14:1445 computed: 'computeMenuCommands_(menuSource_)',
tsergeant13a466462017-05-15 01:21:0346 },
tsergeant2db36262017-05-15 02:47:5347
tsergeant2437f992017-06-13 23:54:2948 /** @private {Set<string>} */
dpapad43083f02018-06-14 22:02:4049 menuIds_: Object,
calamity64e2012a2017-06-21 09:57:1450
51 /** @private */
52 hasAnySublabel_: {
53 type: Boolean,
54 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1455 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1456 },
tsergeant2437f992017-06-13 23:54:2957
Christopher Lam043c7cb2018-01-09 04:14:1458 /**
59 * Indicates where the context menu was opened from. Will be NONE if
60 * menu is not open, indicating that commands are from keyboard shortcuts
61 * or elsewhere in the UI.
62 * @private {MenuSource}
63 */
dpapad43083f02018-06-14 22:02:4064 menuSource_: {
65 type: Number,
66 value: MenuSource.NONE,
67 },
Christopher Lam043c7cb2018-01-09 04:14:1468
tsergeant2437f992017-06-13 23:54:2969 /** @private */
Hector Carmona79b07f02019-09-12 00:40:5370 canPaste_: Boolean,
71
72 /** @private */
tsergeant2437f992017-06-13 23:54:2973 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0374 },
75
tsergeant7fb9e13f2017-06-26 06:35:1976 /** @private {?Function} */
77 confirmOpenCallback_: null,
78
tsergeant2db36262017-05-15 02:47:5379 attached: function() {
80 assert(CommandManager.instance_ == null);
81 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3382
rbpotterf0aa38c2019-11-19 01:21:3183 /** @private {!BrowserProxy} */
84 this.browserProxy_ = BrowserProxy.getInstance();
rbpottercd521682019-11-12 02:00:3585
Hector Carmona79b07f02019-09-12 00:40:5386 this.watch('globalCanEdit_', state => state.prefs.canEdit);
tsergeant2437f992017-06-13 23:54:2987 this.updateFromStore();
88
rbpotterf0aa38c2019-11-19 01:21:3189 /** @private {!Map<Command, KeyboardShortcutList>} */
Tim Sergeanta2233c812017-07-26 03:02:4790 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3591
92 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3593 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
94
Tim Sergeant40d2d2c2017-07-20 01:37:0695 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3596 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
97 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
98
John Lee457ebaaa2019-01-17 17:07:1499 // Note: the undo shortcut is also defined in bookmarks_ui.cc
Chris Hallf62ade22018-10-11 05:37:57100 // TODO(b/893033): de-duplicate shortcut by moving all shortcut
101 // definitions from JS to C++.
tsergeant0292e51a2017-06-16 03:44:35102 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
103 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:41104
105 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
106 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:22107
108 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
109 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
110 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
Christopher Lamf4a16fd2018-02-01 01:47:11111
112 /** @private {!Map<string, Function>} */
113 this.boundListeners_ = new Map();
114
115 const addDocumentListener = (eventName, handler) => {
116 assert(!this.boundListeners_.has(eventName));
117 const boundListener = handler.bind(this);
118 this.boundListeners_.set(eventName, boundListener);
119 document.addEventListener(eventName, boundListener);
120 };
121 addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
122 addDocumentListener('keydown', this.onKeydown_);
123
124 const addDocumentListenerForCommand = (eventName, command) => {
125 addDocumentListener(eventName, (e) => {
Dan Beamd1cca6e2019-01-03 02:46:27126 if (e.path[0].tagName == 'INPUT') {
Christopher Lamf4a16fd2018-02-01 01:47:11127 return;
Dan Beamd1cca6e2019-01-03 02:46:27128 }
Christopher Lamf4a16fd2018-02-01 01:47:11129
130 const items = this.getState().selection.items;
Dan Beamd1cca6e2019-01-03 02:46:27131 if (this.canExecute(command, items)) {
Christopher Lamf4a16fd2018-02-01 01:47:11132 this.handle(command, items);
Dan Beamd1cca6e2019-01-03 02:46:27133 }
Christopher Lamf4a16fd2018-02-01 01:47:11134 });
135 };
136 addDocumentListenerForCommand('command-undo', Command.UNDO);
137 addDocumentListenerForCommand('cut', Command.CUT);
138 addDocumentListenerForCommand('copy', Command.COPY);
139 addDocumentListenerForCommand('paste', Command.PASTE);
tsergeant2db36262017-05-15 02:47:53140 },
tsergeant77365182017-05-05 04:02:33141
tsergeant2db36262017-05-15 02:47:53142 detached: function() {
143 CommandManager.instance_ = null;
Christopher Lamf4a16fd2018-02-01 01:47:11144 this.boundListeners_.forEach(
145 (handler, eventName) =>
146 document.removeEventListener(eventName, handler));
tsergeant2db36262017-05-15 02:47:53147 },
tsergeant77365182017-05-05 04:02:33148
tsergeant2db36262017-05-15 02:47:53149 /**
150 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28151 * Commands will execute on |items| if given, or on the currently selected
152 * items.
tsergeant2db36262017-05-15 02:47:53153 * @param {number} x
154 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43155 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28156 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53157 */
tsergeantd16c95a2017-07-14 04:49:43158 openCommandMenuAtPosition: function(x, y, source, items) {
159 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28160 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58161
dpapad111a34902017-09-12 16:51:10162 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27163 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
164 // Ensure that the menu is fully rendered before trying to position it.
rbpotterf0aa38c2019-11-19 01:21:31165 flush();
166 DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24167 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58168 dropdown.showAtPosition({top: y, left: x});
169 });
tsergeant2db36262017-05-15 02:47:53170 },
tsergeant77365182017-05-05 04:02:33171
tsergeant2db36262017-05-15 02:47:53172 /**
173 * Display the command context menu positioned to cover the |target|
174 * element. Commands will execute on the currently selected items.
175 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43176 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53177 */
tsergeantd16c95a2017-07-14 04:49:43178 openCommandMenuAtElement: function(target, source) {
179 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53180 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58181
dpapad111a34902017-09-12 16:51:10182 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27183 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
184 // Ensure that the menu is fully rendered before trying to position it.
rbpotterf0aa38c2019-11-19 01:21:31185 flush();
186 DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24187 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58188 dropdown.showAt(target);
189 });
tsergeant2db36262017-05-15 02:47:53190 },
tsergeant77365182017-05-05 04:02:33191
tsergeant2db36262017-05-15 02:47:53192 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02193 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43194 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27195 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53196 },
tsergeant77365182017-05-05 04:02:33197
tsergeant2db36262017-05-15 02:47:53198 ////////////////////////////////////////////////////////////////////////////
199 // Command handlers:
tsergeant77365182017-05-05 04:02:33200
tsergeant2db36262017-05-15 02:47:53201 /**
202 * Determine if the |command| can be executed with the given |itemIds|.
203 * Commands which appear in the context menu should be implemented
204 * separately using `isCommandVisible_` and `isCommandEnabled_`.
205 * @param {Command} command
206 * @param {!Set<string>} itemIds
207 * @return {boolean}
208 */
209 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10210 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34211 switch (command) {
212 case Command.OPEN:
213 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58214 case Command.UNDO:
215 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29216 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41217 case Command.SELECT_ALL:
218 case Command.DESELECT_ALL:
219 return true;
tsergeantb7253e92017-07-04 02:59:22220 case Command.COPY:
221 return itemIds.size > 0;
222 case Command.CUT:
223 return itemIds.size > 0 &&
224 !this.containsMatchingNode_(itemIds, function(node) {
rbpotterf0aa38c2019-11-19 01:21:31225 return !canEditNode(state, node.id);
tsergeantb7253e92017-07-04 02:59:22226 });
227 case Command.PASTE:
228 return state.search.term == '' &&
rbpotterf0aa38c2019-11-19 01:21:31229 canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34230 default:
231 return this.isCommandVisible_(command, itemIds) &&
232 this.isCommandEnabled_(command, itemIds);
233 }
tsergeant2db36262017-05-15 02:47:53234 },
tsergeant13a466462017-05-15 01:21:03235
tsergeant2db36262017-05-15 02:47:53236 /**
237 * @param {Command} command
238 * @param {!Set<string>} itemIds
239 * @return {boolean} True if the command should be visible in the context
240 * menu.
241 */
242 isCommandVisible_: function(command, itemIds) {
243 switch (command) {
244 case Command.EDIT:
Hitoshi Yoshidac755d9f2019-09-05 07:18:16245 return itemIds.size == 1 && this.globalCanEdit_;
Hector Carmona79b07f02019-09-12 00:40:53246 case Command.PASTE:
247 return this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09248 case Command.CUT:
249 case Command.COPY:
250 return itemIds.size >= 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22251 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29252 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53253 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29254 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43255 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14256 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43257 this.getState().search.term != '' &&
258 !this.containsMatchingNode_(itemIds, function(node) {
259 return !node.parentId || node.parentId == ROOT_NODE_ID;
260 });
tsergeant2db36262017-05-15 02:47:53261 case Command.OPEN_NEW_TAB:
262 case Command.OPEN_NEW_WINDOW:
263 case Command.OPEN_INCOGNITO:
264 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14265 case Command.ADD_BOOKMARK:
266 case Command.ADD_FOLDER:
267 case Command.SORT:
268 case Command.EXPORT:
269 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28270 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14271 return true;
tsergeantf1ffc892017-05-05 07:43:43272 }
Christopher Lam34be14992018-01-17 11:01:28273 return assert(false);
tsergeant2db36262017-05-15 02:47:53274 },
tsergeantf1ffc892017-05-05 07:43:43275
tsergeant2db36262017-05-15 02:47:53276 /**
277 * @param {Command} command
278 * @param {!Set<string>} itemIds
279 * @return {boolean} True if the command should be clickable in the context
280 * menu.
281 */
282 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14283 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53284 switch (command) {
tsergeant2437f992017-06-13 23:54:29285 case Command.EDIT:
286 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29287 return !this.containsMatchingNode_(itemIds, function(node) {
rbpotterf0aa38c2019-11-19 01:21:31288 return !canEditNode(state, node.id);
tsergeant2437f992017-06-13 23:54:29289 });
tsergeant2db36262017-05-15 02:47:53290 case Command.OPEN_NEW_TAB:
291 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53292 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02293 case Command.OPEN_INCOGNITO:
294 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14295 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02296 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14297 case Command.SORT:
298 return this.canChangeList_() &&
299 state.nodes[state.selectedFolder].children.length > 1;
300 case Command.ADD_BOOKMARK:
301 case Command.ADD_FOLDER:
302 return this.canChangeList_();
303 case Command.IMPORT:
304 return this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09305 case Command.PASTE:
Hector Carmona79b07f02019-09-12 00:40:53306 return this.canPaste_;
tsergeant2db36262017-05-15 02:47:53307 default:
308 return true;
309 }
310 },
tsergeant13a466462017-05-15 01:21:03311
tsergeant2db36262017-05-15 02:47:53312 /**
Christopher Lam043c7cb2018-01-09 04:14:14313 * Returns whether the currently displayed bookmarks list can be changed.
314 * @private
315 * @return {boolean}
316 */
317 canChangeList_: function() {
318 const state = this.getState();
319 return state.search.term == '' &&
rbpotterf0aa38c2019-11-19 01:21:31320 canReorderChildren(state, state.selectedFolder);
Christopher Lam043c7cb2018-01-09 04:14:14321 },
322
323 /**
tsergeant2db36262017-05-15 02:47:53324 * @param {Command} command
325 * @param {!Set<string>} itemIds
326 */
327 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10328 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53329 switch (command) {
dpapad111a34902017-09-12 16:51:10330 case Command.EDIT: {
Scott Chen657ebb7b2018-12-20 01:49:54331 const id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53332 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35333 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53334 break;
dpapad111a34902017-09-12 16:51:10335 }
tsergeantb7253e92017-07-04 02:59:22336 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10337 case Command.COPY: {
Scott Chen657ebb7b2018-12-20 01:49:54338 const idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34339 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10340 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22341 if (command == Command.COPY_URL) {
342 labelPromise =
343 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18344 } else if (idList.length == 1) {
345 labelPromise =
346 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22347 } else {
rbpottercd521682019-11-12 02:00:35348 labelPromise = this.browserProxy_.getPluralString(
349 'toastItemsCopied', idList.length);
tsergeantb7253e92017-07-04 02:59:22350 }
351
352 this.showTitleToast_(
353 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34354 });
tsergeant2db36262017-05-15 02:47:53355 break;
dpapad111a34902017-09-12 16:51:10356 }
357 case Command.SHOW_IN_FOLDER: {
Scott Chen657ebb7b2018-12-20 01:49:54358 const id = Array.from(itemIds)[0];
rbpotterf0aa38c2019-11-19 01:21:31359 this.dispatch(selectFolder(
tsergeantd16c95a2017-07-14 04:49:43360 assert(state.nodes[id].parentId), state.nodes));
rbpotterf0aa38c2019-11-19 01:21:31361 DialogFocusManager.getInstance().clearFocus();
tsergeantf42530c2017-07-28 02:44:57362 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43363 break;
dpapad111a34902017-09-12 16:51:10364 }
365 case Command.DELETE: {
Scott Chen657ebb7b2018-12-20 01:49:54366 const idList = Array.from(this.minimizeDeletionSet_(itemIds));
dpapad111a34902017-09-12 16:51:10367 const title = state.nodes[idList[0]].title;
368 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18369
370 if (idList.length == 1) {
371 labelPromise =
372 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
373 } else {
rbpottercd521682019-11-12 02:00:35374 labelPromise = this.browserProxy_.getPluralString(
375 'toastItemsDeleted', idList.length);
Christopher Lam75ca9102017-07-18 02:15:18376 }
377
dpapad325bf2f12017-07-26 18:47:34378 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22379 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34380 });
tsergeant2db36262017-05-15 02:47:53381 break;
dpapad111a34902017-09-12 16:51:10382 }
calamity2d4b5502017-05-29 03:57:58383 case Command.UNDO:
384 chrome.bookmarkManagerPrivate.undo();
rbpotterf0aa38c2019-11-19 01:21:31385 getInstance().hide();
calamity2d4b5502017-05-29 03:57:58386 break;
387 case Command.REDO:
388 chrome.bookmarkManagerPrivate.redo();
389 break;
tsergeant2db36262017-05-15 02:47:53390 case Command.OPEN_NEW_TAB:
391 case Command.OPEN_NEW_WINDOW:
392 case Command.OPEN_INCOGNITO:
393 this.openUrls_(this.expandUrls_(itemIds), command);
394 break;
tsergeant6c5ad90a2017-05-19 14:12:34395 case Command.OPEN:
Hector Carmonaa90ccca2019-05-17 18:25:22396 if (this.isFolder_(itemIds)) {
dpapad111a34902017-09-12 16:51:10397 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35398 this.dispatch(
rbpotterf0aa38c2019-11-19 01:21:31399 selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34400 } else {
401 this.openUrls_(this.expandUrls_(itemIds), command);
402 }
403 break;
tsergeant679159f2017-06-16 06:58:41404 case Command.SELECT_ALL:
rbpotterf0aa38c2019-11-19 01:21:31405 const displayedIds = getDisplayedList(state);
406 this.dispatch(selectAll(displayedIds, state));
tsergeant679159f2017-06-16 06:58:41407 break;
408 case Command.DESELECT_ALL:
rbpotterf0aa38c2019-11-19 01:21:31409 this.dispatch(deselectItems());
tsergeant679159f2017-06-16 06:58:41410 break;
tsergeantb7253e92017-07-04 02:59:22411 case Command.CUT:
412 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
413 break;
414 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10415 const selectedFolder = state.selectedFolder;
416 const selectedItems = state.selection.items;
rbpotterf0aa38c2019-11-19 01:21:31417 trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22418 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57419 selectedFolder, Array.from(selectedItems),
rbpotterf0aa38c2019-11-19 01:21:31420 highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22421 break;
Christopher Lam043c7cb2018-01-09 04:14:14422 case Command.SORT:
423 chrome.bookmarkManagerPrivate.sortChildren(
424 assert(state.selectedFolder));
rbpotterf0aa38c2019-11-19 01:21:31425 getInstance().show(
Christopher Lam043c7cb2018-01-09 04:14:14426 loadTimeData.getString('toastFolderSorted'), true);
427 break;
428 case Command.ADD_BOOKMARK:
429 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
430 .showAddDialog(false, assert(state.selectedFolder));
431 break;
432 case Command.ADD_FOLDER:
433 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
434 .showAddDialog(true, assert(state.selectedFolder));
435 break;
436 case Command.IMPORT:
437 chrome.bookmarks.import();
438 break;
439 case Command.EXPORT:
440 chrome.bookmarks.export();
441 break;
Christopher Lam34be14992018-01-17 11:01:28442 case Command.HELP_CENTER:
443 window.open('https://support.google.com/chrome/?p=bookmarks');
444 break;
tsergeant6c5ad90a2017-05-19 14:12:34445 default:
446 assert(false);
tsergeant2db36262017-05-15 02:47:53447 }
Hector Carmonaa90ccca2019-05-17 18:25:22448 this.recordCommandHistogram_(
449 itemIds, 'BookmarkManager.CommandExecuted', command);
tsergeant2db36262017-05-15 02:47:53450 },
tsergeant13a466462017-05-15 01:21:03451
tsergeant6c3a6df2017-06-06 23:53:02452 /**
tsergeant0292e51a2017-06-16 03:44:35453 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02454 * @param {!Set<string>} itemIds
455 * @return {boolean} True if the event was handled, triggering a keyboard
456 * shortcut.
457 */
458 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10459 for (const commandTuple of this.shortcuts_) {
460 const command = /** @type {Command} */ (commandTuple[0]);
461 const shortcut =
rbpotterf0aa38c2019-11-19 01:21:31462 /** @type {KeyboardShortcutList} */ (commandTuple[1]);
Tim Sergeanta2233c812017-07-26 03:02:47463 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
464 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02465
Hector Carmonaa90ccca2019-05-17 18:25:22466 this.recordCommandHistogram_(
467 itemIds, 'BookmarkManager.CommandExecutedFromKeyboard', command);
tsergeant6c3a6df2017-06-06 23:53:02468 e.stopPropagation();
469 e.preventDefault();
470 return true;
471 }
472 }
473
474 return false;
475 },
476
tsergeant2db36262017-05-15 02:47:53477 ////////////////////////////////////////////////////////////////////////////
478 // Private functions:
479
480 /**
tsergeant0292e51a2017-06-16 03:44:35481 * Register a keyboard shortcut for a command.
482 * @param {Command} command Command that the shortcut will trigger.
483 * @param {string} shortcut Keyboard shortcut, using the syntax of
484 * cr/ui/command.js.
485 * @param {string=} macShortcut If set, enables a replacement shortcut for
486 * Mac.
487 */
488 addShortcut_: function(command, shortcut, macShortcut) {
rbpotterf0aa38c2019-11-19 01:21:31489 shortcut = (isMac && macShortcut) ? macShortcut : shortcut;
490 this.shortcuts_.set(command, new KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35491 },
492
493 /**
tsergeant2db36262017-05-15 02:47:53494 * Minimize the set of |itemIds| by removing any node which has an ancestor
495 * node already in the set. This ensures that instead of trying to delete
496 * both a node and its descendant, we will only try to delete the topmost
497 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
498 * call.
499 * @param {!Set<string>} itemIds
500 * @return {!Set<string>}
501 */
502 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10503 const minimizedSet = new Set();
504 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53505 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10506 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53507 while (currentId != ROOT_NODE_ID) {
508 currentId = assert(nodes[currentId].parentId);
Dan Beamd1cca6e2019-01-03 02:46:27509 if (itemIds.has(currentId)) {
tsergeant2db36262017-05-15 02:47:53510 return;
Dan Beamd1cca6e2019-01-03 02:46:27511 }
tsergeant2db36262017-05-15 02:47:53512 }
513 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03514 });
tsergeant2db36262017-05-15 02:47:53515 return minimizedSet;
516 },
tsergeant13a466462017-05-15 01:21:03517
tsergeant2db36262017-05-15 02:47:53518 /**
tsergeant7fb9e13f2017-06-26 06:35:19519 * Open the given |urls| in response to a |command|. May show a confirmation
520 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53521 * @param {!Array<string>} urls
522 * @param {Command} command
523 * @private
524 */
525 openUrls_: function(urls, command) {
526 assert(
tsergeant6c5ad90a2017-05-19 14:12:34527 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53528 command == Command.OPEN_NEW_WINDOW ||
529 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03530
Dan Beamd1cca6e2019-01-03 02:46:27531 if (urls.length == 0) {
tsergeantfad224f2017-05-05 04:42:09532 return;
Dan Beamd1cca6e2019-01-03 02:46:27533 }
tsergeantfad224f2017-05-05 04:42:09534
dpapad111a34902017-09-12 16:51:10535 const openUrlsCallback = function() {
536 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19537 if (command == Command.OPEN_NEW_WINDOW || incognito) {
538 chrome.windows.create({url: urls, incognito: incognito});
539 } else {
Dan Beamd1cca6e2019-01-03 02:46:27540 if (command == Command.OPEN) {
tsergeant7fb9e13f2017-06-26 06:35:19541 chrome.tabs.create({url: urls.shift(), active: true});
Dan Beamd1cca6e2019-01-03 02:46:27542 }
tsergeant7fb9e13f2017-06-26 06:35:19543 urls.forEach(function(url) {
544 chrome.tabs.create({url: url, active: false});
545 });
546 }
547 };
548
549 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
550 openUrlsCallback();
551 return;
tsergeant2db36262017-05-15 02:47:53552 }
tsergeant7fb9e13f2017-06-26 06:35:19553
554 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10555 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16556 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19557 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58558
rbpotterf0aa38c2019-11-19 01:21:31559 DialogFocusManager.getInstance().showDialog(
calamity57b2e8fd2017-06-29 18:46:58560 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53561 },
tsergeant77365182017-05-05 04:02:33562
tsergeant2db36262017-05-15 02:47:53563 /**
564 * Returns all URLs in the given set of nodes and their immediate children.
565 * Note that these will be ordered by insertion order into the |itemIds|
566 * set, and that it is possible to duplicate a URL by passing in both the
567 * parent ID and child ID.
568 * @param {!Set<string>} itemIds
569 * @return {!Array<string>}
570 * @private
571 */
572 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10573 const urls = [];
574 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03575
tsergeant2db36262017-05-15 02:47:53576 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10577 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53578 if (node.url) {
579 urls.push(node.url);
580 } else {
581 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10582 const childNode = nodes[childId];
Dan Beamd1cca6e2019-01-03 02:46:27583 if (childNode.url) {
tsergeant2db36262017-05-15 02:47:53584 urls.push(childNode.url);
Dan Beamd1cca6e2019-01-03 02:46:27585 }
tsergeant2db36262017-05-15 02:47:53586 });
587 }
588 });
tsergeant13a466462017-05-15 01:21:03589
tsergeant2db36262017-05-15 02:47:53590 return urls;
591 },
592
593 /**
594 * @param {!Set<string>} itemIds
595 * @param {function(BookmarkNode):boolean} predicate
596 * @return {boolean} True if any node in |itemIds| returns true for
597 * |predicate|.
598 */
599 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10600 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53601
602 return Array.from(itemIds).some(function(id) {
603 return predicate(nodes[id]);
604 });
605 },
606
607 /**
tsergeant2437f992017-06-13 23:54:29608 * @param {!Set<string>} itemIds
609 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
610 * node.
Hector Carmonaa90ccca2019-05-17 18:25:22611 * @private
tsergeant2437f992017-06-13 23:54:29612 */
613 isSingleBookmark_: function(itemIds) {
614 return itemIds.size == 1 &&
615 this.containsMatchingNode_(itemIds, function(node) {
616 return !!node.url;
617 });
618 },
619
620 /**
Hector Carmonaa90ccca2019-05-17 18:25:22621 * @param {!Set<string>} itemIds
622 * @return {boolean}
623 * @private
624 */
625 isFolder_: function(itemIds) {
626 return itemIds.size == 1 &&
627 this.containsMatchingNode_(itemIds, node => !node.url);
628 },
629
630 /**
tsergeant2db36262017-05-15 02:47:53631 * @param {Command} command
632 * @return {string}
633 * @private
634 */
635 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10636 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53637 this.containsMatchingNode_(this.menuIds_, function(node) {
638 return !node.url;
639 });
dpapad111a34902017-09-12 16:51:10640 let label;
tsergeant2db36262017-05-15 02:47:53641 switch (command) {
642 case Command.EDIT:
Dan Beamd1cca6e2019-01-03 02:46:27643 if (this.menuIds_.size != 1) {
tsergeant2db36262017-05-15 02:47:53644 return '';
Dan Beamd1cca6e2019-01-03 02:46:27645 }
tsergeant2db36262017-05-15 02:47:53646
dpapad111a34902017-09-12 16:51:10647 const id = Array.from(this.menuIds_)[0];
648 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53649 label = itemUrl ? 'menuEdit' : 'menuRename';
650 break;
Hector Carmonad1223622019-08-27 19:44:09651 case Command.CUT:
652 label = 'menuCut';
653 break;
654 case Command.COPY:
655 label = 'menuCopy';
656 break;
tsergeantb7253e92017-07-04 02:59:22657 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53658 label = 'menuCopyURL';
659 break;
Hector Carmonad1223622019-08-27 19:44:09660 case Command.PASTE:
661 label = 'menuPaste';
662 break;
tsergeant2db36262017-05-15 02:47:53663 case Command.DELETE:
664 label = 'menuDelete';
665 break;
tsergeantd16c95a2017-07-14 04:49:43666 case Command.SHOW_IN_FOLDER:
667 label = 'menuShowInFolder';
668 break;
tsergeant2db36262017-05-15 02:47:53669 case Command.OPEN_NEW_TAB:
670 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
671 break;
672 case Command.OPEN_NEW_WINDOW:
673 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
674 break;
675 case Command.OPEN_INCOGNITO:
676 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
677 break;
Christopher Lam043c7cb2018-01-09 04:14:14678 case Command.SORT:
679 label = 'menuSort';
680 break;
681 case Command.ADD_BOOKMARK:
682 label = 'menuAddBookmark';
683 break;
684 case Command.ADD_FOLDER:
685 label = 'menuAddFolder';
686 break;
687 case Command.IMPORT:
688 label = 'menuImport';
689 break;
690 case Command.EXPORT:
691 label = 'menuExport';
692 break;
Christopher Lam34be14992018-01-17 11:01:28693 case Command.HELP_CENTER:
694 label = 'menuHelpCenter';
695 break;
tsergeant2db36262017-05-15 02:47:53696 }
Christopher Lam043c7cb2018-01-09 04:14:14697 assert(label);
tsergeant2db36262017-05-15 02:47:53698
699 return loadTimeData.getString(assert(label));
700 },
701
702 /**
703 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14704 * @return {string}
705 * @private
706 */
707 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10708 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14709 this.containsMatchingNode_(this.menuIds_, function(node) {
710 return !node.url;
711 });
712 switch (command) {
713 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10714 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14715 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
716 default:
717 return '';
718 }
719 },
720
721 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14722 computeMenuCommands_: function() {
723 switch (this.menuSource_) {
724 case MenuSource.ITEM:
725 case MenuSource.TREE:
726 return [
727 Command.EDIT,
Christopher Lam043c7cb2018-01-09 04:14:14728 Command.SHOW_IN_FOLDER,
729 Command.DELETE,
730 // <hr>
Hector Carmonad1223622019-08-27 19:44:09731 Command.CUT,
732 Command.COPY,
733 Command.COPY_URL,
734 Command.PASTE,
735 // <hr>
Christopher Lam043c7cb2018-01-09 04:14:14736 Command.OPEN_NEW_TAB,
737 Command.OPEN_NEW_WINDOW,
738 Command.OPEN_INCOGNITO,
739 ];
740 case MenuSource.TOOLBAR:
741 return [
742 Command.SORT,
743 // <hr>
744 Command.ADD_BOOKMARK,
745 Command.ADD_FOLDER,
746 // <hr>
747 Command.IMPORT,
748 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28749 // <hr>
750 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14751 ];
Christopher Lamfde61782018-01-11 04:27:07752 case MenuSource.LIST:
753 return [
754 Command.ADD_BOOKMARK,
755 Command.ADD_FOLDER,
756 ];
Christopher Lam043c7cb2018-01-09 04:14:14757 case MenuSource.NONE:
758 return [];
759 }
760 assert(false);
761 },
762
Christopher Lam430ef002018-01-18 10:08:50763 /**
764 * @return {boolean}
765 * @private
766 */
Christopher Lam043c7cb2018-01-09 04:14:14767 computeHasAnySublabel_: function() {
Dan Beamd1cca6e2019-01-03 02:46:27768 if (this.menuIds_ == undefined || this.menuCommands_ == undefined) {
Christopher Lam430ef002018-01-18 10:08:50769 return false;
Dan Beamd1cca6e2019-01-03 02:46:27770 }
calamity64e2012a2017-06-21 09:57:14771
Christopher Lam430ef002018-01-18 10:08:50772 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34773 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14774 },
775
776 /**
777 * @param {Command} command
Hector Carmonaa90ccca2019-05-17 18:25:22778 * @param {!Set<string>} itemIds
tsergeant2db36262017-05-15 02:47:53779 * @return {boolean}
780 * @private
781 */
tsergeant2437f992017-06-13 23:54:29782 showDividerAfter_: function(command, itemIds) {
Hector Carmonad1223622019-08-27 19:44:09783 switch (command) {
784 case Command.SORT:
785 case Command.ADD_FOLDER:
786 case Command.EXPORT:
787 return this.menuSource_ == MenuSource.TOOLBAR;
788 case Command.DELETE:
789 return this.globalCanEdit_;
790 case Command.PASTE:
791 return this.globalCanEdit_ || this.isSingleBookmark_(itemIds);
792 }
793 return false;
tsergeant2db36262017-05-15 02:47:53794 },
tsergeantb7253e92017-07-04 02:59:22795
796 /**
Hector Carmonaa90ccca2019-05-17 18:25:22797 * @param {!Set<string>} itemIds
798 * @param {string} histogram
799 * @param {number} command
800 * @private
801 */
802 recordCommandHistogram_: function(itemIds, histogram, command) {
803 if (command == Command.OPEN) {
804 command = this.isFolder_(itemIds) ? Command.OPEN_FOLDER :
805 Command.OPEN_BOOKMARK;
806 }
807
rbpottercd521682019-11-12 02:00:35808 this.browserProxy_.recordInHistogram(
809 histogram, command, Command.MAX_VALUE);
Hector Carmonaa90ccca2019-05-17 18:25:22810 },
811
812 /**
tsergeantb7253e92017-07-04 02:59:22813 * Show a toast with a bookmark |title| inserted into a label, with the
814 * title ellipsised if necessary.
815 * @param {!Promise<string>} labelPromise Promise which resolves with the
816 * label for the toast.
817 * @param {string} title Bookmark title to insert.
818 * @param {boolean} canUndo If true, shows an undo button in the toast.
819 * @private
820 */
Christopher Lam12860e72018-11-20 05:17:39821 showTitleToast_: async function(labelPromise, title, canUndo) {
822 const label = await labelPromise;
823 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
824 .map(function(p) {
825 // Make the bookmark name collapsible.
826 p.collapsible = !!p.arg;
827 return p;
828 });
tsergeantb7253e92017-07-04 02:59:22829
rbpotterf0aa38c2019-11-19 01:21:31830 getInstance().showForStringPieces(pieces, canUndo);
tsergeantb7253e92017-07-04 02:59:22831 },
832
Hector Carmona79b07f02019-09-12 00:40:53833 /**
834 * @param {number} targetId
835 * @private
836 */
837 updateCanPaste_: function(targetId) {
838 return new Promise(resolve => {
839 chrome.bookmarkManagerPrivate.canPaste(`${targetId}`, result => {
840 this.canPaste_ = result;
841 resolve();
842 });
843 });
844 },
845
tsergeantb7253e92017-07-04 02:59:22846 ////////////////////////////////////////////////////////////////////////////
847 // Event handlers:
848
849 /**
850 * @param {Event} e
851 * @private
852 */
Hector Carmona79b07f02019-09-12 00:40:53853 onOpenCommandMenu_: async function(e) {
854 await this.updateCanPaste_(e.detail.source);
tsergeantb7253e92017-07-04 02:59:22855 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43856 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22857 } else {
tsergeantd16c95a2017-07-14 04:49:43858 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22859 }
rbpottercd521682019-11-12 02:00:35860 this.browserProxy_.recordInHistogram(
Christopher Lamed175322018-01-18 14:54:49861 'BookmarkManager.CommandMenuOpened', e.detail.source,
862 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22863 },
864
865 /**
866 * @param {Event} e
867 * @private
868 */
869 onCommandClick_: function(e) {
870 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47871 /** @type {Command} */ (
872 Number(e.currentTarget.getAttribute('command'))),
873 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22874 this.closeCommandMenu();
875 },
876
877 /**
878 * @param {!Event} e
879 * @private
880 */
881 onKeydown_: function(e) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04882 const path = e.composedPath();
883 if (path[0].tagName == 'INPUT') {
884 return;
885 }
886 if ((e.target == document.body ||
887 path.some(el => el.tagName == 'BOOKMARKS-TOOLBAR')) &&
rbpotterf0aa38c2019-11-19 01:21:31888 !DialogFocusManager.getInstance().hasOpenDialog()) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04889 this.handleKeyEvent(e, this.getState().selection.items);
Tim Sergeant109279fe2017-07-20 03:07:17890 }
tsergeantb7253e92017-07-04 02:59:22891 },
892
893 /**
894 * Close the menu on mousedown so clicks can propagate to the underlying UI.
895 * This allows the user to right click the list while a context menu is
896 * showing and get another context menu.
897 * @param {Event} e
898 * @private
899 */
900 onMenuMousedown_: function(e) {
Dan Beamd1cca6e2019-01-03 02:46:27901 if (e.path[0].tagName != 'DIALOG') {
tsergeantb7253e92017-07-04 02:59:22902 return;
Dan Beamd1cca6e2019-01-03 02:46:27903 }
tsergeantb7253e92017-07-04 02:59:22904
905 this.closeCommandMenu();
906 },
907
908 /** @private */
909 onOpenCancelTap_: function() {
910 this.$.openDialog.get().cancel();
911 },
912
913 /** @private */
914 onOpenConfirmTap_: function() {
915 this.confirmOpenCallback_();
916 this.$.openDialog.get().close();
917 },
tsergeant2db36262017-05-15 02:47:53918 });
919
rbpotterf0aa38c2019-11-19 01:21:31920 /** @private {CommandManager} */
tsergeant2db36262017-05-15 02:47:53921 CommandManager.instance_ = null;
922
rbpotterf0aa38c2019-11-19 01:21:31923 /** @return {!CommandManager} */
tsergeant2db36262017-05-15 02:47:53924 CommandManager.getInstance = function() {
925 return assert(CommandManager.instance_);
926 };
927