blob: 668a44975227bed73b1f7d49ae6141023cadc509 [file] [log] [blame]
tsergeant77365182017-05-05 04:02:331// Copyright 2017 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
tsergeant2db36262017-05-15 02:47:535/**
6 * @fileoverview Element which shows context menus and handles keyboard
7 * shortcuts.
8 */
9cr.define('bookmarks', function() {
tsergeant77365182017-05-05 04:02:3310
dpapad111a34902017-09-12 16:51:1011 const CommandManager = Polymer({
tsergeant2db36262017-05-15 02:47:5312 is: 'bookmarks-command-manager',
tsergeant77365182017-05-05 04:02:3313
tsergeant2db36262017-05-15 02:47:5314 behaviors: [
15 bookmarks.StoreClient,
16 ],
17
18 properties: {
19 /** @private {!Array<Command>} */
20 menuCommands_: {
21 type: Array,
Christopher Lam043c7cb2018-01-09 04:14:1422 computed: 'computeMenuCommands_(menuSource_)',
tsergeant13a466462017-05-15 01:21:0323 },
tsergeant2db36262017-05-15 02:47:5324
tsergeant2437f992017-06-13 23:54:2925 /** @private {Set<string>} */
calamity64e2012a2017-06-21 09:57:1426 menuIds_: {
27 type: Object,
calamity64e2012a2017-06-21 09:57:1428 },
29
30 /** @private */
31 hasAnySublabel_: {
32 type: Boolean,
33 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1434 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1435 },
tsergeant2437f992017-06-13 23:54:2936
Christopher Lam043c7cb2018-01-09 04:14:1437 /**
38 * Indicates where the context menu was opened from. Will be NONE if
39 * menu is not open, indicating that commands are from keyboard shortcuts
40 * or elsewhere in the UI.
41 * @private {MenuSource}
42 */
43 menuSource_: MenuSource.NONE,
44
tsergeant2437f992017-06-13 23:54:2945 /** @private */
46 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0347 },
48
tsergeant7fb9e13f2017-06-26 06:35:1949 /** @private {?Function} */
50 confirmOpenCallback_: null,
51
tsergeant2db36262017-05-15 02:47:5352 attached: function() {
53 assert(CommandManager.instance_ == null);
54 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3355
tsergeant2437f992017-06-13 23:54:2956 this.watch('globalCanEdit_', function(state) {
57 return state.prefs.canEdit;
58 });
59 this.updateFromStore();
60
tsergeant2db36262017-05-15 02:47:5361 /** @private {function(!Event)} */
Christopher Lam043c7cb2018-01-09 04:14:1462 this.boundOnOpenCommandMenu_ = this.onOpenCommandMenu_.bind(this);
63 document.addEventListener(
64 'open-command-menu', this.boundOnOpenCommandMenu_);
tsergeantfad224f2017-05-05 04:42:0965
calamityefe477352017-06-07 06:44:5866 /** @private {function()} */
dpapad325bf2f12017-07-26 18:47:3467 this.boundOnCommandUndo_ = () => {
calamityefe477352017-06-07 06:44:5868 this.handle(Command.UNDO, new Set());
dpapad325bf2f12017-07-26 18:47:3469 };
calamityefe477352017-06-07 06:44:5870 document.addEventListener('command-undo', this.boundOnCommandUndo_);
71
tsergeant2db36262017-05-15 02:47:5372 /** @private {function(!Event)} */
73 this.boundOnKeydown_ = this.onKeydown_.bind(this);
74 document.addEventListener('keydown', this.boundOnKeydown_);
tsergeantfad224f2017-05-05 04:42:0975
Tim Sergeanta2233c812017-07-26 03:02:4776 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
77 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3578
79 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3580 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
81
Tim Sergeant40d2d2c2017-07-20 01:37:0682 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3583 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
84 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
85
86 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
87 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4188
89 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
90 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2291
92 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
93 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
94 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
tsergeant2db36262017-05-15 02:47:5395 },
tsergeant77365182017-05-05 04:02:3396
tsergeant2db36262017-05-15 02:47:5397 detached: function() {
98 CommandManager.instance_ = null;
Christopher Lam043c7cb2018-01-09 04:14:1499 document.removeEventListener(
100 'open-command-menu', this.boundOnOpenCommandMenu_);
calamityefe477352017-06-07 06:44:58101 document.removeEventListener('command-undo', this.boundOnCommandUndo_);
tsergeant2db36262017-05-15 02:47:53102 document.removeEventListener('keydown', this.boundOnKeydown_);
103 },
tsergeant77365182017-05-05 04:02:33104
tsergeant2db36262017-05-15 02:47:53105 /**
106 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28107 * Commands will execute on |items| if given, or on the currently selected
108 * items.
tsergeant2db36262017-05-15 02:47:53109 * @param {number} x
110 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43111 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28112 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53113 */
tsergeantd16c95a2017-07-14 04:49:43114 openCommandMenuAtPosition: function(x, y, source, items) {
115 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28116 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58117
dpapad111a34902017-09-12 16:51:10118 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27119 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
120 // Ensure that the menu is fully rendered before trying to position it.
121 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58122 bookmarks.DialogFocusManager.getInstance().showDialog(
123 dropdown, function() {
124 dropdown.showAtPosition({top: y, left: x});
125 });
tsergeant2db36262017-05-15 02:47:53126 },
tsergeant77365182017-05-05 04:02:33127
tsergeant2db36262017-05-15 02:47:53128 /**
129 * Display the command context menu positioned to cover the |target|
130 * element. Commands will execute on the currently selected items.
131 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43132 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53133 */
tsergeantd16c95a2017-07-14 04:49:43134 openCommandMenuAtElement: function(target, source) {
135 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53136 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58137
dpapad111a34902017-09-12 16:51:10138 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27139 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
140 // Ensure that the menu is fully rendered before trying to position it.
141 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58142 bookmarks.DialogFocusManager.getInstance().showDialog(
143 dropdown, function() {
144 dropdown.showAt(target);
145 });
tsergeant2db36262017-05-15 02:47:53146 },
tsergeant77365182017-05-05 04:02:33147
tsergeant2db36262017-05-15 02:47:53148 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02149 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43150 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27151 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53152 },
tsergeant77365182017-05-05 04:02:33153
tsergeant2db36262017-05-15 02:47:53154 ////////////////////////////////////////////////////////////////////////////
155 // Command handlers:
tsergeant77365182017-05-05 04:02:33156
tsergeant2db36262017-05-15 02:47:53157 /**
158 * Determine if the |command| can be executed with the given |itemIds|.
159 * Commands which appear in the context menu should be implemented
160 * separately using `isCommandVisible_` and `isCommandEnabled_`.
161 * @param {Command} command
162 * @param {!Set<string>} itemIds
163 * @return {boolean}
164 */
165 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10166 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34167 switch (command) {
168 case Command.OPEN:
169 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58170 case Command.UNDO:
171 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29172 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41173 case Command.SELECT_ALL:
174 case Command.DESELECT_ALL:
175 return true;
tsergeantb7253e92017-07-04 02:59:22176 case Command.COPY:
177 return itemIds.size > 0;
178 case Command.CUT:
179 return itemIds.size > 0 &&
180 !this.containsMatchingNode_(itemIds, function(node) {
181 return !bookmarks.util.canEditNode(state, node.id);
182 });
183 case Command.PASTE:
184 return state.search.term == '' &&
185 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34186 default:
187 return this.isCommandVisible_(command, itemIds) &&
188 this.isCommandEnabled_(command, itemIds);
189 }
tsergeant2db36262017-05-15 02:47:53190 },
tsergeant13a466462017-05-15 01:21:03191
tsergeant2db36262017-05-15 02:47:53192 /**
193 * @param {Command} command
194 * @param {!Set<string>} itemIds
195 * @return {boolean} True if the command should be visible in the context
196 * menu.
197 */
198 isCommandVisible_: function(command, itemIds) {
199 switch (command) {
200 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29201 return itemIds.size == 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22202 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29203 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53204 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29205 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43206 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14207 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43208 this.getState().search.term != '' &&
209 !this.containsMatchingNode_(itemIds, function(node) {
210 return !node.parentId || node.parentId == ROOT_NODE_ID;
211 });
tsergeant2db36262017-05-15 02:47:53212 case Command.OPEN_NEW_TAB:
213 case Command.OPEN_NEW_WINDOW:
214 case Command.OPEN_INCOGNITO:
215 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14216 case Command.ADD_BOOKMARK:
217 case Command.ADD_FOLDER:
218 case Command.SORT:
219 case Command.EXPORT:
220 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28221 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14222 return true;
tsergeantf1ffc892017-05-05 07:43:43223 }
Christopher Lam34be14992018-01-17 11:01:28224 return assert(false);
tsergeant2db36262017-05-15 02:47:53225 },
tsergeantf1ffc892017-05-05 07:43:43226
tsergeant2db36262017-05-15 02:47:53227 /**
228 * @param {Command} command
229 * @param {!Set<string>} itemIds
230 * @return {boolean} True if the command should be clickable in the context
231 * menu.
232 */
233 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14234 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53235 switch (command) {
tsergeant2437f992017-06-13 23:54:29236 case Command.EDIT:
237 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29238 return !this.containsMatchingNode_(itemIds, function(node) {
239 return !bookmarks.util.canEditNode(state, node.id);
240 });
tsergeant2db36262017-05-15 02:47:53241 case Command.OPEN_NEW_TAB:
242 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53243 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02244 case Command.OPEN_INCOGNITO:
245 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14246 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02247 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14248 case Command.SORT:
249 return this.canChangeList_() &&
250 state.nodes[state.selectedFolder].children.length > 1;
251 case Command.ADD_BOOKMARK:
252 case Command.ADD_FOLDER:
253 return this.canChangeList_();
254 case Command.IMPORT:
255 return this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53256 default:
257 return true;
258 }
259 },
tsergeant13a466462017-05-15 01:21:03260
tsergeant2db36262017-05-15 02:47:53261 /**
Christopher Lam043c7cb2018-01-09 04:14:14262 * Returns whether the currently displayed bookmarks list can be changed.
263 * @private
264 * @return {boolean}
265 */
266 canChangeList_: function() {
267 const state = this.getState();
268 return state.search.term == '' &&
269 bookmarks.util.canReorderChildren(state, state.selectedFolder);
270 },
271
272 /**
tsergeant2db36262017-05-15 02:47:53273 * @param {Command} command
274 * @param {!Set<string>} itemIds
275 */
276 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10277 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53278 switch (command) {
dpapad111a34902017-09-12 16:51:10279 case Command.EDIT: {
280 let id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53281 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35282 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53283 break;
dpapad111a34902017-09-12 16:51:10284 }
tsergeantb7253e92017-07-04 02:59:22285 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10286 case Command.COPY: {
287 let idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34288 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10289 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22290 if (command == Command.COPY_URL) {
291 labelPromise =
292 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18293 } else if (idList.length == 1) {
294 labelPromise =
295 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22296 } else {
297 labelPromise = cr.sendWithPromise(
298 'getPluralString', 'toastItemsCopied', idList.length);
299 }
300
301 this.showTitleToast_(
302 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34303 });
tsergeant2db36262017-05-15 02:47:53304 break;
dpapad111a34902017-09-12 16:51:10305 }
306 case Command.SHOW_IN_FOLDER: {
307 let id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43308 this.dispatch(bookmarks.actions.selectFolder(
309 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57310 bookmarks.DialogFocusManager.getInstance().clearFocus();
311 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43312 break;
dpapad111a34902017-09-12 16:51:10313 }
314 case Command.DELETE: {
315 let idList = Array.from(this.minimizeDeletionSet_(itemIds));
316 const title = state.nodes[idList[0]].title;
317 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18318
319 if (idList.length == 1) {
320 labelPromise =
321 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
322 } else {
323 labelPromise = cr.sendWithPromise(
324 'getPluralString', 'toastItemsDeleted', idList.length);
325 }
326
dpapad325bf2f12017-07-26 18:47:34327 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22328 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34329 });
tsergeant2db36262017-05-15 02:47:53330 break;
dpapad111a34902017-09-12 16:51:10331 }
calamity2d4b5502017-05-29 03:57:58332 case Command.UNDO:
333 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58334 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58335 break;
336 case Command.REDO:
337 chrome.bookmarkManagerPrivate.redo();
338 break;
tsergeant2db36262017-05-15 02:47:53339 case Command.OPEN_NEW_TAB:
340 case Command.OPEN_NEW_WINDOW:
341 case Command.OPEN_INCOGNITO:
342 this.openUrls_(this.expandUrls_(itemIds), command);
343 break;
tsergeant6c5ad90a2017-05-19 14:12:34344 case Command.OPEN:
dpapad111a34902017-09-12 16:51:10345 const isFolder = itemIds.size == 1 &&
tsergeant6c5ad90a2017-05-19 14:12:34346 this.containsMatchingNode_(itemIds, function(node) {
347 return !node.url;
348 });
349 if (isFolder) {
dpapad111a34902017-09-12 16:51:10350 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35351 this.dispatch(
352 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34353 } else {
354 this.openUrls_(this.expandUrls_(itemIds), command);
355 }
356 break;
tsergeant679159f2017-06-16 06:58:41357 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10358 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41359 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
360 break;
361 case Command.DESELECT_ALL:
362 this.dispatch(bookmarks.actions.deselectItems());
363 break;
tsergeantb7253e92017-07-04 02:59:22364 case Command.CUT:
365 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
366 break;
367 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10368 const selectedFolder = state.selectedFolder;
369 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57370 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22371 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57372 selectedFolder, Array.from(selectedItems),
373 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22374 break;
Christopher Lam043c7cb2018-01-09 04:14:14375 case Command.SORT:
376 chrome.bookmarkManagerPrivate.sortChildren(
377 assert(state.selectedFolder));
378 bookmarks.ToastManager.getInstance().show(
379 loadTimeData.getString('toastFolderSorted'), true);
380 break;
381 case Command.ADD_BOOKMARK:
382 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
383 .showAddDialog(false, assert(state.selectedFolder));
384 break;
385 case Command.ADD_FOLDER:
386 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
387 .showAddDialog(true, assert(state.selectedFolder));
388 break;
389 case Command.IMPORT:
390 chrome.bookmarks.import();
391 break;
392 case Command.EXPORT:
393 chrome.bookmarks.export();
394 break;
Christopher Lam34be14992018-01-17 11:01:28395 case Command.HELP_CENTER:
396 window.open('https://support.google.com/chrome/?p=bookmarks');
397 break;
tsergeant6c5ad90a2017-05-19 14:12:34398 default:
399 assert(false);
tsergeant2db36262017-05-15 02:47:53400 }
Tim Sergeanta2233c812017-07-26 03:02:47401
402 bookmarks.util.recordEnumHistogram(
403 'BookmarkManager.CommandExecuted', command, Command.MAX_VALUE);
tsergeant2db36262017-05-15 02:47:53404 },
tsergeant13a466462017-05-15 01:21:03405
tsergeant6c3a6df2017-06-06 23:53:02406 /**
tsergeant0292e51a2017-06-16 03:44:35407 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02408 * @param {!Set<string>} itemIds
409 * @return {boolean} True if the event was handled, triggering a keyboard
410 * shortcut.
411 */
412 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10413 for (const commandTuple of this.shortcuts_) {
414 const command = /** @type {Command} */ (commandTuple[0]);
415 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47416 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
417 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
418 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02419
Tim Sergeanta2233c812017-07-26 03:02:47420 bookmarks.util.recordEnumHistogram(
421 'BookmarkManager.CommandExecutedFromKeyboard', command,
422 Command.MAX_VALUE);
tsergeant6c3a6df2017-06-06 23:53:02423 e.stopPropagation();
424 e.preventDefault();
425 return true;
426 }
427 }
428
429 return false;
430 },
431
tsergeant2db36262017-05-15 02:47:53432 ////////////////////////////////////////////////////////////////////////////
433 // Private functions:
434
435 /**
tsergeant0292e51a2017-06-16 03:44:35436 * Register a keyboard shortcut for a command.
437 * @param {Command} command Command that the shortcut will trigger.
438 * @param {string} shortcut Keyboard shortcut, using the syntax of
439 * cr/ui/command.js.
440 * @param {string=} macShortcut If set, enables a replacement shortcut for
441 * Mac.
442 */
443 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10444 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47445 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35446 },
447
448 /**
tsergeant2db36262017-05-15 02:47:53449 * Minimize the set of |itemIds| by removing any node which has an ancestor
450 * node already in the set. This ensures that instead of trying to delete
451 * both a node and its descendant, we will only try to delete the topmost
452 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
453 * call.
454 * @param {!Set<string>} itemIds
455 * @return {!Set<string>}
456 */
457 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10458 const minimizedSet = new Set();
459 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53460 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10461 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53462 while (currentId != ROOT_NODE_ID) {
463 currentId = assert(nodes[currentId].parentId);
464 if (itemIds.has(currentId))
465 return;
466 }
467 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03468 });
tsergeant2db36262017-05-15 02:47:53469 return minimizedSet;
470 },
tsergeant13a466462017-05-15 01:21:03471
tsergeant2db36262017-05-15 02:47:53472 /**
tsergeant7fb9e13f2017-06-26 06:35:19473 * Open the given |urls| in response to a |command|. May show a confirmation
474 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53475 * @param {!Array<string>} urls
476 * @param {Command} command
477 * @private
478 */
479 openUrls_: function(urls, command) {
480 assert(
tsergeant6c5ad90a2017-05-19 14:12:34481 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53482 command == Command.OPEN_NEW_WINDOW ||
483 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03484
tsergeant2db36262017-05-15 02:47:53485 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09486 return;
tsergeantfad224f2017-05-05 04:42:09487
dpapad111a34902017-09-12 16:51:10488 const openUrlsCallback = function() {
489 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19490 if (command == Command.OPEN_NEW_WINDOW || incognito) {
491 chrome.windows.create({url: urls, incognito: incognito});
492 } else {
493 if (command == Command.OPEN)
494 chrome.tabs.create({url: urls.shift(), active: true});
495 urls.forEach(function(url) {
496 chrome.tabs.create({url: url, active: false});
497 });
498 }
499 };
500
501 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
502 openUrlsCallback();
503 return;
tsergeant2db36262017-05-15 02:47:53504 }
tsergeant7fb9e13f2017-06-26 06:35:19505
506 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10507 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16508 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19509 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58510
511 bookmarks.DialogFocusManager.getInstance().showDialog(
512 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53513 },
tsergeant77365182017-05-05 04:02:33514
tsergeant2db36262017-05-15 02:47:53515 /**
516 * Returns all URLs in the given set of nodes and their immediate children.
517 * Note that these will be ordered by insertion order into the |itemIds|
518 * set, and that it is possible to duplicate a URL by passing in both the
519 * parent ID and child ID.
520 * @param {!Set<string>} itemIds
521 * @return {!Array<string>}
522 * @private
523 */
524 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10525 const urls = [];
526 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03527
tsergeant2db36262017-05-15 02:47:53528 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10529 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53530 if (node.url) {
531 urls.push(node.url);
532 } else {
533 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10534 const childNode = nodes[childId];
tsergeant2db36262017-05-15 02:47:53535 if (childNode.url)
536 urls.push(childNode.url);
537 });
538 }
539 });
tsergeant13a466462017-05-15 01:21:03540
tsergeant2db36262017-05-15 02:47:53541 return urls;
542 },
543
544 /**
545 * @param {!Set<string>} itemIds
546 * @param {function(BookmarkNode):boolean} predicate
547 * @return {boolean} True if any node in |itemIds| returns true for
548 * |predicate|.
549 */
550 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10551 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53552
553 return Array.from(itemIds).some(function(id) {
554 return predicate(nodes[id]);
555 });
556 },
557
558 /**
tsergeant2437f992017-06-13 23:54:29559 * @param {!Set<string>} itemIds
560 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
561 * node.
562 */
563 isSingleBookmark_: function(itemIds) {
564 return itemIds.size == 1 &&
565 this.containsMatchingNode_(itemIds, function(node) {
566 return !!node.url;
567 });
568 },
569
570 /**
tsergeant2db36262017-05-15 02:47:53571 * @param {Command} command
572 * @return {string}
573 * @private
574 */
575 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10576 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53577 this.containsMatchingNode_(this.menuIds_, function(node) {
578 return !node.url;
579 });
dpapad111a34902017-09-12 16:51:10580 let label;
tsergeant2db36262017-05-15 02:47:53581 switch (command) {
582 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02583 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53584 return '';
585
dpapad111a34902017-09-12 16:51:10586 const id = Array.from(this.menuIds_)[0];
587 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53588 label = itemUrl ? 'menuEdit' : 'menuRename';
589 break;
tsergeantb7253e92017-07-04 02:59:22590 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53591 label = 'menuCopyURL';
592 break;
593 case Command.DELETE:
594 label = 'menuDelete';
595 break;
tsergeantd16c95a2017-07-14 04:49:43596 case Command.SHOW_IN_FOLDER:
597 label = 'menuShowInFolder';
598 break;
tsergeant2db36262017-05-15 02:47:53599 case Command.OPEN_NEW_TAB:
600 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
601 break;
602 case Command.OPEN_NEW_WINDOW:
603 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
604 break;
605 case Command.OPEN_INCOGNITO:
606 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
607 break;
Christopher Lam043c7cb2018-01-09 04:14:14608 case Command.SORT:
609 label = 'menuSort';
610 break;
611 case Command.ADD_BOOKMARK:
612 label = 'menuAddBookmark';
613 break;
614 case Command.ADD_FOLDER:
615 label = 'menuAddFolder';
616 break;
617 case Command.IMPORT:
618 label = 'menuImport';
619 break;
620 case Command.EXPORT:
621 label = 'menuExport';
622 break;
Christopher Lam34be14992018-01-17 11:01:28623 case Command.HELP_CENTER:
624 label = 'menuHelpCenter';
625 break;
tsergeant2db36262017-05-15 02:47:53626 }
Christopher Lam043c7cb2018-01-09 04:14:14627 assert(label);
tsergeant2db36262017-05-15 02:47:53628
629 return loadTimeData.getString(assert(label));
630 },
631
632 /**
633 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14634 * @return {string}
635 * @private
636 */
637 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10638 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14639 this.containsMatchingNode_(this.menuIds_, function(node) {
640 return !node.url;
641 });
642 switch (command) {
643 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10644 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14645 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
646 default:
647 return '';
648 }
649 },
650
651 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14652 computeMenuCommands_: function() {
653 switch (this.menuSource_) {
654 case MenuSource.ITEM:
655 case MenuSource.TREE:
656 return [
657 Command.EDIT,
658 Command.COPY_URL,
659 Command.SHOW_IN_FOLDER,
660 Command.DELETE,
661 // <hr>
662 Command.OPEN_NEW_TAB,
663 Command.OPEN_NEW_WINDOW,
664 Command.OPEN_INCOGNITO,
665 ];
666 case MenuSource.TOOLBAR:
667 return [
668 Command.SORT,
669 // <hr>
670 Command.ADD_BOOKMARK,
671 Command.ADD_FOLDER,
672 // <hr>
673 Command.IMPORT,
674 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28675 // <hr>
676 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14677 ];
Christopher Lamfde61782018-01-11 04:27:07678 case MenuSource.LIST:
679 return [
680 Command.ADD_BOOKMARK,
681 Command.ADD_FOLDER,
682 ];
Christopher Lam043c7cb2018-01-09 04:14:14683 case MenuSource.NONE:
684 return [];
685 }
686 assert(false);
687 },
688
Christopher Lam430ef002018-01-18 10:08:50689 /**
690 * @return {boolean}
691 * @private
692 */
Christopher Lam043c7cb2018-01-09 04:14:14693 computeHasAnySublabel_: function() {
calamity64e2012a2017-06-21 09:57:14694 if (!this.menuIds_)
Christopher Lam430ef002018-01-18 10:08:50695 return false;
calamity64e2012a2017-06-21 09:57:14696
Christopher Lam430ef002018-01-18 10:08:50697 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34698 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14699 },
700
701 /**
702 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53703 * @return {boolean}
704 * @private
705 */
tsergeant2437f992017-06-13 23:54:29706 showDividerAfter_: function(command, itemIds) {
Christopher Lam34be14992018-01-17 11:01:28707 return ((command == Command.SORT || command == Command.ADD_FOLDER ||
708 command == Command.EXPORT) &&
Christopher Lam043c7cb2018-01-09 04:14:14709 this.menuSource_ == MenuSource.TOOLBAR) ||
710 (command == Command.DELETE &&
711 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)));
tsergeant2db36262017-05-15 02:47:53712 },
tsergeantb7253e92017-07-04 02:59:22713
714 /**
715 * Show a toast with a bookmark |title| inserted into a label, with the
716 * title ellipsised if necessary.
717 * @param {!Promise<string>} labelPromise Promise which resolves with the
718 * label for the toast.
719 * @param {string} title Bookmark title to insert.
720 * @param {boolean} canUndo If true, shows an undo button in the toast.
721 * @private
722 */
723 showTitleToast_: function(labelPromise, title, canUndo) {
724 labelPromise.then(function(label) {
dpapad111a34902017-09-12 16:51:10725 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
726 .map(function(p) {
727 // Make the bookmark name collapsible.
728 p.collapsible = !!p.arg;
729 return p;
730 });
tsergeantb7253e92017-07-04 02:59:22731
732 bookmarks.ToastManager.getInstance().showForStringPieces(
733 pieces, canUndo);
734 });
735 },
736
737 ////////////////////////////////////////////////////////////////////////////
738 // Event handlers:
739
740 /**
741 * @param {Event} e
742 * @private
743 */
Christopher Lam043c7cb2018-01-09 04:14:14744 onOpenCommandMenu_: function(e) {
tsergeantb7253e92017-07-04 02:59:22745 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43746 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22747 } else {
tsergeantd16c95a2017-07-14 04:49:43748 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22749 }
Christopher Lamed175322018-01-18 14:54:49750 bookmarks.util.recordEnumHistogram(
751 'BookmarkManager.CommandMenuOpened', e.detail.source,
752 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22753 },
754
755 /**
756 * @param {Event} e
757 * @private
758 */
759 onCommandClick_: function(e) {
760 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47761 /** @type {Command} */ (
762 Number(e.currentTarget.getAttribute('command'))),
763 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22764 this.closeCommandMenu();
765 },
766
767 /**
768 * @param {!Event} e
769 * @private
770 */
771 onKeydown_: function(e) {
dpapad111a34902017-09-12 16:51:10772 const selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17773 if (e.target == document.body &&
774 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22775 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17776 }
tsergeantb7253e92017-07-04 02:59:22777 },
778
779 /**
780 * Close the menu on mousedown so clicks can propagate to the underlying UI.
781 * This allows the user to right click the list while a context menu is
782 * showing and get another context menu.
783 * @param {Event} e
784 * @private
785 */
786 onMenuMousedown_: function(e) {
787 if (e.path[0] != this.$.dropdown.getIfExists())
788 return;
789
790 this.closeCommandMenu();
791 },
792
793 /** @private */
794 onOpenCancelTap_: function() {
795 this.$.openDialog.get().cancel();
796 },
797
798 /** @private */
799 onOpenConfirmTap_: function() {
800 this.confirmOpenCallback_();
801 this.$.openDialog.get().close();
802 },
tsergeant2db36262017-05-15 02:47:53803 });
804
805 /** @private {bookmarks.CommandManager} */
806 CommandManager.instance_ = null;
807
808 /** @return {!bookmarks.CommandManager} */
809 CommandManager.getInstance = function() {
810 return assert(CommandManager.instance_);
811 };
812
813 return {
814 CommandManager: CommandManager,
815 };
tsergeant77365182017-05-05 04:02:33816});