blob: f7387f43bedae3f58b84b66aa95d2f8a95c37c11 [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
dpapad111a34902017-09-12 16:51:1011 const CommandManager = Polymer({
tsergeant2db36262017-05-15 02:47:5312 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,
Christopher Lam043c7cb2018-01-09 04:14:1422 computed: 'computeMenuCommands_(menuSource_)',
tsergeant13a466462017-05-15 01:21:0323 },
tsergeant2db36262017-05-15 02:47:5324
tsergeant2437f992017-06-13 23:54:2925 /** @private {Set<string>} */
calamity64e2012a2017-06-21 09:57:1426 menuIds_: {
27 type: Object,
calamity64e2012a2017-06-21 09:57:1428 },
29
30 /** @private */
31 hasAnySublabel_: {
32 type: Boolean,
33 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1434 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1435 },
tsergeant2437f992017-06-13 23:54:2936
Christopher Lam043c7cb2018-01-09 04:14:1437 /**
38 * Indicates where the context menu was opened from. Will be NONE if
39 * menu is not open, indicating that commands are from keyboard shortcuts
40 * or elsewhere in the UI.
41 * @private {MenuSource}
42 */
43 menuSource_: MenuSource.NONE,
44
tsergeant2437f992017-06-13 23:54:2945 /** @private */
46 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0347 },
48
tsergeant7fb9e13f2017-06-26 06:35:1949 /** @private {?Function} */
50 confirmOpenCallback_: null,
51
tsergeant2db36262017-05-15 02:47:5352 attached: function() {
53 assert(CommandManager.instance_ == null);
54 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3355
tsergeant2437f992017-06-13 23:54:2956 this.watch('globalCanEdit_', function(state) {
57 return state.prefs.canEdit;
58 });
59 this.updateFromStore();
60
tsergeant2db36262017-05-15 02:47:5361 /** @private {function(!Event)} */
Christopher Lam043c7cb2018-01-09 04:14:1462 this.boundOnOpenCommandMenu_ = this.onOpenCommandMenu_.bind(this);
63 document.addEventListener(
64 'open-command-menu', this.boundOnOpenCommandMenu_);
tsergeantfad224f2017-05-05 04:42:0965
calamityefe477352017-06-07 06:44:5866 /** @private {function()} */
dpapad325bf2f12017-07-26 18:47:3467 this.boundOnCommandUndo_ = () => {
calamityefe477352017-06-07 06:44:5868 this.handle(Command.UNDO, new Set());
dpapad325bf2f12017-07-26 18:47:3469 };
calamityefe477352017-06-07 06:44:5870 document.addEventListener('command-undo', this.boundOnCommandUndo_);
71
tsergeant2db36262017-05-15 02:47:5372 /** @private {function(!Event)} */
73 this.boundOnKeydown_ = this.onKeydown_.bind(this);
74 document.addEventListener('keydown', this.boundOnKeydown_);
tsergeantfad224f2017-05-05 04:42:0975
Tim Sergeanta2233c812017-07-26 03:02:4776 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
77 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3578
79 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3580 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
81
Tim Sergeant40d2d2c2017-07-20 01:37:0682 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3583 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
84 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
85
86 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
87 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4188
89 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
90 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2291
92 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
93 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
94 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
tsergeant2db36262017-05-15 02:47:5395 },
tsergeant77365182017-05-05 04:02:3396
tsergeant2db36262017-05-15 02:47:5397 detached: function() {
98 CommandManager.instance_ = null;
Christopher Lam043c7cb2018-01-09 04:14:1499 document.removeEventListener(
100 'open-command-menu', this.boundOnOpenCommandMenu_);
calamityefe477352017-06-07 06:44:58101 document.removeEventListener('command-undo', this.boundOnCommandUndo_);
tsergeant2db36262017-05-15 02:47:53102 document.removeEventListener('keydown', this.boundOnKeydown_);
103 },
tsergeant77365182017-05-05 04:02:33104
tsergeant2db36262017-05-15 02:47:53105 /**
106 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28107 * Commands will execute on |items| if given, or on the currently selected
108 * items.
tsergeant2db36262017-05-15 02:47:53109 * @param {number} x
110 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43111 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28112 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53113 */
tsergeantd16c95a2017-07-14 04:49:43114 openCommandMenuAtPosition: function(x, y, source, items) {
115 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28116 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58117
dpapad111a34902017-09-12 16:51:10118 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27119 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
120 // Ensure that the menu is fully rendered before trying to position it.
121 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58122 bookmarks.DialogFocusManager.getInstance().showDialog(
123 dropdown, function() {
124 dropdown.showAtPosition({top: y, left: x});
125 });
tsergeant2db36262017-05-15 02:47:53126 },
tsergeant77365182017-05-05 04:02:33127
tsergeant2db36262017-05-15 02:47:53128 /**
129 * Display the command context menu positioned to cover the |target|
130 * element. Commands will execute on the currently selected items.
131 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43132 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53133 */
tsergeantd16c95a2017-07-14 04:49:43134 openCommandMenuAtElement: function(target, source) {
135 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53136 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58137
dpapad111a34902017-09-12 16:51:10138 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27139 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
140 // Ensure that the menu is fully rendered before trying to position it.
141 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58142 bookmarks.DialogFocusManager.getInstance().showDialog(
143 dropdown, function() {
144 dropdown.showAt(target);
145 });
tsergeant2db36262017-05-15 02:47:53146 },
tsergeant77365182017-05-05 04:02:33147
tsergeant2db36262017-05-15 02:47:53148 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02149 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43150 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27151 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53152 },
tsergeant77365182017-05-05 04:02:33153
tsergeant2db36262017-05-15 02:47:53154 ////////////////////////////////////////////////////////////////////////////
155 // Command handlers:
tsergeant77365182017-05-05 04:02:33156
tsergeant2db36262017-05-15 02:47:53157 /**
158 * Determine if the |command| can be executed with the given |itemIds|.
159 * Commands which appear in the context menu should be implemented
160 * separately using `isCommandVisible_` and `isCommandEnabled_`.
161 * @param {Command} command
162 * @param {!Set<string>} itemIds
163 * @return {boolean}
164 */
165 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10166 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34167 switch (command) {
168 case Command.OPEN:
169 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58170 case Command.UNDO:
171 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29172 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41173 case Command.SELECT_ALL:
174 case Command.DESELECT_ALL:
175 return true;
tsergeantb7253e92017-07-04 02:59:22176 case Command.COPY:
177 return itemIds.size > 0;
178 case Command.CUT:
179 return itemIds.size > 0 &&
180 !this.containsMatchingNode_(itemIds, function(node) {
181 return !bookmarks.util.canEditNode(state, node.id);
182 });
183 case Command.PASTE:
184 return state.search.term == '' &&
185 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34186 default:
187 return this.isCommandVisible_(command, itemIds) &&
188 this.isCommandEnabled_(command, itemIds);
189 }
tsergeant2db36262017-05-15 02:47:53190 },
tsergeant13a466462017-05-15 01:21:03191
tsergeant2db36262017-05-15 02:47:53192 /**
193 * @param {Command} command
194 * @param {!Set<string>} itemIds
195 * @return {boolean} True if the command should be visible in the context
196 * menu.
197 */
198 isCommandVisible_: function(command, itemIds) {
199 switch (command) {
200 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29201 return itemIds.size == 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22202 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29203 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53204 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29205 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43206 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14207 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43208 this.getState().search.term != '' &&
209 !this.containsMatchingNode_(itemIds, function(node) {
210 return !node.parentId || node.parentId == ROOT_NODE_ID;
211 });
tsergeant2db36262017-05-15 02:47:53212 case Command.OPEN_NEW_TAB:
213 case Command.OPEN_NEW_WINDOW:
214 case Command.OPEN_INCOGNITO:
215 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14216 case Command.ADD_BOOKMARK:
217 case Command.ADD_FOLDER:
218 case Command.SORT:
219 case Command.EXPORT:
220 case Command.IMPORT:
221 return true;
tsergeant2db36262017-05-15 02:47:53222 default:
223 return false;
tsergeantf1ffc892017-05-05 07:43:43224 }
tsergeant2db36262017-05-15 02:47:53225 },
tsergeantf1ffc892017-05-05 07:43:43226
tsergeant2db36262017-05-15 02:47:53227 /**
228 * @param {Command} command
229 * @param {!Set<string>} itemIds
230 * @return {boolean} True if the command should be clickable in the context
231 * menu.
232 */
233 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14234 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53235 switch (command) {
tsergeant2437f992017-06-13 23:54:29236 case Command.EDIT:
237 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29238 return !this.containsMatchingNode_(itemIds, function(node) {
239 return !bookmarks.util.canEditNode(state, node.id);
240 });
tsergeant2db36262017-05-15 02:47:53241 case Command.OPEN_NEW_TAB:
242 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53243 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02244 case Command.OPEN_INCOGNITO:
245 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14246 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02247 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14248 case Command.SORT:
249 return this.canChangeList_() &&
250 state.nodes[state.selectedFolder].children.length > 1;
251 case Command.ADD_BOOKMARK:
252 case Command.ADD_FOLDER:
253 return this.canChangeList_();
254 case Command.IMPORT:
255 return this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53256 default:
257 return true;
258 }
259 },
tsergeant13a466462017-05-15 01:21:03260
tsergeant2db36262017-05-15 02:47:53261 /**
Christopher Lam043c7cb2018-01-09 04:14:14262 * Returns whether the currently displayed bookmarks list can be changed.
263 * @private
264 * @return {boolean}
265 */
266 canChangeList_: function() {
267 const state = this.getState();
268 return state.search.term == '' &&
269 bookmarks.util.canReorderChildren(state, state.selectedFolder);
270 },
271
272 /**
tsergeant2db36262017-05-15 02:47:53273 * @param {Command} command
274 * @param {!Set<string>} itemIds
275 */
276 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10277 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53278 switch (command) {
dpapad111a34902017-09-12 16:51:10279 case Command.EDIT: {
280 let id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53281 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35282 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53283 break;
dpapad111a34902017-09-12 16:51:10284 }
tsergeantb7253e92017-07-04 02:59:22285 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10286 case Command.COPY: {
287 let idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34288 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10289 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22290 if (command == Command.COPY_URL) {
291 labelPromise =
292 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18293 } else if (idList.length == 1) {
294 labelPromise =
295 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22296 } else {
297 labelPromise = cr.sendWithPromise(
298 'getPluralString', 'toastItemsCopied', idList.length);
299 }
300
301 this.showTitleToast_(
302 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34303 });
tsergeant2db36262017-05-15 02:47:53304 break;
dpapad111a34902017-09-12 16:51:10305 }
306 case Command.SHOW_IN_FOLDER: {
307 let id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43308 this.dispatch(bookmarks.actions.selectFolder(
309 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57310 bookmarks.DialogFocusManager.getInstance().clearFocus();
311 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43312 break;
dpapad111a34902017-09-12 16:51:10313 }
314 case Command.DELETE: {
315 let idList = Array.from(this.minimizeDeletionSet_(itemIds));
316 const title = state.nodes[idList[0]].title;
317 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18318
319 if (idList.length == 1) {
320 labelPromise =
321 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
322 } else {
323 labelPromise = cr.sendWithPromise(
324 'getPluralString', 'toastItemsDeleted', idList.length);
325 }
326
dpapad325bf2f12017-07-26 18:47:34327 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22328 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34329 });
tsergeant2db36262017-05-15 02:47:53330 break;
dpapad111a34902017-09-12 16:51:10331 }
calamity2d4b5502017-05-29 03:57:58332 case Command.UNDO:
333 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58334 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58335 break;
336 case Command.REDO:
337 chrome.bookmarkManagerPrivate.redo();
338 break;
tsergeant2db36262017-05-15 02:47:53339 case Command.OPEN_NEW_TAB:
340 case Command.OPEN_NEW_WINDOW:
341 case Command.OPEN_INCOGNITO:
342 this.openUrls_(this.expandUrls_(itemIds), command);
343 break;
tsergeant6c5ad90a2017-05-19 14:12:34344 case Command.OPEN:
dpapad111a34902017-09-12 16:51:10345 const isFolder = itemIds.size == 1 &&
tsergeant6c5ad90a2017-05-19 14:12:34346 this.containsMatchingNode_(itemIds, function(node) {
347 return !node.url;
348 });
349 if (isFolder) {
dpapad111a34902017-09-12 16:51:10350 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35351 this.dispatch(
352 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34353 } else {
354 this.openUrls_(this.expandUrls_(itemIds), command);
355 }
356 break;
tsergeant679159f2017-06-16 06:58:41357 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10358 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41359 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
360 break;
361 case Command.DESELECT_ALL:
362 this.dispatch(bookmarks.actions.deselectItems());
363 break;
tsergeantb7253e92017-07-04 02:59:22364 case Command.CUT:
365 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
366 break;
367 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10368 const selectedFolder = state.selectedFolder;
369 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57370 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22371 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57372 selectedFolder, Array.from(selectedItems),
373 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22374 break;
Christopher Lam043c7cb2018-01-09 04:14:14375 case Command.SORT:
376 chrome.bookmarkManagerPrivate.sortChildren(
377 assert(state.selectedFolder));
378 bookmarks.ToastManager.getInstance().show(
379 loadTimeData.getString('toastFolderSorted'), true);
380 break;
381 case Command.ADD_BOOKMARK:
382 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
383 .showAddDialog(false, assert(state.selectedFolder));
384 break;
385 case Command.ADD_FOLDER:
386 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
387 .showAddDialog(true, assert(state.selectedFolder));
388 break;
389 case Command.IMPORT:
390 chrome.bookmarks.import();
391 break;
392 case Command.EXPORT:
393 chrome.bookmarks.export();
394 break;
tsergeant6c5ad90a2017-05-19 14:12:34395 default:
396 assert(false);
tsergeant2db36262017-05-15 02:47:53397 }
Tim Sergeanta2233c812017-07-26 03:02:47398
399 bookmarks.util.recordEnumHistogram(
400 'BookmarkManager.CommandExecuted', command, Command.MAX_VALUE);
tsergeant2db36262017-05-15 02:47:53401 },
tsergeant13a466462017-05-15 01:21:03402
tsergeant6c3a6df2017-06-06 23:53:02403 /**
tsergeant0292e51a2017-06-16 03:44:35404 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02405 * @param {!Set<string>} itemIds
406 * @return {boolean} True if the event was handled, triggering a keyboard
407 * shortcut.
408 */
409 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10410 for (const commandTuple of this.shortcuts_) {
411 const command = /** @type {Command} */ (commandTuple[0]);
412 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47413 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
414 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
415 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02416
Tim Sergeanta2233c812017-07-26 03:02:47417 bookmarks.util.recordEnumHistogram(
418 'BookmarkManager.CommandExecutedFromKeyboard', command,
419 Command.MAX_VALUE);
tsergeant6c3a6df2017-06-06 23:53:02420 e.stopPropagation();
421 e.preventDefault();
422 return true;
423 }
424 }
425
426 return false;
427 },
428
tsergeant2db36262017-05-15 02:47:53429 ////////////////////////////////////////////////////////////////////////////
430 // Private functions:
431
432 /**
tsergeant0292e51a2017-06-16 03:44:35433 * Register a keyboard shortcut for a command.
434 * @param {Command} command Command that the shortcut will trigger.
435 * @param {string} shortcut Keyboard shortcut, using the syntax of
436 * cr/ui/command.js.
437 * @param {string=} macShortcut If set, enables a replacement shortcut for
438 * Mac.
439 */
440 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10441 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47442 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35443 },
444
445 /**
tsergeant2db36262017-05-15 02:47:53446 * Minimize the set of |itemIds| by removing any node which has an ancestor
447 * node already in the set. This ensures that instead of trying to delete
448 * both a node and its descendant, we will only try to delete the topmost
449 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
450 * call.
451 * @param {!Set<string>} itemIds
452 * @return {!Set<string>}
453 */
454 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10455 const minimizedSet = new Set();
456 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53457 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10458 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53459 while (currentId != ROOT_NODE_ID) {
460 currentId = assert(nodes[currentId].parentId);
461 if (itemIds.has(currentId))
462 return;
463 }
464 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03465 });
tsergeant2db36262017-05-15 02:47:53466 return minimizedSet;
467 },
tsergeant13a466462017-05-15 01:21:03468
tsergeant2db36262017-05-15 02:47:53469 /**
tsergeant7fb9e13f2017-06-26 06:35:19470 * Open the given |urls| in response to a |command|. May show a confirmation
471 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53472 * @param {!Array<string>} urls
473 * @param {Command} command
474 * @private
475 */
476 openUrls_: function(urls, command) {
477 assert(
tsergeant6c5ad90a2017-05-19 14:12:34478 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53479 command == Command.OPEN_NEW_WINDOW ||
480 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03481
tsergeant2db36262017-05-15 02:47:53482 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09483 return;
tsergeantfad224f2017-05-05 04:42:09484
dpapad111a34902017-09-12 16:51:10485 const openUrlsCallback = function() {
486 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19487 if (command == Command.OPEN_NEW_WINDOW || incognito) {
488 chrome.windows.create({url: urls, incognito: incognito});
489 } else {
490 if (command == Command.OPEN)
491 chrome.tabs.create({url: urls.shift(), active: true});
492 urls.forEach(function(url) {
493 chrome.tabs.create({url: url, active: false});
494 });
495 }
496 };
497
498 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
499 openUrlsCallback();
500 return;
tsergeant2db36262017-05-15 02:47:53501 }
tsergeant7fb9e13f2017-06-26 06:35:19502
503 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10504 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16505 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19506 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58507
508 bookmarks.DialogFocusManager.getInstance().showDialog(
509 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53510 },
tsergeant77365182017-05-05 04:02:33511
tsergeant2db36262017-05-15 02:47:53512 /**
513 * Returns all URLs in the given set of nodes and their immediate children.
514 * Note that these will be ordered by insertion order into the |itemIds|
515 * set, and that it is possible to duplicate a URL by passing in both the
516 * parent ID and child ID.
517 * @param {!Set<string>} itemIds
518 * @return {!Array<string>}
519 * @private
520 */
521 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10522 const urls = [];
523 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03524
tsergeant2db36262017-05-15 02:47:53525 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10526 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53527 if (node.url) {
528 urls.push(node.url);
529 } else {
530 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10531 const childNode = nodes[childId];
tsergeant2db36262017-05-15 02:47:53532 if (childNode.url)
533 urls.push(childNode.url);
534 });
535 }
536 });
tsergeant13a466462017-05-15 01:21:03537
tsergeant2db36262017-05-15 02:47:53538 return urls;
539 },
540
541 /**
542 * @param {!Set<string>} itemIds
543 * @param {function(BookmarkNode):boolean} predicate
544 * @return {boolean} True if any node in |itemIds| returns true for
545 * |predicate|.
546 */
547 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10548 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53549
550 return Array.from(itemIds).some(function(id) {
551 return predicate(nodes[id]);
552 });
553 },
554
555 /**
tsergeant2437f992017-06-13 23:54:29556 * @param {!Set<string>} itemIds
557 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
558 * node.
559 */
560 isSingleBookmark_: function(itemIds) {
561 return itemIds.size == 1 &&
562 this.containsMatchingNode_(itemIds, function(node) {
563 return !!node.url;
564 });
565 },
566
567 /**
tsergeant2db36262017-05-15 02:47:53568 * @param {Command} command
569 * @return {string}
570 * @private
571 */
572 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10573 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53574 this.containsMatchingNode_(this.menuIds_, function(node) {
575 return !node.url;
576 });
dpapad111a34902017-09-12 16:51:10577 let label;
tsergeant2db36262017-05-15 02:47:53578 switch (command) {
579 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02580 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53581 return '';
582
dpapad111a34902017-09-12 16:51:10583 const id = Array.from(this.menuIds_)[0];
584 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53585 label = itemUrl ? 'menuEdit' : 'menuRename';
586 break;
tsergeantb7253e92017-07-04 02:59:22587 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53588 label = 'menuCopyURL';
589 break;
590 case Command.DELETE:
591 label = 'menuDelete';
592 break;
tsergeantd16c95a2017-07-14 04:49:43593 case Command.SHOW_IN_FOLDER:
594 label = 'menuShowInFolder';
595 break;
tsergeant2db36262017-05-15 02:47:53596 case Command.OPEN_NEW_TAB:
597 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
598 break;
599 case Command.OPEN_NEW_WINDOW:
600 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
601 break;
602 case Command.OPEN_INCOGNITO:
603 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
604 break;
Christopher Lam043c7cb2018-01-09 04:14:14605 case Command.SORT:
606 label = 'menuSort';
607 break;
608 case Command.ADD_BOOKMARK:
609 label = 'menuAddBookmark';
610 break;
611 case Command.ADD_FOLDER:
612 label = 'menuAddFolder';
613 break;
614 case Command.IMPORT:
615 label = 'menuImport';
616 break;
617 case Command.EXPORT:
618 label = 'menuExport';
619 break;
tsergeant2db36262017-05-15 02:47:53620 }
Christopher Lam043c7cb2018-01-09 04:14:14621 assert(label);
tsergeant2db36262017-05-15 02:47:53622
623 return loadTimeData.getString(assert(label));
624 },
625
626 /**
627 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14628 * @return {string}
629 * @private
630 */
631 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10632 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14633 this.containsMatchingNode_(this.menuIds_, function(node) {
634 return !node.url;
635 });
636 switch (command) {
637 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10638 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14639 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
640 default:
641 return '';
642 }
643 },
644
645 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14646 computeMenuCommands_: function() {
647 switch (this.menuSource_) {
648 case MenuSource.ITEM:
649 case MenuSource.TREE:
650 return [
651 Command.EDIT,
652 Command.COPY_URL,
653 Command.SHOW_IN_FOLDER,
654 Command.DELETE,
655 // <hr>
656 Command.OPEN_NEW_TAB,
657 Command.OPEN_NEW_WINDOW,
658 Command.OPEN_INCOGNITO,
659 ];
660 case MenuSource.TOOLBAR:
661 return [
662 Command.SORT,
663 // <hr>
664 Command.ADD_BOOKMARK,
665 Command.ADD_FOLDER,
666 // <hr>
667 Command.IMPORT,
668 Command.EXPORT,
669 ];
Christopher Lamfde61782018-01-11 04:27:07670 case MenuSource.LIST:
671 return [
672 Command.ADD_BOOKMARK,
673 Command.ADD_FOLDER,
674 ];
Christopher Lam043c7cb2018-01-09 04:14:14675 case MenuSource.NONE:
676 return [];
677 }
678 assert(false);
679 },
680
681 /** @private */
682 computeHasAnySublabel_: function() {
calamity64e2012a2017-06-21 09:57:14683 if (!this.menuIds_)
684 return;
685
dpapad325bf2f12017-07-26 18:47:34686 this.hasAnySublabel_ = this.menuCommands_.some(
687 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14688 },
689
690 /**
691 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53692 * @return {boolean}
693 * @private
694 */
tsergeant2437f992017-06-13 23:54:29695 showDividerAfter_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14696 return ((command == Command.SORT || command == Command.ADD_FOLDER) &&
697 this.menuSource_ == MenuSource.TOOLBAR) ||
698 (command == Command.DELETE &&
699 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)));
tsergeant2db36262017-05-15 02:47:53700 },
tsergeantb7253e92017-07-04 02:59:22701
702 /**
703 * Show a toast with a bookmark |title| inserted into a label, with the
704 * title ellipsised if necessary.
705 * @param {!Promise<string>} labelPromise Promise which resolves with the
706 * label for the toast.
707 * @param {string} title Bookmark title to insert.
708 * @param {boolean} canUndo If true, shows an undo button in the toast.
709 * @private
710 */
711 showTitleToast_: function(labelPromise, title, canUndo) {
712 labelPromise.then(function(label) {
dpapad111a34902017-09-12 16:51:10713 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
714 .map(function(p) {
715 // Make the bookmark name collapsible.
716 p.collapsible = !!p.arg;
717 return p;
718 });
tsergeantb7253e92017-07-04 02:59:22719
720 bookmarks.ToastManager.getInstance().showForStringPieces(
721 pieces, canUndo);
722 });
723 },
724
725 ////////////////////////////////////////////////////////////////////////////
726 // Event handlers:
727
728 /**
729 * @param {Event} e
730 * @private
731 */
Christopher Lam043c7cb2018-01-09 04:14:14732 onOpenCommandMenu_: function(e) {
tsergeantb7253e92017-07-04 02:59:22733 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43734 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22735 } else {
tsergeantd16c95a2017-07-14 04:49:43736 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22737 }
738 },
739
740 /**
741 * @param {Event} e
742 * @private
743 */
744 onCommandClick_: function(e) {
745 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47746 /** @type {Command} */ (
747 Number(e.currentTarget.getAttribute('command'))),
748 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22749 this.closeCommandMenu();
750 },
751
752 /**
753 * @param {!Event} e
754 * @private
755 */
756 onKeydown_: function(e) {
dpapad111a34902017-09-12 16:51:10757 const selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17758 if (e.target == document.body &&
759 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22760 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17761 }
tsergeantb7253e92017-07-04 02:59:22762 },
763
764 /**
765 * Close the menu on mousedown so clicks can propagate to the underlying UI.
766 * This allows the user to right click the list while a context menu is
767 * showing and get another context menu.
768 * @param {Event} e
769 * @private
770 */
771 onMenuMousedown_: function(e) {
772 if (e.path[0] != this.$.dropdown.getIfExists())
773 return;
774
775 this.closeCommandMenu();
776 },
777
778 /** @private */
779 onOpenCancelTap_: function() {
780 this.$.openDialog.get().cancel();
781 },
782
783 /** @private */
784 onOpenConfirmTap_: function() {
785 this.confirmOpenCallback_();
786 this.$.openDialog.get().close();
787 },
tsergeant2db36262017-05-15 02:47:53788 });
789
790 /** @private {bookmarks.CommandManager} */
791 CommandManager.instance_ = null;
792
793 /** @return {!bookmarks.CommandManager} */
794 CommandManager.getInstance = function() {
795 return assert(CommandManager.instance_);
796 };
797
798 return {
799 CommandManager: CommandManager,
800 };
tsergeant77365182017-05-05 04:02:33801});