blob: 01288a636f3d269c9bb022b312ff703cf3c4ee5b [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 */
Hector Carmona79b07f02019-09-12 00:40:5346 canPaste_: Boolean,
47
48 /** @private */
tsergeant2437f992017-06-13 23:54:2949 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0350 },
51
tsergeant7fb9e13f2017-06-26 06:35:1952 /** @private {?Function} */
53 confirmOpenCallback_: null,
54
tsergeant2db36262017-05-15 02:47:5355 attached: function() {
56 assert(CommandManager.instance_ == null);
57 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3358
rbpottercd521682019-11-12 02:00:3559 /** @private {!bookmarks.BrowserProxy} */
60 this.browserProxy_ = bookmarks.BrowserProxy.getInstance();
61
Hector Carmona79b07f02019-09-12 00:40:5362 this.watch('globalCanEdit_', state => state.prefs.canEdit);
tsergeant2437f992017-06-13 23:54:2963 this.updateFromStore();
64
Tim Sergeanta2233c812017-07-26 03:02:4765 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
66 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3567
68 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3569 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
70
Tim Sergeant40d2d2c2017-07-20 01:37:0671 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3572 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
73 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
74
John Lee457ebaaa2019-01-17 17:07:1475 // Note: the undo shortcut is also defined in bookmarks_ui.cc
Chris Hallf62ade22018-10-11 05:37:5776 // TODO(b/893033): de-duplicate shortcut by moving all shortcut
77 // definitions from JS to C++.
tsergeant0292e51a2017-06-16 03:44:3578 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
79 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4180
81 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
82 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2283
84 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
85 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
86 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
Christopher Lamf4a16fd2018-02-01 01:47:1187
88 /** @private {!Map<string, Function>} */
89 this.boundListeners_ = new Map();
90
91 const addDocumentListener = (eventName, handler) => {
92 assert(!this.boundListeners_.has(eventName));
93 const boundListener = handler.bind(this);
94 this.boundListeners_.set(eventName, boundListener);
95 document.addEventListener(eventName, boundListener);
96 };
97 addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
98 addDocumentListener('keydown', this.onKeydown_);
99
100 const addDocumentListenerForCommand = (eventName, command) => {
101 addDocumentListener(eventName, (e) => {
Dan Beamd1cca6e2019-01-03 02:46:27102 if (e.path[0].tagName == 'INPUT') {
Christopher Lamf4a16fd2018-02-01 01:47:11103 return;
Dan Beamd1cca6e2019-01-03 02:46:27104 }
Christopher Lamf4a16fd2018-02-01 01:47:11105
106 const items = this.getState().selection.items;
Dan Beamd1cca6e2019-01-03 02:46:27107 if (this.canExecute(command, items)) {
Christopher Lamf4a16fd2018-02-01 01:47:11108 this.handle(command, items);
Dan Beamd1cca6e2019-01-03 02:46:27109 }
Christopher Lamf4a16fd2018-02-01 01:47:11110 });
111 };
112 addDocumentListenerForCommand('command-undo', Command.UNDO);
113 addDocumentListenerForCommand('cut', Command.CUT);
114 addDocumentListenerForCommand('copy', Command.COPY);
115 addDocumentListenerForCommand('paste', Command.PASTE);
tsergeant2db36262017-05-15 02:47:53116 },
tsergeant77365182017-05-05 04:02:33117
tsergeant2db36262017-05-15 02:47:53118 detached: function() {
119 CommandManager.instance_ = null;
Christopher Lamf4a16fd2018-02-01 01:47:11120 this.boundListeners_.forEach(
121 (handler, eventName) =>
122 document.removeEventListener(eventName, handler));
tsergeant2db36262017-05-15 02:47:53123 },
tsergeant77365182017-05-05 04:02:33124
tsergeant2db36262017-05-15 02:47:53125 /**
126 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28127 * Commands will execute on |items| if given, or on the currently selected
128 * items.
tsergeant2db36262017-05-15 02:47:53129 * @param {number} x
130 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43131 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28132 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53133 */
tsergeantd16c95a2017-07-14 04:49:43134 openCommandMenuAtPosition: function(x, y, source, items) {
135 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28136 this.menuIds_ = items || 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(
Christopher Lam42944a02018-03-16 04:11:24143 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58144 dropdown.showAtPosition({top: y, left: x});
145 });
tsergeant2db36262017-05-15 02:47:53146 },
tsergeant77365182017-05-05 04:02:33147
tsergeant2db36262017-05-15 02:47:53148 /**
149 * Display the command context menu positioned to cover the |target|
150 * element. Commands will execute on the currently selected items.
151 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43152 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53153 */
tsergeantd16c95a2017-07-14 04:49:43154 openCommandMenuAtElement: function(target, source) {
155 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53156 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58157
dpapad111a34902017-09-12 16:51:10158 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27159 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
160 // Ensure that the menu is fully rendered before trying to position it.
161 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58162 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24163 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58164 dropdown.showAt(target);
165 });
tsergeant2db36262017-05-15 02:47:53166 },
tsergeant77365182017-05-05 04:02:33167
tsergeant2db36262017-05-15 02:47:53168 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02169 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43170 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27171 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53172 },
tsergeant77365182017-05-05 04:02:33173
tsergeant2db36262017-05-15 02:47:53174 ////////////////////////////////////////////////////////////////////////////
175 // Command handlers:
tsergeant77365182017-05-05 04:02:33176
tsergeant2db36262017-05-15 02:47:53177 /**
178 * Determine if the |command| can be executed with the given |itemIds|.
179 * Commands which appear in the context menu should be implemented
180 * separately using `isCommandVisible_` and `isCommandEnabled_`.
181 * @param {Command} command
182 * @param {!Set<string>} itemIds
183 * @return {boolean}
184 */
185 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10186 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34187 switch (command) {
188 case Command.OPEN:
189 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58190 case Command.UNDO:
191 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29192 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41193 case Command.SELECT_ALL:
194 case Command.DESELECT_ALL:
195 return true;
tsergeantb7253e92017-07-04 02:59:22196 case Command.COPY:
197 return itemIds.size > 0;
198 case Command.CUT:
199 return itemIds.size > 0 &&
200 !this.containsMatchingNode_(itemIds, function(node) {
201 return !bookmarks.util.canEditNode(state, node.id);
202 });
203 case Command.PASTE:
204 return state.search.term == '' &&
205 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34206 default:
207 return this.isCommandVisible_(command, itemIds) &&
208 this.isCommandEnabled_(command, itemIds);
209 }
tsergeant2db36262017-05-15 02:47:53210 },
tsergeant13a466462017-05-15 01:21:03211
tsergeant2db36262017-05-15 02:47:53212 /**
213 * @param {Command} command
214 * @param {!Set<string>} itemIds
215 * @return {boolean} True if the command should be visible in the context
216 * menu.
217 */
218 isCommandVisible_: function(command, itemIds) {
219 switch (command) {
220 case Command.EDIT:
Hitoshi Yoshidac755d9f2019-09-05 07:18:16221 return itemIds.size == 1 && this.globalCanEdit_;
Hector Carmona79b07f02019-09-12 00:40:53222 case Command.PASTE:
223 return this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09224 case Command.CUT:
225 case Command.COPY:
226 return itemIds.size >= 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22227 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29228 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53229 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29230 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43231 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14232 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43233 this.getState().search.term != '' &&
234 !this.containsMatchingNode_(itemIds, function(node) {
235 return !node.parentId || node.parentId == ROOT_NODE_ID;
236 });
tsergeant2db36262017-05-15 02:47:53237 case Command.OPEN_NEW_TAB:
238 case Command.OPEN_NEW_WINDOW:
239 case Command.OPEN_INCOGNITO:
240 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14241 case Command.ADD_BOOKMARK:
242 case Command.ADD_FOLDER:
243 case Command.SORT:
244 case Command.EXPORT:
245 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28246 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14247 return true;
tsergeantf1ffc892017-05-05 07:43:43248 }
Christopher Lam34be14992018-01-17 11:01:28249 return assert(false);
tsergeant2db36262017-05-15 02:47:53250 },
tsergeantf1ffc892017-05-05 07:43:43251
tsergeant2db36262017-05-15 02:47:53252 /**
253 * @param {Command} command
254 * @param {!Set<string>} itemIds
255 * @return {boolean} True if the command should be clickable in the context
256 * menu.
257 */
258 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14259 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53260 switch (command) {
tsergeant2437f992017-06-13 23:54:29261 case Command.EDIT:
262 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29263 return !this.containsMatchingNode_(itemIds, function(node) {
264 return !bookmarks.util.canEditNode(state, node.id);
265 });
tsergeant2db36262017-05-15 02:47:53266 case Command.OPEN_NEW_TAB:
267 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53268 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02269 case Command.OPEN_INCOGNITO:
270 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14271 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02272 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14273 case Command.SORT:
274 return this.canChangeList_() &&
275 state.nodes[state.selectedFolder].children.length > 1;
276 case Command.ADD_BOOKMARK:
277 case Command.ADD_FOLDER:
278 return this.canChangeList_();
279 case Command.IMPORT:
280 return this.globalCanEdit_;
Hector Carmonad1223622019-08-27 19:44:09281 case Command.PASTE:
Hector Carmona79b07f02019-09-12 00:40:53282 return this.canPaste_;
tsergeant2db36262017-05-15 02:47:53283 default:
284 return true;
285 }
286 },
tsergeant13a466462017-05-15 01:21:03287
tsergeant2db36262017-05-15 02:47:53288 /**
Christopher Lam043c7cb2018-01-09 04:14:14289 * Returns whether the currently displayed bookmarks list can be changed.
290 * @private
291 * @return {boolean}
292 */
293 canChangeList_: function() {
294 const state = this.getState();
295 return state.search.term == '' &&
296 bookmarks.util.canReorderChildren(state, state.selectedFolder);
297 },
298
299 /**
tsergeant2db36262017-05-15 02:47:53300 * @param {Command} command
301 * @param {!Set<string>} itemIds
302 */
303 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10304 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53305 switch (command) {
dpapad111a34902017-09-12 16:51:10306 case Command.EDIT: {
Scott Chen657ebb7b2018-12-20 01:49:54307 const id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53308 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35309 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53310 break;
dpapad111a34902017-09-12 16:51:10311 }
tsergeantb7253e92017-07-04 02:59:22312 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10313 case Command.COPY: {
Scott Chen657ebb7b2018-12-20 01:49:54314 const idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34315 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10316 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22317 if (command == Command.COPY_URL) {
318 labelPromise =
319 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18320 } else if (idList.length == 1) {
321 labelPromise =
322 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22323 } else {
rbpottercd521682019-11-12 02:00:35324 labelPromise = this.browserProxy_.getPluralString(
325 'toastItemsCopied', idList.length);
tsergeantb7253e92017-07-04 02:59:22326 }
327
328 this.showTitleToast_(
329 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34330 });
tsergeant2db36262017-05-15 02:47:53331 break;
dpapad111a34902017-09-12 16:51:10332 }
333 case Command.SHOW_IN_FOLDER: {
Scott Chen657ebb7b2018-12-20 01:49:54334 const id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43335 this.dispatch(bookmarks.actions.selectFolder(
336 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57337 bookmarks.DialogFocusManager.getInstance().clearFocus();
338 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43339 break;
dpapad111a34902017-09-12 16:51:10340 }
341 case Command.DELETE: {
Scott Chen657ebb7b2018-12-20 01:49:54342 const idList = Array.from(this.minimizeDeletionSet_(itemIds));
dpapad111a34902017-09-12 16:51:10343 const title = state.nodes[idList[0]].title;
344 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18345
346 if (idList.length == 1) {
347 labelPromise =
348 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
349 } else {
rbpottercd521682019-11-12 02:00:35350 labelPromise = this.browserProxy_.getPluralString(
351 'toastItemsDeleted', idList.length);
Christopher Lam75ca9102017-07-18 02:15:18352 }
353
dpapad325bf2f12017-07-26 18:47:34354 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22355 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34356 });
tsergeant2db36262017-05-15 02:47:53357 break;
dpapad111a34902017-09-12 16:51:10358 }
calamity2d4b5502017-05-29 03:57:58359 case Command.UNDO:
360 chrome.bookmarkManagerPrivate.undo();
Esmael El-Moslimany89dd49b2019-03-19 20:43:56361 cr.toastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58362 break;
363 case Command.REDO:
364 chrome.bookmarkManagerPrivate.redo();
365 break;
tsergeant2db36262017-05-15 02:47:53366 case Command.OPEN_NEW_TAB:
367 case Command.OPEN_NEW_WINDOW:
368 case Command.OPEN_INCOGNITO:
369 this.openUrls_(this.expandUrls_(itemIds), command);
370 break;
tsergeant6c5ad90a2017-05-19 14:12:34371 case Command.OPEN:
Hector Carmonaa90ccca2019-05-17 18:25:22372 if (this.isFolder_(itemIds)) {
dpapad111a34902017-09-12 16:51:10373 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35374 this.dispatch(
375 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34376 } else {
377 this.openUrls_(this.expandUrls_(itemIds), command);
378 }
379 break;
tsergeant679159f2017-06-16 06:58:41380 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10381 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41382 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
383 break;
384 case Command.DESELECT_ALL:
385 this.dispatch(bookmarks.actions.deselectItems());
386 break;
tsergeantb7253e92017-07-04 02:59:22387 case Command.CUT:
388 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
389 break;
390 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10391 const selectedFolder = state.selectedFolder;
392 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57393 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22394 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57395 selectedFolder, Array.from(selectedItems),
396 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22397 break;
Christopher Lam043c7cb2018-01-09 04:14:14398 case Command.SORT:
399 chrome.bookmarkManagerPrivate.sortChildren(
400 assert(state.selectedFolder));
Esmael El-Moslimany89dd49b2019-03-19 20:43:56401 cr.toastManager.getInstance().show(
Christopher Lam043c7cb2018-01-09 04:14:14402 loadTimeData.getString('toastFolderSorted'), true);
403 break;
404 case Command.ADD_BOOKMARK:
405 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
406 .showAddDialog(false, assert(state.selectedFolder));
407 break;
408 case Command.ADD_FOLDER:
409 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
410 .showAddDialog(true, assert(state.selectedFolder));
411 break;
412 case Command.IMPORT:
413 chrome.bookmarks.import();
414 break;
415 case Command.EXPORT:
416 chrome.bookmarks.export();
417 break;
Christopher Lam34be14992018-01-17 11:01:28418 case Command.HELP_CENTER:
419 window.open('https://support.google.com/chrome/?p=bookmarks');
420 break;
tsergeant6c5ad90a2017-05-19 14:12:34421 default:
422 assert(false);
tsergeant2db36262017-05-15 02:47:53423 }
Hector Carmonaa90ccca2019-05-17 18:25:22424 this.recordCommandHistogram_(
425 itemIds, 'BookmarkManager.CommandExecuted', command);
tsergeant2db36262017-05-15 02:47:53426 },
tsergeant13a466462017-05-15 01:21:03427
tsergeant6c3a6df2017-06-06 23:53:02428 /**
tsergeant0292e51a2017-06-16 03:44:35429 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02430 * @param {!Set<string>} itemIds
431 * @return {boolean} True if the event was handled, triggering a keyboard
432 * shortcut.
433 */
434 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10435 for (const commandTuple of this.shortcuts_) {
436 const command = /** @type {Command} */ (commandTuple[0]);
437 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47438 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
439 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
440 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02441
Hector Carmonaa90ccca2019-05-17 18:25:22442 this.recordCommandHistogram_(
443 itemIds, 'BookmarkManager.CommandExecutedFromKeyboard', command);
tsergeant6c3a6df2017-06-06 23:53:02444 e.stopPropagation();
445 e.preventDefault();
446 return true;
447 }
448 }
449
450 return false;
451 },
452
tsergeant2db36262017-05-15 02:47:53453 ////////////////////////////////////////////////////////////////////////////
454 // Private functions:
455
456 /**
tsergeant0292e51a2017-06-16 03:44:35457 * Register a keyboard shortcut for a command.
458 * @param {Command} command Command that the shortcut will trigger.
459 * @param {string} shortcut Keyboard shortcut, using the syntax of
460 * cr/ui/command.js.
461 * @param {string=} macShortcut If set, enables a replacement shortcut for
462 * Mac.
463 */
464 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10465 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47466 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35467 },
468
469 /**
tsergeant2db36262017-05-15 02:47:53470 * Minimize the set of |itemIds| by removing any node which has an ancestor
471 * node already in the set. This ensures that instead of trying to delete
472 * both a node and its descendant, we will only try to delete the topmost
473 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
474 * call.
475 * @param {!Set<string>} itemIds
476 * @return {!Set<string>}
477 */
478 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10479 const minimizedSet = new Set();
480 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53481 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10482 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53483 while (currentId != ROOT_NODE_ID) {
484 currentId = assert(nodes[currentId].parentId);
Dan Beamd1cca6e2019-01-03 02:46:27485 if (itemIds.has(currentId)) {
tsergeant2db36262017-05-15 02:47:53486 return;
Dan Beamd1cca6e2019-01-03 02:46:27487 }
tsergeant2db36262017-05-15 02:47:53488 }
489 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03490 });
tsergeant2db36262017-05-15 02:47:53491 return minimizedSet;
492 },
tsergeant13a466462017-05-15 01:21:03493
tsergeant2db36262017-05-15 02:47:53494 /**
tsergeant7fb9e13f2017-06-26 06:35:19495 * Open the given |urls| in response to a |command|. May show a confirmation
496 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53497 * @param {!Array<string>} urls
498 * @param {Command} command
499 * @private
500 */
501 openUrls_: function(urls, command) {
502 assert(
tsergeant6c5ad90a2017-05-19 14:12:34503 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53504 command == Command.OPEN_NEW_WINDOW ||
505 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03506
Dan Beamd1cca6e2019-01-03 02:46:27507 if (urls.length == 0) {
tsergeantfad224f2017-05-05 04:42:09508 return;
Dan Beamd1cca6e2019-01-03 02:46:27509 }
tsergeantfad224f2017-05-05 04:42:09510
dpapad111a34902017-09-12 16:51:10511 const openUrlsCallback = function() {
512 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19513 if (command == Command.OPEN_NEW_WINDOW || incognito) {
514 chrome.windows.create({url: urls, incognito: incognito});
515 } else {
Dan Beamd1cca6e2019-01-03 02:46:27516 if (command == Command.OPEN) {
tsergeant7fb9e13f2017-06-26 06:35:19517 chrome.tabs.create({url: urls.shift(), active: true});
Dan Beamd1cca6e2019-01-03 02:46:27518 }
tsergeant7fb9e13f2017-06-26 06:35:19519 urls.forEach(function(url) {
520 chrome.tabs.create({url: url, active: false});
521 });
522 }
523 };
524
525 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
526 openUrlsCallback();
527 return;
tsergeant2db36262017-05-15 02:47:53528 }
tsergeant7fb9e13f2017-06-26 06:35:19529
530 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10531 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16532 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19533 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58534
535 bookmarks.DialogFocusManager.getInstance().showDialog(
536 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53537 },
tsergeant77365182017-05-05 04:02:33538
tsergeant2db36262017-05-15 02:47:53539 /**
540 * Returns all URLs in the given set of nodes and their immediate children.
541 * Note that these will be ordered by insertion order into the |itemIds|
542 * set, and that it is possible to duplicate a URL by passing in both the
543 * parent ID and child ID.
544 * @param {!Set<string>} itemIds
545 * @return {!Array<string>}
546 * @private
547 */
548 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10549 const urls = [];
550 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03551
tsergeant2db36262017-05-15 02:47:53552 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10553 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53554 if (node.url) {
555 urls.push(node.url);
556 } else {
557 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10558 const childNode = nodes[childId];
Dan Beamd1cca6e2019-01-03 02:46:27559 if (childNode.url) {
tsergeant2db36262017-05-15 02:47:53560 urls.push(childNode.url);
Dan Beamd1cca6e2019-01-03 02:46:27561 }
tsergeant2db36262017-05-15 02:47:53562 });
563 }
564 });
tsergeant13a466462017-05-15 01:21:03565
tsergeant2db36262017-05-15 02:47:53566 return urls;
567 },
568
569 /**
570 * @param {!Set<string>} itemIds
571 * @param {function(BookmarkNode):boolean} predicate
572 * @return {boolean} True if any node in |itemIds| returns true for
573 * |predicate|.
574 */
575 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10576 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53577
578 return Array.from(itemIds).some(function(id) {
579 return predicate(nodes[id]);
580 });
581 },
582
583 /**
tsergeant2437f992017-06-13 23:54:29584 * @param {!Set<string>} itemIds
585 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
586 * node.
Hector Carmonaa90ccca2019-05-17 18:25:22587 * @private
tsergeant2437f992017-06-13 23:54:29588 */
589 isSingleBookmark_: function(itemIds) {
590 return itemIds.size == 1 &&
591 this.containsMatchingNode_(itemIds, function(node) {
592 return !!node.url;
593 });
594 },
595
596 /**
Hector Carmonaa90ccca2019-05-17 18:25:22597 * @param {!Set<string>} itemIds
598 * @return {boolean}
599 * @private
600 */
601 isFolder_: function(itemIds) {
602 return itemIds.size == 1 &&
603 this.containsMatchingNode_(itemIds, node => !node.url);
604 },
605
606 /**
tsergeant2db36262017-05-15 02:47:53607 * @param {Command} command
608 * @return {string}
609 * @private
610 */
611 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10612 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53613 this.containsMatchingNode_(this.menuIds_, function(node) {
614 return !node.url;
615 });
dpapad111a34902017-09-12 16:51:10616 let label;
tsergeant2db36262017-05-15 02:47:53617 switch (command) {
618 case Command.EDIT:
Dan Beamd1cca6e2019-01-03 02:46:27619 if (this.menuIds_.size != 1) {
tsergeant2db36262017-05-15 02:47:53620 return '';
Dan Beamd1cca6e2019-01-03 02:46:27621 }
tsergeant2db36262017-05-15 02:47:53622
dpapad111a34902017-09-12 16:51:10623 const id = Array.from(this.menuIds_)[0];
624 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53625 label = itemUrl ? 'menuEdit' : 'menuRename';
626 break;
Hector Carmonad1223622019-08-27 19:44:09627 case Command.CUT:
628 label = 'menuCut';
629 break;
630 case Command.COPY:
631 label = 'menuCopy';
632 break;
tsergeantb7253e92017-07-04 02:59:22633 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53634 label = 'menuCopyURL';
635 break;
Hector Carmonad1223622019-08-27 19:44:09636 case Command.PASTE:
637 label = 'menuPaste';
638 break;
tsergeant2db36262017-05-15 02:47:53639 case Command.DELETE:
640 label = 'menuDelete';
641 break;
tsergeantd16c95a2017-07-14 04:49:43642 case Command.SHOW_IN_FOLDER:
643 label = 'menuShowInFolder';
644 break;
tsergeant2db36262017-05-15 02:47:53645 case Command.OPEN_NEW_TAB:
646 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
647 break;
648 case Command.OPEN_NEW_WINDOW:
649 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
650 break;
651 case Command.OPEN_INCOGNITO:
652 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
653 break;
Christopher Lam043c7cb2018-01-09 04:14:14654 case Command.SORT:
655 label = 'menuSort';
656 break;
657 case Command.ADD_BOOKMARK:
658 label = 'menuAddBookmark';
659 break;
660 case Command.ADD_FOLDER:
661 label = 'menuAddFolder';
662 break;
663 case Command.IMPORT:
664 label = 'menuImport';
665 break;
666 case Command.EXPORT:
667 label = 'menuExport';
668 break;
Christopher Lam34be14992018-01-17 11:01:28669 case Command.HELP_CENTER:
670 label = 'menuHelpCenter';
671 break;
tsergeant2db36262017-05-15 02:47:53672 }
Christopher Lam043c7cb2018-01-09 04:14:14673 assert(label);
tsergeant2db36262017-05-15 02:47:53674
675 return loadTimeData.getString(assert(label));
676 },
677
678 /**
679 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14680 * @return {string}
681 * @private
682 */
683 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10684 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14685 this.containsMatchingNode_(this.menuIds_, function(node) {
686 return !node.url;
687 });
688 switch (command) {
689 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10690 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14691 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
692 default:
693 return '';
694 }
695 },
696
697 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14698 computeMenuCommands_: function() {
699 switch (this.menuSource_) {
700 case MenuSource.ITEM:
701 case MenuSource.TREE:
702 return [
703 Command.EDIT,
Christopher Lam043c7cb2018-01-09 04:14:14704 Command.SHOW_IN_FOLDER,
705 Command.DELETE,
706 // <hr>
Hector Carmonad1223622019-08-27 19:44:09707 Command.CUT,
708 Command.COPY,
709 Command.COPY_URL,
710 Command.PASTE,
711 // <hr>
Christopher Lam043c7cb2018-01-09 04:14:14712 Command.OPEN_NEW_TAB,
713 Command.OPEN_NEW_WINDOW,
714 Command.OPEN_INCOGNITO,
715 ];
716 case MenuSource.TOOLBAR:
717 return [
718 Command.SORT,
719 // <hr>
720 Command.ADD_BOOKMARK,
721 Command.ADD_FOLDER,
722 // <hr>
723 Command.IMPORT,
724 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28725 // <hr>
726 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14727 ];
Christopher Lamfde61782018-01-11 04:27:07728 case MenuSource.LIST:
729 return [
730 Command.ADD_BOOKMARK,
731 Command.ADD_FOLDER,
732 ];
Christopher Lam043c7cb2018-01-09 04:14:14733 case MenuSource.NONE:
734 return [];
735 }
736 assert(false);
737 },
738
Christopher Lam430ef002018-01-18 10:08:50739 /**
740 * @return {boolean}
741 * @private
742 */
Christopher Lam043c7cb2018-01-09 04:14:14743 computeHasAnySublabel_: function() {
Dan Beamd1cca6e2019-01-03 02:46:27744 if (this.menuIds_ == undefined || this.menuCommands_ == undefined) {
Christopher Lam430ef002018-01-18 10:08:50745 return false;
Dan Beamd1cca6e2019-01-03 02:46:27746 }
calamity64e2012a2017-06-21 09:57:14747
Christopher Lam430ef002018-01-18 10:08:50748 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34749 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14750 },
751
752 /**
753 * @param {Command} command
Hector Carmonaa90ccca2019-05-17 18:25:22754 * @param {!Set<string>} itemIds
tsergeant2db36262017-05-15 02:47:53755 * @return {boolean}
756 * @private
757 */
tsergeant2437f992017-06-13 23:54:29758 showDividerAfter_: function(command, itemIds) {
Hector Carmonad1223622019-08-27 19:44:09759 switch (command) {
760 case Command.SORT:
761 case Command.ADD_FOLDER:
762 case Command.EXPORT:
763 return this.menuSource_ == MenuSource.TOOLBAR;
764 case Command.DELETE:
765 return this.globalCanEdit_;
766 case Command.PASTE:
767 return this.globalCanEdit_ || this.isSingleBookmark_(itemIds);
768 }
769 return false;
tsergeant2db36262017-05-15 02:47:53770 },
tsergeantb7253e92017-07-04 02:59:22771
772 /**
Hector Carmonaa90ccca2019-05-17 18:25:22773 * @param {!Set<string>} itemIds
774 * @param {string} histogram
775 * @param {number} command
776 * @private
777 */
778 recordCommandHistogram_: function(itemIds, histogram, command) {
779 if (command == Command.OPEN) {
780 command = this.isFolder_(itemIds) ? Command.OPEN_FOLDER :
781 Command.OPEN_BOOKMARK;
782 }
783
rbpottercd521682019-11-12 02:00:35784 this.browserProxy_.recordInHistogram(
785 histogram, command, Command.MAX_VALUE);
Hector Carmonaa90ccca2019-05-17 18:25:22786 },
787
788 /**
tsergeantb7253e92017-07-04 02:59:22789 * Show a toast with a bookmark |title| inserted into a label, with the
790 * title ellipsised if necessary.
791 * @param {!Promise<string>} labelPromise Promise which resolves with the
792 * label for the toast.
793 * @param {string} title Bookmark title to insert.
794 * @param {boolean} canUndo If true, shows an undo button in the toast.
795 * @private
796 */
Christopher Lam12860e72018-11-20 05:17:39797 showTitleToast_: async function(labelPromise, title, canUndo) {
798 const label = await labelPromise;
799 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
800 .map(function(p) {
801 // Make the bookmark name collapsible.
802 p.collapsible = !!p.arg;
803 return p;
804 });
tsergeantb7253e92017-07-04 02:59:22805
Esmael El-Moslimany89dd49b2019-03-19 20:43:56806 cr.toastManager.getInstance().showForStringPieces(pieces, canUndo);
tsergeantb7253e92017-07-04 02:59:22807 },
808
Hector Carmona79b07f02019-09-12 00:40:53809 /**
810 * @param {number} targetId
811 * @private
812 */
813 updateCanPaste_: function(targetId) {
814 return new Promise(resolve => {
815 chrome.bookmarkManagerPrivate.canPaste(`${targetId}`, result => {
816 this.canPaste_ = result;
817 resolve();
818 });
819 });
820 },
821
tsergeantb7253e92017-07-04 02:59:22822 ////////////////////////////////////////////////////////////////////////////
823 // Event handlers:
824
825 /**
826 * @param {Event} e
827 * @private
828 */
Hector Carmona79b07f02019-09-12 00:40:53829 onOpenCommandMenu_: async function(e) {
830 await this.updateCanPaste_(e.detail.source);
tsergeantb7253e92017-07-04 02:59:22831 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43832 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22833 } else {
tsergeantd16c95a2017-07-14 04:49:43834 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22835 }
rbpottercd521682019-11-12 02:00:35836 this.browserProxy_.recordInHistogram(
Christopher Lamed175322018-01-18 14:54:49837 'BookmarkManager.CommandMenuOpened', e.detail.source,
838 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22839 },
840
841 /**
842 * @param {Event} e
843 * @private
844 */
845 onCommandClick_: function(e) {
846 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47847 /** @type {Command} */ (
848 Number(e.currentTarget.getAttribute('command'))),
849 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22850 this.closeCommandMenu();
851 },
852
853 /**
854 * @param {!Event} e
855 * @private
856 */
857 onKeydown_: function(e) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04858 const path = e.composedPath();
859 if (path[0].tagName == 'INPUT') {
860 return;
861 }
862 if ((e.target == document.body ||
863 path.some(el => el.tagName == 'BOOKMARKS-TOOLBAR')) &&
Tim Sergeant109279fe2017-07-20 03:07:17864 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
Esmael El-Moslimanyf50f28b2019-03-21 03:06:04865 this.handleKeyEvent(e, this.getState().selection.items);
Tim Sergeant109279fe2017-07-20 03:07:17866 }
tsergeantb7253e92017-07-04 02:59:22867 },
868
869 /**
870 * Close the menu on mousedown so clicks can propagate to the underlying UI.
871 * This allows the user to right click the list while a context menu is
872 * showing and get another context menu.
873 * @param {Event} e
874 * @private
875 */
876 onMenuMousedown_: function(e) {
Dan Beamd1cca6e2019-01-03 02:46:27877 if (e.path[0].tagName != 'DIALOG') {
tsergeantb7253e92017-07-04 02:59:22878 return;
Dan Beamd1cca6e2019-01-03 02:46:27879 }
tsergeantb7253e92017-07-04 02:59:22880
881 this.closeCommandMenu();
882 },
883
884 /** @private */
885 onOpenCancelTap_: function() {
886 this.$.openDialog.get().cancel();
887 },
888
889 /** @private */
890 onOpenConfirmTap_: function() {
891 this.confirmOpenCallback_();
892 this.$.openDialog.get().close();
893 },
tsergeant2db36262017-05-15 02:47:53894 });
895
896 /** @private {bookmarks.CommandManager} */
897 CommandManager.instance_ = null;
898
899 /** @return {!bookmarks.CommandManager} */
900 CommandManager.getInstance = function() {
901 return assert(CommandManager.instance_);
902 };
903
904 return {
905 CommandManager: CommandManager,
906 };
tsergeant77365182017-05-05 04:02:33907});