blob: a9e4ede4656eef5c44ac5127d0dae8cb38a482bd [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>} */
dpapad43083f02018-06-14 22:02:4026 menuIds_: Object,
calamity64e2012a2017-06-21 09:57:1427
28 /** @private */
29 hasAnySublabel_: {
30 type: Boolean,
31 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1432 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1433 },
tsergeant2437f992017-06-13 23:54:2934
Christopher Lam043c7cb2018-01-09 04:14:1435 /**
36 * Indicates where the context menu was opened from. Will be NONE if
37 * menu is not open, indicating that commands are from keyboard shortcuts
38 * or elsewhere in the UI.
39 * @private {MenuSource}
40 */
dpapad43083f02018-06-14 22:02:4041 menuSource_: {
42 type: Number,
43 value: MenuSource.NONE,
44 },
Christopher Lam043c7cb2018-01-09 04:14:1445
tsergeant2437f992017-06-13 23:54:2946 /** @private */
47 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0348 },
49
tsergeant7fb9e13f2017-06-26 06:35:1950 /** @private {?Function} */
51 confirmOpenCallback_: null,
52
tsergeant2db36262017-05-15 02:47:5353 attached: function() {
54 assert(CommandManager.instance_ == null);
55 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3356
tsergeant2437f992017-06-13 23:54:2957 this.watch('globalCanEdit_', function(state) {
58 return state.prefs.canEdit;
59 });
60 this.updateFromStore();
61
Tim Sergeanta2233c812017-07-26 03:02:4762 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
63 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3564
65 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3566 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
67
Tim Sergeant40d2d2c2017-07-20 01:37:0668 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3569 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
70 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
71
Chris Hallf62ade22018-10-11 05:37:5772 // Note: the undo shortcut is also defined in md_bookmarks_ui.cc
73 // TODO(b/893033): de-duplicate shortcut by moving all shortcut
74 // definitions from JS to C++.
tsergeant0292e51a2017-06-16 03:44:3575 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
76 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4177
78 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
79 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2280
81 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
82 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
83 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
Christopher Lamf4a16fd2018-02-01 01:47:1184
85 /** @private {!Map<string, Function>} */
86 this.boundListeners_ = new Map();
87
88 const addDocumentListener = (eventName, handler) => {
89 assert(!this.boundListeners_.has(eventName));
90 const boundListener = handler.bind(this);
91 this.boundListeners_.set(eventName, boundListener);
92 document.addEventListener(eventName, boundListener);
93 };
94 addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
95 addDocumentListener('keydown', this.onKeydown_);
96
97 const addDocumentListenerForCommand = (eventName, command) => {
98 addDocumentListener(eventName, (e) => {
99 if (e.path[0].tagName == 'INPUT')
100 return;
101
102 const items = this.getState().selection.items;
103 if (this.canExecute(command, items))
104 this.handle(command, items);
105 });
106 };
107 addDocumentListenerForCommand('command-undo', Command.UNDO);
108 addDocumentListenerForCommand('cut', Command.CUT);
109 addDocumentListenerForCommand('copy', Command.COPY);
110 addDocumentListenerForCommand('paste', Command.PASTE);
tsergeant2db36262017-05-15 02:47:53111 },
tsergeant77365182017-05-05 04:02:33112
tsergeant2db36262017-05-15 02:47:53113 detached: function() {
114 CommandManager.instance_ = null;
Christopher Lamf4a16fd2018-02-01 01:47:11115 this.boundListeners_.forEach(
116 (handler, eventName) =>
117 document.removeEventListener(eventName, handler));
tsergeant2db36262017-05-15 02:47:53118 },
tsergeant77365182017-05-05 04:02:33119
tsergeant2db36262017-05-15 02:47:53120 /**
121 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28122 * Commands will execute on |items| if given, or on the currently selected
123 * items.
tsergeant2db36262017-05-15 02:47:53124 * @param {number} x
125 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43126 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28127 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53128 */
tsergeantd16c95a2017-07-14 04:49:43129 openCommandMenuAtPosition: function(x, y, source, items) {
130 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28131 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58132
dpapad111a34902017-09-12 16:51:10133 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27134 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
135 // Ensure that the menu is fully rendered before trying to position it.
136 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58137 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24138 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58139 dropdown.showAtPosition({top: y, left: x});
140 });
tsergeant2db36262017-05-15 02:47:53141 },
tsergeant77365182017-05-05 04:02:33142
tsergeant2db36262017-05-15 02:47:53143 /**
144 * Display the command context menu positioned to cover the |target|
145 * element. Commands will execute on the currently selected items.
146 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43147 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53148 */
tsergeantd16c95a2017-07-14 04:49:43149 openCommandMenuAtElement: function(target, source) {
150 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53151 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58152
dpapad111a34902017-09-12 16:51:10153 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27154 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
155 // Ensure that the menu is fully rendered before trying to position it.
156 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58157 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24158 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58159 dropdown.showAt(target);
160 });
tsergeant2db36262017-05-15 02:47:53161 },
tsergeant77365182017-05-05 04:02:33162
tsergeant2db36262017-05-15 02:47:53163 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02164 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43165 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27166 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53167 },
tsergeant77365182017-05-05 04:02:33168
tsergeant2db36262017-05-15 02:47:53169 ////////////////////////////////////////////////////////////////////////////
170 // Command handlers:
tsergeant77365182017-05-05 04:02:33171
tsergeant2db36262017-05-15 02:47:53172 /**
173 * Determine if the |command| can be executed with the given |itemIds|.
174 * Commands which appear in the context menu should be implemented
175 * separately using `isCommandVisible_` and `isCommandEnabled_`.
176 * @param {Command} command
177 * @param {!Set<string>} itemIds
178 * @return {boolean}
179 */
180 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10181 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34182 switch (command) {
183 case Command.OPEN:
184 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58185 case Command.UNDO:
186 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29187 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41188 case Command.SELECT_ALL:
189 case Command.DESELECT_ALL:
190 return true;
tsergeantb7253e92017-07-04 02:59:22191 case Command.COPY:
192 return itemIds.size > 0;
193 case Command.CUT:
194 return itemIds.size > 0 &&
195 !this.containsMatchingNode_(itemIds, function(node) {
196 return !bookmarks.util.canEditNode(state, node.id);
197 });
198 case Command.PASTE:
199 return state.search.term == '' &&
200 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34201 default:
202 return this.isCommandVisible_(command, itemIds) &&
203 this.isCommandEnabled_(command, itemIds);
204 }
tsergeant2db36262017-05-15 02:47:53205 },
tsergeant13a466462017-05-15 01:21:03206
tsergeant2db36262017-05-15 02:47:53207 /**
208 * @param {Command} command
209 * @param {!Set<string>} itemIds
210 * @return {boolean} True if the command should be visible in the context
211 * menu.
212 */
213 isCommandVisible_: function(command, itemIds) {
214 switch (command) {
215 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29216 return itemIds.size == 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22217 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29218 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53219 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29220 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43221 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14222 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43223 this.getState().search.term != '' &&
224 !this.containsMatchingNode_(itemIds, function(node) {
225 return !node.parentId || node.parentId == ROOT_NODE_ID;
226 });
tsergeant2db36262017-05-15 02:47:53227 case Command.OPEN_NEW_TAB:
228 case Command.OPEN_NEW_WINDOW:
229 case Command.OPEN_INCOGNITO:
230 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14231 case Command.ADD_BOOKMARK:
232 case Command.ADD_FOLDER:
233 case Command.SORT:
234 case Command.EXPORT:
235 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28236 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14237 return true;
tsergeantf1ffc892017-05-05 07:43:43238 }
Christopher Lam34be14992018-01-17 11:01:28239 return assert(false);
tsergeant2db36262017-05-15 02:47:53240 },
tsergeantf1ffc892017-05-05 07:43:43241
tsergeant2db36262017-05-15 02:47:53242 /**
243 * @param {Command} command
244 * @param {!Set<string>} itemIds
245 * @return {boolean} True if the command should be clickable in the context
246 * menu.
247 */
248 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14249 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53250 switch (command) {
tsergeant2437f992017-06-13 23:54:29251 case Command.EDIT:
252 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29253 return !this.containsMatchingNode_(itemIds, function(node) {
254 return !bookmarks.util.canEditNode(state, node.id);
255 });
tsergeant2db36262017-05-15 02:47:53256 case Command.OPEN_NEW_TAB:
257 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53258 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02259 case Command.OPEN_INCOGNITO:
260 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14261 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02262 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14263 case Command.SORT:
264 return this.canChangeList_() &&
265 state.nodes[state.selectedFolder].children.length > 1;
266 case Command.ADD_BOOKMARK:
267 case Command.ADD_FOLDER:
268 return this.canChangeList_();
269 case Command.IMPORT:
270 return this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53271 default:
272 return true;
273 }
274 },
tsergeant13a466462017-05-15 01:21:03275
tsergeant2db36262017-05-15 02:47:53276 /**
Christopher Lam043c7cb2018-01-09 04:14:14277 * Returns whether the currently displayed bookmarks list can be changed.
278 * @private
279 * @return {boolean}
280 */
281 canChangeList_: function() {
282 const state = this.getState();
283 return state.search.term == '' &&
284 bookmarks.util.canReorderChildren(state, state.selectedFolder);
285 },
286
287 /**
tsergeant2db36262017-05-15 02:47:53288 * @param {Command} command
289 * @param {!Set<string>} itemIds
290 */
291 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10292 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53293 switch (command) {
dpapad111a34902017-09-12 16:51:10294 case Command.EDIT: {
Scott Chen657ebb7b2018-12-20 01:49:54295 const id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53296 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35297 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53298 break;
dpapad111a34902017-09-12 16:51:10299 }
tsergeantb7253e92017-07-04 02:59:22300 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10301 case Command.COPY: {
Scott Chen657ebb7b2018-12-20 01:49:54302 const idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34303 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10304 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22305 if (command == Command.COPY_URL) {
306 labelPromise =
307 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18308 } else if (idList.length == 1) {
309 labelPromise =
310 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22311 } else {
312 labelPromise = cr.sendWithPromise(
313 'getPluralString', 'toastItemsCopied', idList.length);
314 }
315
316 this.showTitleToast_(
317 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34318 });
tsergeant2db36262017-05-15 02:47:53319 break;
dpapad111a34902017-09-12 16:51:10320 }
321 case Command.SHOW_IN_FOLDER: {
Scott Chen657ebb7b2018-12-20 01:49:54322 const id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43323 this.dispatch(bookmarks.actions.selectFolder(
324 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57325 bookmarks.DialogFocusManager.getInstance().clearFocus();
326 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43327 break;
dpapad111a34902017-09-12 16:51:10328 }
329 case Command.DELETE: {
Scott Chen657ebb7b2018-12-20 01:49:54330 const idList = Array.from(this.minimizeDeletionSet_(itemIds));
dpapad111a34902017-09-12 16:51:10331 const title = state.nodes[idList[0]].title;
332 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18333
334 if (idList.length == 1) {
335 labelPromise =
336 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
337 } else {
338 labelPromise = cr.sendWithPromise(
339 'getPluralString', 'toastItemsDeleted', idList.length);
340 }
341
dpapad325bf2f12017-07-26 18:47:34342 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22343 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34344 });
tsergeant2db36262017-05-15 02:47:53345 break;
dpapad111a34902017-09-12 16:51:10346 }
calamity2d4b5502017-05-29 03:57:58347 case Command.UNDO:
348 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58349 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58350 break;
351 case Command.REDO:
352 chrome.bookmarkManagerPrivate.redo();
353 break;
tsergeant2db36262017-05-15 02:47:53354 case Command.OPEN_NEW_TAB:
355 case Command.OPEN_NEW_WINDOW:
356 case Command.OPEN_INCOGNITO:
357 this.openUrls_(this.expandUrls_(itemIds), command);
358 break;
tsergeant6c5ad90a2017-05-19 14:12:34359 case Command.OPEN:
dpapad111a34902017-09-12 16:51:10360 const isFolder = itemIds.size == 1 &&
tsergeant6c5ad90a2017-05-19 14:12:34361 this.containsMatchingNode_(itemIds, function(node) {
362 return !node.url;
363 });
364 if (isFolder) {
dpapad111a34902017-09-12 16:51:10365 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35366 this.dispatch(
367 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34368 } else {
369 this.openUrls_(this.expandUrls_(itemIds), command);
370 }
371 break;
tsergeant679159f2017-06-16 06:58:41372 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10373 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41374 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
375 break;
376 case Command.DESELECT_ALL:
377 this.dispatch(bookmarks.actions.deselectItems());
378 break;
tsergeantb7253e92017-07-04 02:59:22379 case Command.CUT:
380 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
381 break;
382 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10383 const selectedFolder = state.selectedFolder;
384 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57385 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22386 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57387 selectedFolder, Array.from(selectedItems),
388 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22389 break;
Christopher Lam043c7cb2018-01-09 04:14:14390 case Command.SORT:
391 chrome.bookmarkManagerPrivate.sortChildren(
392 assert(state.selectedFolder));
393 bookmarks.ToastManager.getInstance().show(
394 loadTimeData.getString('toastFolderSorted'), true);
395 break;
396 case Command.ADD_BOOKMARK:
397 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
398 .showAddDialog(false, assert(state.selectedFolder));
399 break;
400 case Command.ADD_FOLDER:
401 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
402 .showAddDialog(true, assert(state.selectedFolder));
403 break;
404 case Command.IMPORT:
405 chrome.bookmarks.import();
406 break;
407 case Command.EXPORT:
408 chrome.bookmarks.export();
409 break;
Christopher Lam34be14992018-01-17 11:01:28410 case Command.HELP_CENTER:
411 window.open('https://support.google.com/chrome/?p=bookmarks');
412 break;
tsergeant6c5ad90a2017-05-19 14:12:34413 default:
414 assert(false);
tsergeant2db36262017-05-15 02:47:53415 }
Tim Sergeanta2233c812017-07-26 03:02:47416
417 bookmarks.util.recordEnumHistogram(
418 'BookmarkManager.CommandExecuted', command, Command.MAX_VALUE);
tsergeant2db36262017-05-15 02:47:53419 },
tsergeant13a466462017-05-15 01:21:03420
tsergeant6c3a6df2017-06-06 23:53:02421 /**
tsergeant0292e51a2017-06-16 03:44:35422 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02423 * @param {!Set<string>} itemIds
424 * @return {boolean} True if the event was handled, triggering a keyboard
425 * shortcut.
426 */
427 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10428 for (const commandTuple of this.shortcuts_) {
429 const command = /** @type {Command} */ (commandTuple[0]);
430 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47431 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
432 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
433 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02434
Tim Sergeanta2233c812017-07-26 03:02:47435 bookmarks.util.recordEnumHistogram(
436 'BookmarkManager.CommandExecutedFromKeyboard', command,
437 Command.MAX_VALUE);
tsergeant6c3a6df2017-06-06 23:53:02438 e.stopPropagation();
439 e.preventDefault();
440 return true;
441 }
442 }
443
444 return false;
445 },
446
tsergeant2db36262017-05-15 02:47:53447 ////////////////////////////////////////////////////////////////////////////
448 // Private functions:
449
450 /**
tsergeant0292e51a2017-06-16 03:44:35451 * Register a keyboard shortcut for a command.
452 * @param {Command} command Command that the shortcut will trigger.
453 * @param {string} shortcut Keyboard shortcut, using the syntax of
454 * cr/ui/command.js.
455 * @param {string=} macShortcut If set, enables a replacement shortcut for
456 * Mac.
457 */
458 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10459 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47460 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35461 },
462
463 /**
tsergeant2db36262017-05-15 02:47:53464 * Minimize the set of |itemIds| by removing any node which has an ancestor
465 * node already in the set. This ensures that instead of trying to delete
466 * both a node and its descendant, we will only try to delete the topmost
467 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
468 * call.
469 * @param {!Set<string>} itemIds
470 * @return {!Set<string>}
471 */
472 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10473 const minimizedSet = new Set();
474 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53475 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10476 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53477 while (currentId != ROOT_NODE_ID) {
478 currentId = assert(nodes[currentId].parentId);
479 if (itemIds.has(currentId))
480 return;
481 }
482 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03483 });
tsergeant2db36262017-05-15 02:47:53484 return minimizedSet;
485 },
tsergeant13a466462017-05-15 01:21:03486
tsergeant2db36262017-05-15 02:47:53487 /**
tsergeant7fb9e13f2017-06-26 06:35:19488 * Open the given |urls| in response to a |command|. May show a confirmation
489 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53490 * @param {!Array<string>} urls
491 * @param {Command} command
492 * @private
493 */
494 openUrls_: function(urls, command) {
495 assert(
tsergeant6c5ad90a2017-05-19 14:12:34496 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53497 command == Command.OPEN_NEW_WINDOW ||
498 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03499
tsergeant2db36262017-05-15 02:47:53500 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09501 return;
tsergeantfad224f2017-05-05 04:42:09502
dpapad111a34902017-09-12 16:51:10503 const openUrlsCallback = function() {
504 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19505 if (command == Command.OPEN_NEW_WINDOW || incognito) {
506 chrome.windows.create({url: urls, incognito: incognito});
507 } else {
508 if (command == Command.OPEN)
509 chrome.tabs.create({url: urls.shift(), active: true});
510 urls.forEach(function(url) {
511 chrome.tabs.create({url: url, active: false});
512 });
513 }
514 };
515
516 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
517 openUrlsCallback();
518 return;
tsergeant2db36262017-05-15 02:47:53519 }
tsergeant7fb9e13f2017-06-26 06:35:19520
521 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10522 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16523 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19524 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58525
526 bookmarks.DialogFocusManager.getInstance().showDialog(
527 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53528 },
tsergeant77365182017-05-05 04:02:33529
tsergeant2db36262017-05-15 02:47:53530 /**
531 * Returns all URLs in the given set of nodes and their immediate children.
532 * Note that these will be ordered by insertion order into the |itemIds|
533 * set, and that it is possible to duplicate a URL by passing in both the
534 * parent ID and child ID.
535 * @param {!Set<string>} itemIds
536 * @return {!Array<string>}
537 * @private
538 */
539 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10540 const urls = [];
541 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03542
tsergeant2db36262017-05-15 02:47:53543 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10544 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53545 if (node.url) {
546 urls.push(node.url);
547 } else {
548 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10549 const childNode = nodes[childId];
tsergeant2db36262017-05-15 02:47:53550 if (childNode.url)
551 urls.push(childNode.url);
552 });
553 }
554 });
tsergeant13a466462017-05-15 01:21:03555
tsergeant2db36262017-05-15 02:47:53556 return urls;
557 },
558
559 /**
560 * @param {!Set<string>} itemIds
561 * @param {function(BookmarkNode):boolean} predicate
562 * @return {boolean} True if any node in |itemIds| returns true for
563 * |predicate|.
564 */
565 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10566 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53567
568 return Array.from(itemIds).some(function(id) {
569 return predicate(nodes[id]);
570 });
571 },
572
573 /**
tsergeant2437f992017-06-13 23:54:29574 * @param {!Set<string>} itemIds
575 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
576 * node.
577 */
578 isSingleBookmark_: function(itemIds) {
579 return itemIds.size == 1 &&
580 this.containsMatchingNode_(itemIds, function(node) {
581 return !!node.url;
582 });
583 },
584
585 /**
tsergeant2db36262017-05-15 02:47:53586 * @param {Command} command
587 * @return {string}
588 * @private
589 */
590 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10591 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53592 this.containsMatchingNode_(this.menuIds_, function(node) {
593 return !node.url;
594 });
dpapad111a34902017-09-12 16:51:10595 let label;
tsergeant2db36262017-05-15 02:47:53596 switch (command) {
597 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02598 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53599 return '';
600
dpapad111a34902017-09-12 16:51:10601 const id = Array.from(this.menuIds_)[0];
602 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53603 label = itemUrl ? 'menuEdit' : 'menuRename';
604 break;
tsergeantb7253e92017-07-04 02:59:22605 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53606 label = 'menuCopyURL';
607 break;
608 case Command.DELETE:
609 label = 'menuDelete';
610 break;
tsergeantd16c95a2017-07-14 04:49:43611 case Command.SHOW_IN_FOLDER:
612 label = 'menuShowInFolder';
613 break;
tsergeant2db36262017-05-15 02:47:53614 case Command.OPEN_NEW_TAB:
615 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
616 break;
617 case Command.OPEN_NEW_WINDOW:
618 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
619 break;
620 case Command.OPEN_INCOGNITO:
621 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
622 break;
Christopher Lam043c7cb2018-01-09 04:14:14623 case Command.SORT:
624 label = 'menuSort';
625 break;
626 case Command.ADD_BOOKMARK:
627 label = 'menuAddBookmark';
628 break;
629 case Command.ADD_FOLDER:
630 label = 'menuAddFolder';
631 break;
632 case Command.IMPORT:
633 label = 'menuImport';
634 break;
635 case Command.EXPORT:
636 label = 'menuExport';
637 break;
Christopher Lam34be14992018-01-17 11:01:28638 case Command.HELP_CENTER:
639 label = 'menuHelpCenter';
640 break;
tsergeant2db36262017-05-15 02:47:53641 }
Christopher Lam043c7cb2018-01-09 04:14:14642 assert(label);
tsergeant2db36262017-05-15 02:47:53643
644 return loadTimeData.getString(assert(label));
645 },
646
647 /**
648 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14649 * @return {string}
650 * @private
651 */
652 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10653 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14654 this.containsMatchingNode_(this.menuIds_, function(node) {
655 return !node.url;
656 });
657 switch (command) {
658 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10659 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14660 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
661 default:
662 return '';
663 }
664 },
665
666 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14667 computeMenuCommands_: function() {
668 switch (this.menuSource_) {
669 case MenuSource.ITEM:
670 case MenuSource.TREE:
671 return [
672 Command.EDIT,
673 Command.COPY_URL,
674 Command.SHOW_IN_FOLDER,
675 Command.DELETE,
676 // <hr>
677 Command.OPEN_NEW_TAB,
678 Command.OPEN_NEW_WINDOW,
679 Command.OPEN_INCOGNITO,
680 ];
681 case MenuSource.TOOLBAR:
682 return [
683 Command.SORT,
684 // <hr>
685 Command.ADD_BOOKMARK,
686 Command.ADD_FOLDER,
687 // <hr>
688 Command.IMPORT,
689 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28690 // <hr>
691 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14692 ];
Christopher Lamfde61782018-01-11 04:27:07693 case MenuSource.LIST:
694 return [
695 Command.ADD_BOOKMARK,
696 Command.ADD_FOLDER,
697 ];
Christopher Lam043c7cb2018-01-09 04:14:14698 case MenuSource.NONE:
699 return [];
700 }
701 assert(false);
702 },
703
Christopher Lam430ef002018-01-18 10:08:50704 /**
705 * @return {boolean}
706 * @private
707 */
Christopher Lam043c7cb2018-01-09 04:14:14708 computeHasAnySublabel_: function() {
dpapad43083f02018-06-14 22:02:40709 if (this.menuIds_ == undefined || this.menuCommands_ == undefined)
Christopher Lam430ef002018-01-18 10:08:50710 return false;
calamity64e2012a2017-06-21 09:57:14711
Christopher Lam430ef002018-01-18 10:08:50712 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34713 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14714 },
715
716 /**
717 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53718 * @return {boolean}
719 * @private
720 */
tsergeant2437f992017-06-13 23:54:29721 showDividerAfter_: function(command, itemIds) {
Christopher Lam34be14992018-01-17 11:01:28722 return ((command == Command.SORT || command == Command.ADD_FOLDER ||
723 command == Command.EXPORT) &&
Christopher Lam043c7cb2018-01-09 04:14:14724 this.menuSource_ == MenuSource.TOOLBAR) ||
725 (command == Command.DELETE &&
726 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)));
tsergeant2db36262017-05-15 02:47:53727 },
tsergeantb7253e92017-07-04 02:59:22728
729 /**
730 * Show a toast with a bookmark |title| inserted into a label, with the
731 * title ellipsised if necessary.
732 * @param {!Promise<string>} labelPromise Promise which resolves with the
733 * label for the toast.
734 * @param {string} title Bookmark title to insert.
735 * @param {boolean} canUndo If true, shows an undo button in the toast.
736 * @private
737 */
Christopher Lam12860e72018-11-20 05:17:39738 showTitleToast_: async function(labelPromise, title, canUndo) {
739 const label = await labelPromise;
740 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
741 .map(function(p) {
742 // Make the bookmark name collapsible.
743 p.collapsible = !!p.arg;
744 return p;
745 });
tsergeantb7253e92017-07-04 02:59:22746
Christopher Lam12860e72018-11-20 05:17:39747 bookmarks.ToastManager.getInstance().showForStringPieces(pieces, canUndo);
tsergeantb7253e92017-07-04 02:59:22748 },
749
750 ////////////////////////////////////////////////////////////////////////////
751 // Event handlers:
752
753 /**
754 * @param {Event} e
755 * @private
756 */
Christopher Lam043c7cb2018-01-09 04:14:14757 onOpenCommandMenu_: function(e) {
tsergeantb7253e92017-07-04 02:59:22758 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43759 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22760 } else {
tsergeantd16c95a2017-07-14 04:49:43761 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22762 }
Christopher Lamed175322018-01-18 14:54:49763 bookmarks.util.recordEnumHistogram(
764 'BookmarkManager.CommandMenuOpened', e.detail.source,
765 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22766 },
767
768 /**
769 * @param {Event} e
770 * @private
771 */
772 onCommandClick_: function(e) {
773 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47774 /** @type {Command} */ (
775 Number(e.currentTarget.getAttribute('command'))),
776 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22777 this.closeCommandMenu();
778 },
779
780 /**
781 * @param {!Event} e
782 * @private
783 */
784 onKeydown_: function(e) {
dpapad111a34902017-09-12 16:51:10785 const selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17786 if (e.target == document.body &&
787 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22788 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17789 }
tsergeantb7253e92017-07-04 02:59:22790 },
791
792 /**
793 * Close the menu on mousedown so clicks can propagate to the underlying UI.
794 * This allows the user to right click the list while a context menu is
795 * showing and get another context menu.
796 * @param {Event} e
797 * @private
798 */
799 onMenuMousedown_: function(e) {
Christopher Lamf65fe25c2018-04-18 08:59:49800 if (e.path[0].tagName != 'DIALOG')
tsergeantb7253e92017-07-04 02:59:22801 return;
802
803 this.closeCommandMenu();
804 },
805
806 /** @private */
807 onOpenCancelTap_: function() {
808 this.$.openDialog.get().cancel();
809 },
810
811 /** @private */
812 onOpenConfirmTap_: function() {
813 this.confirmOpenCallback_();
814 this.$.openDialog.get().close();
815 },
tsergeant2db36262017-05-15 02:47:53816 });
817
818 /** @private {bookmarks.CommandManager} */
819 CommandManager.instance_ = null;
820
821 /** @return {!bookmarks.CommandManager} */
822 CommandManager.getInstance = function() {
823 return assert(CommandManager.instance_);
824 };
825
826 return {
827 CommandManager: CommandManager,
828 };
tsergeant77365182017-05-05 04:02:33829});