blob: 3d83ab732f610b80c0d9fbec67a4e1485e1ce401 [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() {
dpapad111a34902017-09-12 16:51:1010 const CommandManager = Polymer({
tsergeant2db36262017-05-15 02:47:5311 is: 'bookmarks-command-manager',
tsergeant77365182017-05-05 04:02:3312
tsergeant2db36262017-05-15 02:47:5313 behaviors: [
14 bookmarks.StoreClient,
15 ],
16
17 properties: {
18 /** @private {!Array<Command>} */
19 menuCommands_: {
20 type: Array,
Christopher Lam043c7cb2018-01-09 04:14:1421 computed: 'computeMenuCommands_(menuSource_)',
tsergeant13a466462017-05-15 01:21:0322 },
tsergeant2db36262017-05-15 02:47:5323
tsergeant2437f992017-06-13 23:54:2924 /** @private {Set<string>} */
dpapad43083f02018-06-14 22:02:4025 menuIds_: Object,
calamity64e2012a2017-06-21 09:57:1426
27 /** @private */
28 hasAnySublabel_: {
29 type: Boolean,
30 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1431 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1432 },
tsergeant2437f992017-06-13 23:54:2933
Christopher Lam043c7cb2018-01-09 04:14:1434 /**
35 * Indicates where the context menu was opened from. Will be NONE if
36 * menu is not open, indicating that commands are from keyboard shortcuts
37 * or elsewhere in the UI.
38 * @private {MenuSource}
39 */
dpapad43083f02018-06-14 22:02:4040 menuSource_: {
41 type: Number,
42 value: MenuSource.NONE,
43 },
Christopher Lam043c7cb2018-01-09 04:14:1444
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
Hitoshi Yoshidac755d9f2019-09-05 07:18:1656 this.watch('globalCanEdit_', function(state) {
57 return state.prefs.canEdit;
58 });
tsergeant2437f992017-06-13 23:54:2959 this.updateFromStore();
60
Tim Sergeanta2233c812017-07-26 03:02:4761 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
62 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3563
64 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3565 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
66
Tim Sergeant40d2d2c2017-07-20 01:37:0667 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3568 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
69 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
70
John Lee457ebaaa2019-01-17 17:07:1471 // Note: the undo shortcut is also defined in bookmarks_ui.cc
Chris Hallf62ade22018-10-11 05:37:5772 // TODO(b/893033): de-duplicate shortcut by moving all shortcut
73 // definitions from JS to C++.
tsergeant0292e51a2017-06-16 03:44:3574 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
75 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4176
77 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
78 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2279
80 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
81 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
82 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
Christopher Lamf4a16fd2018-02-01 01:47:1183
84 /** @private {!Map<string, Function>} */
85 this.boundListeners_ = new Map();
86
87 const addDocumentListener = (eventName, handler) => {
88 assert(!this.boundListeners_.has(eventName));
89 const boundListener = handler.bind(this);
90 this.boundListeners_.set(eventName, boundListener);
91 document.addEventListener(eventName, boundListener);
92 };
93 addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
94 addDocumentListener('keydown', this.onKeydown_);
95
96 const addDocumentListenerForCommand = (eventName, command) => {
97 addDocumentListener(eventName, (e) => {
Dan Beamd1cca6e2019-01-03 02:46:2798 if (e.path[0].tagName == 'INPUT') {
Christopher Lamf4a16fd2018-02-01 01:47:1199 return;
Dan Beamd1cca6e2019-01-03 02:46:27100 }
Christopher Lamf4a16fd2018-02-01 01:47:11101
102 const items = this.getState().selection.items;
Dan Beamd1cca6e2019-01-03 02:46:27103 if (this.canExecute(command, items)) {
Christopher Lamf4a16fd2018-02-01 01:47:11104 this.handle(command, items);
Dan Beamd1cca6e2019-01-03 02:46:27105 }
Christopher Lamf4a16fd2018-02-01 01:47:11106 });
107 };
108 addDocumentListenerForCommand('command-undo', Command.UNDO);
109 addDocumentListenerForCommand('cut', Command.CUT);
110 addDocumentListenerForCommand('copy', Command.COPY);
111 addDocumentListenerForCommand('paste', Command.PASTE);
tsergeant2db36262017-05-15 02:47:53112 },
tsergeant77365182017-05-05 04:02:33113
tsergeant2db36262017-05-15 02:47:53114 detached: function() {
115 CommandManager.instance_ = null;
Christopher Lamf4a16fd2018-02-01 01:47:11116 this.boundListeners_.forEach(
117 (handler, eventName) =>
118 document.removeEventListener(eventName, handler));
tsergeant2db36262017-05-15 02:47:53119 },
tsergeant77365182017-05-05 04:02:33120
tsergeant2db36262017-05-15 02:47:53121 /**
122 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28123 * Commands will execute on |items| if given, or on the currently selected
124 * items.
tsergeant2db36262017-05-15 02:47:53125 * @param {number} x
126 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43127 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28128 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53129 */
tsergeantd16c95a2017-07-14 04:49:43130 openCommandMenuAtPosition: function(x, y, source, items) {
131 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28132 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58133
dpapad111a34902017-09-12 16:51:10134 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27135 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
136 // Ensure that the menu is fully rendered before trying to position it.
137 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58138 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24139 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58140 dropdown.showAtPosition({top: y, left: x});
141 });
tsergeant2db36262017-05-15 02:47:53142 },
tsergeant77365182017-05-05 04:02:33143
tsergeant2db36262017-05-15 02:47:53144 /**
145 * Display the command context menu positioned to cover the |target|
146 * element. Commands will execute on the currently selected items.
147 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43148 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53149 */
tsergeantd16c95a2017-07-14 04:49:43150 openCommandMenuAtElement: function(target, source) {
151 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53152 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58153
dpapad111a34902017-09-12 16:51:10154 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27155 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
156 // Ensure that the menu is fully rendered before trying to position it.
157 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58158 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24159 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58160 dropdown.showAt(target);
161 });
tsergeant2db36262017-05-15 02:47:53162 },
tsergeant77365182017-05-05 04:02:33163
tsergeant2db36262017-05-15 02:47:53164 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02165 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43166 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27167 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53168 },
tsergeant77365182017-05-05 04:02:33169
tsergeant2db36262017-05-15 02:47:53170 ////////////////////////////////////////////////////////////////////////////
171 // Command handlers:
tsergeant77365182017-05-05 04:02:33172
tsergeant2db36262017-05-15 02:47:53173 /**
174 * Determine if the |command| can be executed with the given |itemIds|.
175 * Commands which appear in the context menu should be implemented
176 * separately using `isCommandVisible_` and `isCommandEnabled_`.
177 * @param {Command} command
178 * @param {!Set<string>} itemIds
179 * @return {boolean}
180 */
181 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10182 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34183 switch (command) {
184 case Command.OPEN:
185 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58186 case Command.UNDO:
187 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29188 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41189 case Command.SELECT_ALL:
190 case Command.DESELECT_ALL:
191 return true;
tsergeantb7253e92017-07-04 02:59:22192 case Command.COPY:
193 return itemIds.size > 0;
194 case Command.CUT:
195 return itemIds.size > 0 &&
196 !this.containsMatchingNode_(itemIds, function(node) {
197 return !bookmarks.util.canEditNode(state, node.id);
198 });
199 case Command.PASTE:
200 return state.search.term == '' &&
201 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34202 default:
203 return this.isCommandVisible_(command, itemIds) &&
204 this.isCommandEnabled_(command, itemIds);
205 }
tsergeant2db36262017-05-15 02:47:53206 },
tsergeant13a466462017-05-15 01:21:03207
tsergeant2db36262017-05-15 02:47:53208 /**
209 * @param {Command} command
210 * @param {!Set<string>} itemIds
211 * @return {boolean} True if the command should be visible in the context
212 * menu.
213 */
214 isCommandVisible_: function(command, itemIds) {
215 switch (command) {
216 case Command.EDIT:
Hector Carmona95a99b882019-09-04 23:38:34217 case Command.PASTE:
Hitoshi Yoshidac755d9f2019-09-05 07:18:16218 return itemIds.size == 1 && this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09219 case Command.CUT:
220 case Command.COPY:
221 return itemIds.size >= 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22222 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29223 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53224 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29225 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43226 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14227 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43228 this.getState().search.term != '' &&
229 !this.containsMatchingNode_(itemIds, function(node) {
230 return !node.parentId || node.parentId == ROOT_NODE_ID;
231 });
tsergeant2db36262017-05-15 02:47:53232 case Command.OPEN_NEW_TAB:
233 case Command.OPEN_NEW_WINDOW:
234 case Command.OPEN_INCOGNITO:
235 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14236 case Command.ADD_BOOKMARK:
237 case Command.ADD_FOLDER:
238 case Command.SORT:
239 case Command.EXPORT:
240 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28241 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14242 return true;
tsergeantf1ffc892017-05-05 07:43:43243 }
Christopher Lam34be14992018-01-17 11:01:28244 return assert(false);
tsergeant2db36262017-05-15 02:47:53245 },
tsergeantf1ffc892017-05-05 07:43:43246
tsergeant2db36262017-05-15 02:47:53247 /**
248 * @param {Command} command
249 * @param {!Set<string>} itemIds
250 * @return {boolean} True if the command should be clickable in the context
251 * menu.
252 */
253 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14254 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53255 switch (command) {
tsergeant2437f992017-06-13 23:54:29256 case Command.EDIT:
257 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29258 return !this.containsMatchingNode_(itemIds, function(node) {
259 return !bookmarks.util.canEditNode(state, node.id);
260 });
tsergeant2db36262017-05-15 02:47:53261 case Command.OPEN_NEW_TAB:
262 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53263 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02264 case Command.OPEN_INCOGNITO:
265 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14266 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02267 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14268 case Command.SORT:
269 return this.canChangeList_() &&
270 state.nodes[state.selectedFolder].children.length > 1;
271 case Command.ADD_BOOKMARK:
272 case Command.ADD_FOLDER:
273 return this.canChangeList_();
274 case Command.IMPORT:
275 return this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09276 case Command.PASTE:
Hitoshi Yoshidac755d9f2019-09-05 07:18:16277 return true; // TODO(hcarmona): Add check for CanPasteFromClipboard.
tsergeant2db36262017-05-15 02:47:53278 default:
279 return true;
280 }
281 },
tsergeant13a466462017-05-15 01:21:03282
tsergeant2db36262017-05-15 02:47:53283 /**
Christopher Lam043c7cb2018-01-09 04:14:14284 * Returns whether the currently displayed bookmarks list can be changed.
285 * @private
286 * @return {boolean}
287 */
288 canChangeList_: function() {
289 const state = this.getState();
290 return state.search.term == '' &&
291 bookmarks.util.canReorderChildren(state, state.selectedFolder);
292 },
293
294 /**
tsergeant2db36262017-05-15 02:47:53295 * @param {Command} command
296 * @param {!Set<string>} itemIds
297 */
298 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10299 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53300 switch (command) {
dpapad111a34902017-09-12 16:51:10301 case Command.EDIT: {
Scott Chen657ebb7b2018-12-20 01:49:54302 const id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53303 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35304 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53305 break;
dpapad111a34902017-09-12 16:51:10306 }
tsergeantb7253e92017-07-04 02:59:22307 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10308 case Command.COPY: {
Scott Chen657ebb7b2018-12-20 01:49:54309 const idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34310 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10311 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22312 if (command == Command.COPY_URL) {
313 labelPromise =
314 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18315 } else if (idList.length == 1) {
316 labelPromise =
317 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22318 } else {
319 labelPromise = cr.sendWithPromise(
320 'getPluralString', 'toastItemsCopied', idList.length);
321 }
322
323 this.showTitleToast_(
324 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34325 });
tsergeant2db36262017-05-15 02:47:53326 break;
dpapad111a34902017-09-12 16:51:10327 }
328 case Command.SHOW_IN_FOLDER: {
Scott Chen657ebb7b2018-12-20 01:49:54329 const id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43330 this.dispatch(bookmarks.actions.selectFolder(
331 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57332 bookmarks.DialogFocusManager.getInstance().clearFocus();
333 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43334 break;
dpapad111a34902017-09-12 16:51:10335 }
336 case Command.DELETE: {
Scott Chen657ebb7b2018-12-20 01:49:54337 const idList = Array.from(this.minimizeDeletionSet_(itemIds));
dpapad111a34902017-09-12 16:51:10338 const title = state.nodes[idList[0]].title;
339 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18340
341 if (idList.length == 1) {
342 labelPromise =
343 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
344 } else {
345 labelPromise = cr.sendWithPromise(
346 'getPluralString', 'toastItemsDeleted', idList.length);
347 }
348
dpapad325bf2f12017-07-26 18:47:34349 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22350 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34351 });
tsergeant2db36262017-05-15 02:47:53352 break;
dpapad111a34902017-09-12 16:51:10353 }
calamity2d4b5502017-05-29 03:57:58354 case Command.UNDO:
355 chrome.bookmarkManagerPrivate.undo();
Esmael El-Moslimany89dd49b2019-03-19 20:43:56356 cr.toastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58357 break;
358 case Command.REDO:
359 chrome.bookmarkManagerPrivate.redo();
360 break;
tsergeant2db36262017-05-15 02:47:53361 case Command.OPEN_NEW_TAB:
362 case Command.OPEN_NEW_WINDOW:
363 case Command.OPEN_INCOGNITO:
364 this.openUrls_(this.expandUrls_(itemIds), command);
365 break;
tsergeant6c5ad90a2017-05-19 14:12:34366 case Command.OPEN:
Hector Carmonaa90ccca2019-05-17 18:25:22367 if (this.isFolder_(itemIds)) {
dpapad111a34902017-09-12 16:51:10368 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35369 this.dispatch(
370 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34371 } else {
372 this.openUrls_(this.expandUrls_(itemIds), command);
373 }
374 break;
tsergeant679159f2017-06-16 06:58:41375 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10376 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41377 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
378 break;
379 case Command.DESELECT_ALL:
380 this.dispatch(bookmarks.actions.deselectItems());
381 break;
tsergeantb7253e92017-07-04 02:59:22382 case Command.CUT:
383 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
384 break;
385 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10386 const selectedFolder = state.selectedFolder;
387 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57388 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22389 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57390 selectedFolder, Array.from(selectedItems),
391 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22392 break;
Christopher Lam043c7cb2018-01-09 04:14:14393 case Command.SORT:
394 chrome.bookmarkManagerPrivate.sortChildren(
395 assert(state.selectedFolder));
Esmael El-Moslimany89dd49b2019-03-19 20:43:56396 cr.toastManager.getInstance().show(
Christopher Lam043c7cb2018-01-09 04:14:14397 loadTimeData.getString('toastFolderSorted'), true);
398 break;
399 case Command.ADD_BOOKMARK:
400 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
401 .showAddDialog(false, assert(state.selectedFolder));
402 break;
403 case Command.ADD_FOLDER:
404 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
405 .showAddDialog(true, assert(state.selectedFolder));
406 break;
407 case Command.IMPORT:
408 chrome.bookmarks.import();
409 break;
410 case Command.EXPORT:
411 chrome.bookmarks.export();
412 break;
Christopher Lam34be14992018-01-17 11:01:28413 case Command.HELP_CENTER:
414 window.open('https://support.google.com/chrome/?p=bookmarks');
415 break;
tsergeant6c5ad90a2017-05-19 14:12:34416 default:
417 assert(false);
tsergeant2db36262017-05-15 02:47:53418 }
Hector Carmonaa90ccca2019-05-17 18:25:22419 this.recordCommandHistogram_(
420 itemIds, 'BookmarkManager.CommandExecuted', command);
tsergeant2db36262017-05-15 02:47:53421 },
tsergeant13a466462017-05-15 01:21:03422
tsergeant6c3a6df2017-06-06 23:53:02423 /**
tsergeant0292e51a2017-06-16 03:44:35424 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02425 * @param {!Set<string>} itemIds
426 * @return {boolean} True if the event was handled, triggering a keyboard
427 * shortcut.
428 */
429 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10430 for (const commandTuple of this.shortcuts_) {
431 const command = /** @type {Command} */ (commandTuple[0]);
432 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47433 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
434 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
435 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02436
Hector Carmonaa90ccca2019-05-17 18:25:22437 this.recordCommandHistogram_(
438 itemIds, 'BookmarkManager.CommandExecutedFromKeyboard', command);
tsergeant6c3a6df2017-06-06 23:53:02439 e.stopPropagation();
440 e.preventDefault();
441 return true;
442 }
443 }
444
445 return false;
446 },
447
tsergeant2db36262017-05-15 02:47:53448 ////////////////////////////////////////////////////////////////////////////
449 // Private functions:
450
451 /**
tsergeant0292e51a2017-06-16 03:44:35452 * Register a keyboard shortcut for a command.
453 * @param {Command} command Command that the shortcut will trigger.
454 * @param {string} shortcut Keyboard shortcut, using the syntax of
455 * cr/ui/command.js.
456 * @param {string=} macShortcut If set, enables a replacement shortcut for
457 * Mac.
458 */
459 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10460 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47461 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35462 },
463
464 /**
tsergeant2db36262017-05-15 02:47:53465 * Minimize the set of |itemIds| by removing any node which has an ancestor
466 * node already in the set. This ensures that instead of trying to delete
467 * both a node and its descendant, we will only try to delete the topmost
468 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
469 * call.
470 * @param {!Set<string>} itemIds
471 * @return {!Set<string>}
472 */
473 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10474 const minimizedSet = new Set();
475 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53476 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10477 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53478 while (currentId != ROOT_NODE_ID) {
479 currentId = assert(nodes[currentId].parentId);
Dan Beamd1cca6e2019-01-03 02:46:27480 if (itemIds.has(currentId)) {
tsergeant2db36262017-05-15 02:47:53481 return;
Dan Beamd1cca6e2019-01-03 02:46:27482 }
tsergeant2db36262017-05-15 02:47:53483 }
484 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03485 });
tsergeant2db36262017-05-15 02:47:53486 return minimizedSet;
487 },
tsergeant13a466462017-05-15 01:21:03488
tsergeant2db36262017-05-15 02:47:53489 /**
tsergeant7fb9e13f2017-06-26 06:35:19490 * Open the given |urls| in response to a |command|. May show a confirmation
491 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53492 * @param {!Array<string>} urls
493 * @param {Command} command
494 * @private
495 */
496 openUrls_: function(urls, command) {
497 assert(
tsergeant6c5ad90a2017-05-19 14:12:34498 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53499 command == Command.OPEN_NEW_WINDOW ||
500 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03501
Dan Beamd1cca6e2019-01-03 02:46:27502 if (urls.length == 0) {
tsergeantfad224f2017-05-05 04:42:09503 return;
Dan Beamd1cca6e2019-01-03 02:46:27504 }
tsergeantfad224f2017-05-05 04:42:09505
dpapad111a34902017-09-12 16:51:10506 const openUrlsCallback = function() {
507 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19508 if (command == Command.OPEN_NEW_WINDOW || incognito) {
509 chrome.windows.create({url: urls, incognito: incognito});
510 } else {
Dan Beamd1cca6e2019-01-03 02:46:27511 if (command == Command.OPEN) {
tsergeant7fb9e13f2017-06-26 06:35:19512 chrome.tabs.create({url: urls.shift(), active: true});
Dan Beamd1cca6e2019-01-03 02:46:27513 }
tsergeant7fb9e13f2017-06-26 06:35:19514 urls.forEach(function(url) {
515 chrome.tabs.create({url: url, active: false});
516 });
517 }
518 };
519
520 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
521 openUrlsCallback();
522 return;
tsergeant2db36262017-05-15 02:47:53523 }
tsergeant7fb9e13f2017-06-26 06:35:19524
525 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10526 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16527 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19528 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58529
530 bookmarks.DialogFocusManager.getInstance().showDialog(
531 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53532 },
tsergeant77365182017-05-05 04:02:33533
tsergeant2db36262017-05-15 02:47:53534 /**
535 * Returns all URLs in the given set of nodes and their immediate children.
536 * Note that these will be ordered by insertion order into the |itemIds|
537 * set, and that it is possible to duplicate a URL by passing in both the
538 * parent ID and child ID.
539 * @param {!Set<string>} itemIds
540 * @return {!Array<string>}
541 * @private
542 */
543 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10544 const urls = [];
545 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03546
tsergeant2db36262017-05-15 02:47:53547 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10548 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53549 if (node.url) {
550 urls.push(node.url);
551 } else {
552 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10553 const childNode = nodes[childId];
Dan Beamd1cca6e2019-01-03 02:46:27554 if (childNode.url) {
tsergeant2db36262017-05-15 02:47:53555 urls.push(childNode.url);
Dan Beamd1cca6e2019-01-03 02:46:27556 }
tsergeant2db36262017-05-15 02:47:53557 });
558 }
559 });
tsergeant13a466462017-05-15 01:21:03560
tsergeant2db36262017-05-15 02:47:53561 return urls;
562 },
563
564 /**
565 * @param {!Set<string>} itemIds
566 * @param {function(BookmarkNode):boolean} predicate
567 * @return {boolean} True if any node in |itemIds| returns true for
568 * |predicate|.
569 */
570 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10571 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53572
573 return Array.from(itemIds).some(function(id) {
574 return predicate(nodes[id]);
575 });
576 },
577
578 /**
tsergeant2437f992017-06-13 23:54:29579 * @param {!Set<string>} itemIds
580 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
581 * node.
Hector Carmonaa90ccca2019-05-17 18:25:22582 * @private
tsergeant2437f992017-06-13 23:54:29583 */
584 isSingleBookmark_: function(itemIds) {
585 return itemIds.size == 1 &&
586 this.containsMatchingNode_(itemIds, function(node) {
587 return !!node.url;
588 });
589 },
590
591 /**
Hector Carmonaa90ccca2019-05-17 18:25:22592 * @param {!Set<string>} itemIds
593 * @return {boolean}
594 * @private
595 */
596 isFolder_: function(itemIds) {
597 return itemIds.size == 1 &&
598 this.containsMatchingNode_(itemIds, node => !node.url);
599 },
600
601 /**
tsergeant2db36262017-05-15 02:47:53602 * @param {Command} command
603 * @return {string}
604 * @private
605 */
606 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10607 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53608 this.containsMatchingNode_(this.menuIds_, function(node) {
609 return !node.url;
610 });
dpapad111a34902017-09-12 16:51:10611 let label;
tsergeant2db36262017-05-15 02:47:53612 switch (command) {
613 case Command.EDIT:
Dan Beamd1cca6e2019-01-03 02:46:27614 if (this.menuIds_.size != 1) {
tsergeant2db36262017-05-15 02:47:53615 return '';
Dan Beamd1cca6e2019-01-03 02:46:27616 }
tsergeant2db36262017-05-15 02:47:53617
dpapad111a34902017-09-12 16:51:10618 const id = Array.from(this.menuIds_)[0];
619 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53620 label = itemUrl ? 'menuEdit' : 'menuRename';
621 break;
Hector Carmonad1223622019-08-27 19:44:09622 case Command.CUT:
623 label = 'menuCut';
624 break;
625 case Command.COPY:
626 label = 'menuCopy';
627 break;
tsergeantb7253e92017-07-04 02:59:22628 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53629 label = 'menuCopyURL';
630 break;
Hector Carmonad1223622019-08-27 19:44:09631 case Command.PASTE:
632 label = 'menuPaste';
633 break;
tsergeant2db36262017-05-15 02:47:53634 case Command.DELETE:
635 label = 'menuDelete';
636 break;
tsergeantd16c95a2017-07-14 04:49:43637 case Command.SHOW_IN_FOLDER:
638 label = 'menuShowInFolder';
639 break;
tsergeant2db36262017-05-15 02:47:53640 case Command.OPEN_NEW_TAB:
641 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
642 break;
643 case Command.OPEN_NEW_WINDOW:
644 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
645 break;
646 case Command.OPEN_INCOGNITO:
647 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
648 break;
Christopher Lam043c7cb2018-01-09 04:14:14649 case Command.SORT:
650 label = 'menuSort';
651 break;
652 case Command.ADD_BOOKMARK:
653 label = 'menuAddBookmark';
654 break;
655 case Command.ADD_FOLDER:
656 label = 'menuAddFolder';
657 break;
658 case Command.IMPORT:
659 label = 'menuImport';
660 break;
661 case Command.EXPORT:
662 label = 'menuExport';
663 break;
Christopher Lam34be14992018-01-17 11:01:28664 case Command.HELP_CENTER:
665 label = 'menuHelpCenter';
666 break;
tsergeant2db36262017-05-15 02:47:53667 }
Christopher Lam043c7cb2018-01-09 04:14:14668 assert(label);
tsergeant2db36262017-05-15 02:47:53669
670 return loadTimeData.getString(assert(label));
671 },
672
673 /**
674 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14675 * @return {string}
676 * @private
677 */
678 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10679 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14680 this.containsMatchingNode_(this.menuIds_, function(node) {
681 return !node.url;
682 });
683 switch (command) {
684 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10685 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14686 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
687 default:
688 return '';
689 }
690 },
691
692 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14693 computeMenuCommands_: function() {
694 switch (this.menuSource_) {
695 case MenuSource.ITEM:
696 case MenuSource.TREE:
697 return [
698 Command.EDIT,
Christopher Lam043c7cb2018-01-09 04:14:14699 Command.SHOW_IN_FOLDER,
700 Command.DELETE,
701 // <hr>
Hector Carmonad1223622019-08-27 19:44:09702 Command.CUT,
703 Command.COPY,
704 Command.COPY_URL,
705 Command.PASTE,
706 // <hr>
Christopher Lam043c7cb2018-01-09 04:14:14707 Command.OPEN_NEW_TAB,
708 Command.OPEN_NEW_WINDOW,
709 Command.OPEN_INCOGNITO,
710 ];
711 case MenuSource.TOOLBAR:
712 return [
713 Command.SORT,
714 // <hr>
715 Command.ADD_BOOKMARK,
716 Command.ADD_FOLDER,
717 // <hr>
718 Command.IMPORT,
719 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28720 // <hr>
721 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14722 ];
Christopher Lamfde61782018-01-11 04:27:07723 case MenuSource.LIST:
724 return [
725 Command.ADD_BOOKMARK,
726 Command.ADD_FOLDER,
727 ];
Christopher Lam043c7cb2018-01-09 04:14:14728 case MenuSource.NONE:
729 return [];
730 }
731 assert(false);
732 },
733
Christopher Lam430ef002018-01-18 10:08:50734 /**
735 * @return {boolean}
736 * @private
737 */
Christopher Lam043c7cb2018-01-09 04:14:14738 computeHasAnySublabel_: function() {
Dan Beamd1cca6e2019-01-03 02:46:27739 if (this.menuIds_ == undefined || this.menuCommands_ == undefined) {
Christopher Lam430ef002018-01-18 10:08:50740 return false;
Dan Beamd1cca6e2019-01-03 02:46:27741 }
calamity64e2012a2017-06-21 09:57:14742
Christopher Lam430ef002018-01-18 10:08:50743 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34744 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14745 },
746
747 /**
748 * @param {Command} command
Hector Carmonaa90ccca2019-05-17 18:25:22749 * @param {!Set<string>} itemIds
tsergeant2db36262017-05-15 02:47:53750 * @return {boolean}
751 * @private
752 */
tsergeant2437f992017-06-13 23:54:29753 showDividerAfter_: function(command, itemIds) {
Hector Carmonad1223622019-08-27 19:44:09754 switch (command) {
755 case Command.SORT:
756 case Command.ADD_FOLDER:
757 case Command.EXPORT:
758 return this.menuSource_ == MenuSource.TOOLBAR;
759 case Command.DELETE:
760 return this.globalCanEdit_;
761 case Command.PASTE:
762 return this.globalCanEdit_ || this.isSingleBookmark_(itemIds);
763 }
764 return false;
tsergeant2db36262017-05-15 02:47:53765 },
tsergeantb7253e92017-07-04 02:59:22766
767 /**
Hector Carmonaa90ccca2019-05-17 18:25:22768 * @param {!Set<string>} itemIds
769 * @param {string} histogram
770 * @param {number} command
771 * @private
772 */
773 recordCommandHistogram_: function(itemIds, histogram, command) {
774 if (command == Command.OPEN) {
775 command = this.isFolder_(itemIds) ? Command.OPEN_FOLDER :
776 Command.OPEN_BOOKMARK;
777 }
778
779 bookmarks.util.recordEnumHistogram(histogram, command, Command.MAX_VALUE);
780 },
781
782 /**
tsergeantb7253e92017-07-04 02:59:22783 * Show a toast with a bookmark |title| inserted into a label, with the
784 * title ellipsised if necessary.
785 * @param {!Promise<string>} labelPromise Promise which resolves with the
786 * label for the toast.
787 * @param {string} title Bookmark title to insert.
788 * @param {boolean} canUndo If true, shows an undo button in the toast.
789 * @private
790 */
Christopher Lam12860e72018-11-20 05:17:39791 showTitleToast_: async function(labelPromise, title, canUndo) {
792 const label = await labelPromise;
793 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
794 .map(function(p) {
795 // Make the bookmark name collapsible.
796 p.collapsible = !!p.arg;
797 return p;
798 });
tsergeantb7253e92017-07-04 02:59:22799
Esmael El-Moslimany89dd49b2019-03-19 20:43:56800 cr.toastManager.getInstance().showForStringPieces(pieces, canUndo);
tsergeantb7253e92017-07-04 02:59:22801 },
802
803 ////////////////////////////////////////////////////////////////////////////
804 // Event handlers:
805
806 /**
807 * @param {Event} e
808 * @private
809 */
Hitoshi Yoshidac755d9f2019-09-05 07:18:16810 onOpenCommandMenu_: function(e) {
tsergeantb7253e92017-07-04 02:59:22811 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43812 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22813 } else {
tsergeantd16c95a2017-07-14 04:49:43814 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22815 }
Christopher Lamed175322018-01-18 14:54:49816 bookmarks.util.recordEnumHistogram(
817 'BookmarkManager.CommandMenuOpened', e.detail.source,
818 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22819 },
820
821 /**
822 * @param {Event} e
823 * @private
824 */
825 onCommandClick_: function(e) {
826 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47827 /** @type {Command} */ (
828 Number(e.currentTarget.getAttribute('command'))),
829 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22830 this.closeCommandMenu();
831 },
832
833 /**
834 * @param {!Event} e
835 * @private
836 */
837 onKeydown_: function(e) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04838 const path = e.composedPath();
839 if (path[0].tagName == 'INPUT') {
840 return;
841 }
842 if ((e.target == document.body ||
843 path.some(el => el.tagName == 'BOOKMARKS-TOOLBAR')) &&
Tim Sergeant109279fe2017-07-20 03:07:17844 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04845 this.handleKeyEvent(e, this.getState().selection.items);
Tim Sergeant109279fe2017-07-20 03:07:17846 }
tsergeantb7253e92017-07-04 02:59:22847 },
848
849 /**
850 * Close the menu on mousedown so clicks can propagate to the underlying UI.
851 * This allows the user to right click the list while a context menu is
852 * showing and get another context menu.
853 * @param {Event} e
854 * @private
855 */
856 onMenuMousedown_: function(e) {
Dan Beamd1cca6e2019-01-03 02:46:27857 if (e.path[0].tagName != 'DIALOG') {
tsergeantb7253e92017-07-04 02:59:22858 return;
Dan Beamd1cca6e2019-01-03 02:46:27859 }
tsergeantb7253e92017-07-04 02:59:22860
861 this.closeCommandMenu();
862 },
863
864 /** @private */
865 onOpenCancelTap_: function() {
866 this.$.openDialog.get().cancel();
867 },
868
869 /** @private */
870 onOpenConfirmTap_: function() {
871 this.confirmOpenCallback_();
872 this.$.openDialog.get().close();
873 },
tsergeant2db36262017-05-15 02:47:53874 });
875
876 /** @private {bookmarks.CommandManager} */
877 CommandManager.instance_ = null;
878
879 /** @return {!bookmarks.CommandManager} */
880 CommandManager.getInstance = function() {
881 return assert(CommandManager.instance_);
882 };
883
884 return {
885 CommandManager: CommandManager,
886 };
tsergeant77365182017-05-05 04:02:33887});