blob: 0636bd9f0cf983f53a324aa89a1d8843df90d394 [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
tsergeant2db36262017-05-15 02:47:5311 var CommandManager = Polymer({
12 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,
22 value: function() {
23 return [
24 Command.EDIT,
tsergeantb7253e92017-07-04 02:59:2225 Command.COPY_URL,
tsergeantd16c95a2017-07-14 04:49:4326 Command.SHOW_IN_FOLDER,
tsergeant2db36262017-05-15 02:47:5327 Command.DELETE,
28 // <hr>
29 Command.OPEN_NEW_TAB,
30 Command.OPEN_NEW_WINDOW,
31 Command.OPEN_INCOGNITO,
32 ];
33 },
tsergeant13a466462017-05-15 01:21:0334 },
tsergeant2db36262017-05-15 02:47:5335
tsergeant2437f992017-06-13 23:54:2936 /** @private {Set<string>} */
calamity64e2012a2017-06-21 09:57:1437 menuIds_: {
38 type: Object,
39 observer: 'onMenuIdsChanged_',
40 },
41
42 /** @private */
43 hasAnySublabel_: {
44 type: Boolean,
45 reflectToAttribute: true,
46 },
tsergeant2437f992017-06-13 23:54:2947
48 /** @private */
49 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
tsergeant2437f992017-06-13 23:54:2959 this.watch('globalCanEdit_', function(state) {
60 return state.prefs.canEdit;
61 });
62 this.updateFromStore();
63
tsergeant2db36262017-05-15 02:47:5364 /** @private {function(!Event)} */
65 this.boundOnOpenItemMenu_ = this.onOpenItemMenu_.bind(this);
66 document.addEventListener('open-item-menu', this.boundOnOpenItemMenu_);
tsergeantfad224f2017-05-05 04:42:0967
calamityefe477352017-06-07 06:44:5868 /** @private {function()} */
dpapad325bf2f12017-07-26 18:47:3469 this.boundOnCommandUndo_ = () => {
calamityefe477352017-06-07 06:44:5870 this.handle(Command.UNDO, new Set());
dpapad325bf2f12017-07-26 18:47:3471 };
calamityefe477352017-06-07 06:44:5872 document.addEventListener('command-undo', this.boundOnCommandUndo_);
73
tsergeant2db36262017-05-15 02:47:5374 /** @private {function(!Event)} */
75 this.boundOnKeydown_ = this.onKeydown_.bind(this);
76 document.addEventListener('keydown', this.boundOnKeydown_);
tsergeantfad224f2017-05-05 04:42:0977
tsergeantd16c95a2017-07-14 04:49:4378 /**
79 * Indicates where the context menu was opened from. Will be NONE if
80 * menu is not open, indicating that commands are from keyboard shortcuts
81 * or elsewhere in the UI.
82 * @private {MenuSource}
83 */
84 this.menuSource_ = MenuSource.NONE;
85
Tim Sergeanta2233c812017-07-26 03:02:4786 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
87 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3588
89 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3590 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
91
Tim Sergeant40d2d2c2017-07-20 01:37:0692 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3593 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
94 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
95
96 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
97 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4198
99 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
100 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:22101
102 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
103 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
104 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
tsergeant2db36262017-05-15 02:47:53105 },
tsergeant77365182017-05-05 04:02:33106
tsergeant2db36262017-05-15 02:47:53107 detached: function() {
108 CommandManager.instance_ = null;
109 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_);
calamityefe477352017-06-07 06:44:58110 document.removeEventListener('command-undo', this.boundOnCommandUndo_);
tsergeant2db36262017-05-15 02:47:53111 document.removeEventListener('keydown', this.boundOnKeydown_);
112 },
tsergeant77365182017-05-05 04:02:33113
tsergeant2db36262017-05-15 02:47:53114 /**
115 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28116 * Commands will execute on |items| if given, or on the currently selected
117 * items.
tsergeant2db36262017-05-15 02:47:53118 * @param {number} x
119 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43120 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28121 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53122 */
tsergeantd16c95a2017-07-14 04:49:43123 openCommandMenuAtPosition: function(x, y, source, items) {
124 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28125 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58126
tsergeant9b9aa15f2017-06-22 03:22:27127 var dropdown =
128 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
129 // Ensure that the menu is fully rendered before trying to position it.
130 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58131 bookmarks.DialogFocusManager.getInstance().showDialog(
132 dropdown, function() {
133 dropdown.showAtPosition({top: y, left: x});
134 });
tsergeant2db36262017-05-15 02:47:53135 },
tsergeant77365182017-05-05 04:02:33136
tsergeant2db36262017-05-15 02:47:53137 /**
138 * Display the command context menu positioned to cover the |target|
139 * element. Commands will execute on the currently selected items.
140 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43141 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53142 */
tsergeantd16c95a2017-07-14 04:49:43143 openCommandMenuAtElement: function(target, source) {
144 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53145 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58146
tsergeant9b9aa15f2017-06-22 03:22:27147 var dropdown =
148 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
149 // Ensure that the menu is fully rendered before trying to position it.
150 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58151 bookmarks.DialogFocusManager.getInstance().showDialog(
152 dropdown, function() {
153 dropdown.showAt(target);
154 });
tsergeant2db36262017-05-15 02:47:53155 },
tsergeant77365182017-05-05 04:02:33156
tsergeant2db36262017-05-15 02:47:53157 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02158 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43159 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27160 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53161 },
tsergeant77365182017-05-05 04:02:33162
tsergeant2db36262017-05-15 02:47:53163 ////////////////////////////////////////////////////////////////////////////
164 // Command handlers:
tsergeant77365182017-05-05 04:02:33165
tsergeant2db36262017-05-15 02:47:53166 /**
167 * Determine if the |command| can be executed with the given |itemIds|.
168 * Commands which appear in the context menu should be implemented
169 * separately using `isCommandVisible_` and `isCommandEnabled_`.
170 * @param {Command} command
171 * @param {!Set<string>} itemIds
172 * @return {boolean}
173 */
174 canExecute: function(command, itemIds) {
tsergeantb7253e92017-07-04 02:59:22175 var state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34176 switch (command) {
177 case Command.OPEN:
178 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58179 case Command.UNDO:
180 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29181 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41182 case Command.SELECT_ALL:
183 case Command.DESELECT_ALL:
184 return true;
tsergeantb7253e92017-07-04 02:59:22185 case Command.COPY:
186 return itemIds.size > 0;
187 case Command.CUT:
188 return itemIds.size > 0 &&
189 !this.containsMatchingNode_(itemIds, function(node) {
190 return !bookmarks.util.canEditNode(state, node.id);
191 });
192 case Command.PASTE:
193 return state.search.term == '' &&
194 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34195 default:
196 return this.isCommandVisible_(command, itemIds) &&
197 this.isCommandEnabled_(command, itemIds);
198 }
tsergeant2db36262017-05-15 02:47:53199 },
tsergeant13a466462017-05-15 01:21:03200
tsergeant2db36262017-05-15 02:47:53201 /**
202 * @param {Command} command
203 * @param {!Set<string>} itemIds
204 * @return {boolean} True if the command should be visible in the context
205 * menu.
206 */
207 isCommandVisible_: function(command, itemIds) {
208 switch (command) {
209 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29210 return itemIds.size == 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22211 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29212 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53213 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29214 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43215 case Command.SHOW_IN_FOLDER:
216 return this.menuSource_ == MenuSource.LIST && itemIds.size == 1 &&
217 this.getState().search.term != '' &&
218 !this.containsMatchingNode_(itemIds, function(node) {
219 return !node.parentId || node.parentId == ROOT_NODE_ID;
220 });
tsergeant2db36262017-05-15 02:47:53221 case Command.OPEN_NEW_TAB:
222 case Command.OPEN_NEW_WINDOW:
223 case Command.OPEN_INCOGNITO:
224 return itemIds.size > 0;
225 default:
226 return false;
tsergeantf1ffc892017-05-05 07:43:43227 }
tsergeant2db36262017-05-15 02:47:53228 },
tsergeantf1ffc892017-05-05 07:43:43229
tsergeant2db36262017-05-15 02:47:53230 /**
231 * @param {Command} command
232 * @param {!Set<string>} itemIds
233 * @return {boolean} True if the command should be clickable in the context
234 * menu.
235 */
236 isCommandEnabled_: function(command, itemIds) {
237 switch (command) {
tsergeant2437f992017-06-13 23:54:29238 case Command.EDIT:
239 case Command.DELETE:
240 var state = this.getState();
241 return !this.containsMatchingNode_(itemIds, function(node) {
242 return !bookmarks.util.canEditNode(state, node.id);
243 });
tsergeant2db36262017-05-15 02:47:53244 case Command.OPEN_NEW_TAB:
245 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53246 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02247 case Command.OPEN_INCOGNITO:
248 return this.expandUrls_(itemIds).length > 0 &&
249 this.getState().prefs.incognitoAvailability !=
250 IncognitoAvailability.DISABLED;
tsergeant2db36262017-05-15 02:47:53251 default:
252 return true;
253 }
254 },
tsergeant13a466462017-05-15 01:21:03255
tsergeant2db36262017-05-15 02:47:53256 /**
257 * @param {Command} command
258 * @param {!Set<string>} itemIds
259 */
260 handle: function(command, itemIds) {
tsergeant0292e51a2017-06-16 03:44:35261 var state = this.getState();
tsergeant2db36262017-05-15 02:47:53262 switch (command) {
263 case Command.EDIT:
264 var id = Array.from(itemIds)[0];
265 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35266 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53267 break;
tsergeantb7253e92017-07-04 02:59:22268 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53269 case Command.COPY:
270 var idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34271 chrome.bookmarkManagerPrivate.copy(idList, () => {
tsergeantb7253e92017-07-04 02:59:22272 var labelPromise;
273 if (command == Command.COPY_URL) {
274 labelPromise =
275 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18276 } else if (idList.length == 1) {
277 labelPromise =
278 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22279 } else {
280 labelPromise = cr.sendWithPromise(
281 'getPluralString', 'toastItemsCopied', idList.length);
282 }
283
284 this.showTitleToast_(
285 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34286 });
tsergeant2db36262017-05-15 02:47:53287 break;
tsergeantd16c95a2017-07-14 04:49:43288 case Command.SHOW_IN_FOLDER:
289 var id = Array.from(itemIds)[0];
290 this.dispatch(bookmarks.actions.selectFolder(
291 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57292 bookmarks.DialogFocusManager.getInstance().clearFocus();
293 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43294 break;
tsergeant2db36262017-05-15 02:47:53295 case Command.DELETE:
calamityefe477352017-06-07 06:44:58296 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
tsergeant0292e51a2017-06-16 03:44:35297 var title = state.nodes[idList[0]].title;
Christopher Lam75ca9102017-07-18 02:15:18298 var labelPromise;
299
300 if (idList.length == 1) {
301 labelPromise =
302 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
303 } else {
304 labelPromise = cr.sendWithPromise(
305 'getPluralString', 'toastItemsDeleted', idList.length);
306 }
307
dpapad325bf2f12017-07-26 18:47:34308 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22309 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34310 });
tsergeant2db36262017-05-15 02:47:53311 break;
calamity2d4b5502017-05-29 03:57:58312 case Command.UNDO:
313 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58314 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58315 break;
316 case Command.REDO:
317 chrome.bookmarkManagerPrivate.redo();
318 break;
tsergeant2db36262017-05-15 02:47:53319 case Command.OPEN_NEW_TAB:
320 case Command.OPEN_NEW_WINDOW:
321 case Command.OPEN_INCOGNITO:
322 this.openUrls_(this.expandUrls_(itemIds), command);
323 break;
tsergeant6c5ad90a2017-05-19 14:12:34324 case Command.OPEN:
325 var isFolder = itemIds.size == 1 &&
326 this.containsMatchingNode_(itemIds, function(node) {
327 return !node.url;
328 });
329 if (isFolder) {
330 var folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35331 this.dispatch(
332 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34333 } else {
334 this.openUrls_(this.expandUrls_(itemIds), command);
335 }
336 break;
tsergeant679159f2017-06-16 06:58:41337 case Command.SELECT_ALL:
338 var displayedIds = bookmarks.util.getDisplayedList(state);
339 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
340 break;
341 case Command.DESELECT_ALL:
342 this.dispatch(bookmarks.actions.deselectItems());
343 break;
tsergeantb7253e92017-07-04 02:59:22344 case Command.CUT:
345 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
346 break;
347 case Command.PASTE:
348 var selectedFolder = state.selectedFolder;
349 var selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57350 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22351 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57352 selectedFolder, Array.from(selectedItems),
353 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22354 break;
tsergeant6c5ad90a2017-05-19 14:12:34355 default:
356 assert(false);
tsergeant2db36262017-05-15 02:47:53357 }
Tim Sergeanta2233c812017-07-26 03:02:47358
359 bookmarks.util.recordEnumHistogram(
360 'BookmarkManager.CommandExecuted', command, Command.MAX_VALUE);
tsergeant2db36262017-05-15 02:47:53361 },
tsergeant13a466462017-05-15 01:21:03362
tsergeant6c3a6df2017-06-06 23:53:02363 /**
tsergeant0292e51a2017-06-16 03:44:35364 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02365 * @param {!Set<string>} itemIds
366 * @return {boolean} True if the event was handled, triggering a keyboard
367 * shortcut.
368 */
369 handleKeyEvent: function(e, itemIds) {
Tim Sergeanta2233c812017-07-26 03:02:47370 for (var commandTuple of this.shortcuts_) {
371 var command = /** @type {Command} */ (commandTuple[0]);
372 var shortcut =
373 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
374 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
375 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02376
Tim Sergeanta2233c812017-07-26 03:02:47377 bookmarks.util.recordEnumHistogram(
378 'BookmarkManager.CommandExecutedFromKeyboard', command,
379 Command.MAX_VALUE);
tsergeant6c3a6df2017-06-06 23:53:02380 e.stopPropagation();
381 e.preventDefault();
382 return true;
383 }
384 }
385
386 return false;
387 },
388
tsergeant2db36262017-05-15 02:47:53389 ////////////////////////////////////////////////////////////////////////////
390 // Private functions:
391
392 /**
tsergeant0292e51a2017-06-16 03:44:35393 * Register a keyboard shortcut for a command.
394 * @param {Command} command Command that the shortcut will trigger.
395 * @param {string} shortcut Keyboard shortcut, using the syntax of
396 * cr/ui/command.js.
397 * @param {string=} macShortcut If set, enables a replacement shortcut for
398 * Mac.
399 */
400 addShortcut_: function(command, shortcut, macShortcut) {
401 var shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47402 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35403 },
404
405 /**
tsergeant2db36262017-05-15 02:47:53406 * Minimize the set of |itemIds| by removing any node which has an ancestor
407 * node already in the set. This ensures that instead of trying to delete
408 * both a node and its descendant, we will only try to delete the topmost
409 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
410 * call.
411 * @param {!Set<string>} itemIds
412 * @return {!Set<string>}
413 */
414 minimizeDeletionSet_: function(itemIds) {
415 var minimizedSet = new Set();
416 var nodes = this.getState().nodes;
417 itemIds.forEach(function(itemId) {
418 var currentId = itemId;
419 while (currentId != ROOT_NODE_ID) {
420 currentId = assert(nodes[currentId].parentId);
421 if (itemIds.has(currentId))
422 return;
423 }
424 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03425 });
tsergeant2db36262017-05-15 02:47:53426 return minimizedSet;
427 },
tsergeant13a466462017-05-15 01:21:03428
tsergeant2db36262017-05-15 02:47:53429 /**
tsergeant7fb9e13f2017-06-26 06:35:19430 * Open the given |urls| in response to a |command|. May show a confirmation
431 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53432 * @param {!Array<string>} urls
433 * @param {Command} command
434 * @private
435 */
436 openUrls_: function(urls, command) {
437 assert(
tsergeant6c5ad90a2017-05-19 14:12:34438 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53439 command == Command.OPEN_NEW_WINDOW ||
440 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03441
tsergeant2db36262017-05-15 02:47:53442 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09443 return;
tsergeantfad224f2017-05-05 04:42:09444
tsergeant7fb9e13f2017-06-26 06:35:19445 var openUrlsCallback = function() {
446 var incognito = command == Command.OPEN_INCOGNITO;
447 if (command == Command.OPEN_NEW_WINDOW || incognito) {
448 chrome.windows.create({url: urls, incognito: incognito});
449 } else {
450 if (command == Command.OPEN)
451 chrome.tabs.create({url: urls.shift(), active: true});
452 urls.forEach(function(url) {
453 chrome.tabs.create({url: url, active: false});
454 });
455 }
456 };
457
458 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
459 openUrlsCallback();
460 return;
tsergeant2db36262017-05-15 02:47:53461 }
tsergeant7fb9e13f2017-06-26 06:35:19462
463 this.confirmOpenCallback_ = openUrlsCallback;
464 var dialog = this.$.openDialog.get();
465 dialog.querySelector('.body').textContent =
466 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58467
468 bookmarks.DialogFocusManager.getInstance().showDialog(
469 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53470 },
tsergeant77365182017-05-05 04:02:33471
tsergeant2db36262017-05-15 02:47:53472 /**
473 * Returns all URLs in the given set of nodes and their immediate children.
474 * Note that these will be ordered by insertion order into the |itemIds|
475 * set, and that it is possible to duplicate a URL by passing in both the
476 * parent ID and child ID.
477 * @param {!Set<string>} itemIds
478 * @return {!Array<string>}
479 * @private
480 */
481 expandUrls_: function(itemIds) {
482 var urls = [];
483 var nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03484
tsergeant2db36262017-05-15 02:47:53485 itemIds.forEach(function(id) {
486 var node = nodes[id];
487 if (node.url) {
488 urls.push(node.url);
489 } else {
490 node.children.forEach(function(childId) {
491 var childNode = nodes[childId];
492 if (childNode.url)
493 urls.push(childNode.url);
494 });
495 }
496 });
tsergeant13a466462017-05-15 01:21:03497
tsergeant2db36262017-05-15 02:47:53498 return urls;
499 },
500
501 /**
502 * @param {!Set<string>} itemIds
503 * @param {function(BookmarkNode):boolean} predicate
504 * @return {boolean} True if any node in |itemIds| returns true for
505 * |predicate|.
506 */
507 containsMatchingNode_: function(itemIds, predicate) {
508 var nodes = this.getState().nodes;
509
510 return Array.from(itemIds).some(function(id) {
511 return predicate(nodes[id]);
512 });
513 },
514
515 /**
tsergeant2437f992017-06-13 23:54:29516 * @param {!Set<string>} itemIds
517 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
518 * node.
519 */
520 isSingleBookmark_: function(itemIds) {
521 return itemIds.size == 1 &&
522 this.containsMatchingNode_(itemIds, function(node) {
523 return !!node.url;
524 });
525 },
526
527 /**
tsergeant2db36262017-05-15 02:47:53528 * @param {Command} command
529 * @return {string}
530 * @private
531 */
532 getCommandLabel_: function(command) {
533 var multipleNodes = this.menuIds_.size > 1 ||
534 this.containsMatchingNode_(this.menuIds_, function(node) {
535 return !node.url;
536 });
537 var label;
538 switch (command) {
539 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02540 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53541 return '';
542
543 var id = Array.from(this.menuIds_)[0];
544 var itemUrl = this.getState().nodes[id].url;
545 label = itemUrl ? 'menuEdit' : 'menuRename';
546 break;
tsergeantb7253e92017-07-04 02:59:22547 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53548 label = 'menuCopyURL';
549 break;
550 case Command.DELETE:
551 label = 'menuDelete';
552 break;
tsergeantd16c95a2017-07-14 04:49:43553 case Command.SHOW_IN_FOLDER:
554 label = 'menuShowInFolder';
555 break;
tsergeant2db36262017-05-15 02:47:53556 case Command.OPEN_NEW_TAB:
557 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
558 break;
559 case Command.OPEN_NEW_WINDOW:
560 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
561 break;
562 case Command.OPEN_INCOGNITO:
563 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
564 break;
565 }
566
567 return loadTimeData.getString(assert(label));
568 },
569
570 /**
571 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14572 * @return {string}
573 * @private
574 */
575 getCommandSublabel_: function(command) {
576 var multipleNodes = this.menuIds_.size > 1 ||
577 this.containsMatchingNode_(this.menuIds_, function(node) {
578 return !node.url;
579 });
580 switch (command) {
581 case Command.OPEN_NEW_TAB:
582 var urls = this.expandUrls_(this.menuIds_);
583 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
584 default:
585 return '';
586 }
587 },
588
589 /** @private */
590 onMenuIdsChanged_: function() {
591 if (!this.menuIds_)
592 return;
593
dpapad325bf2f12017-07-26 18:47:34594 this.hasAnySublabel_ = this.menuCommands_.some(
595 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14596 },
597
598 /**
599 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53600 * @return {boolean}
601 * @private
602 */
tsergeant2437f992017-06-13 23:54:29603 showDividerAfter_: function(command, itemIds) {
604 return command == Command.DELETE &&
605 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
tsergeant2db36262017-05-15 02:47:53606 },
tsergeantb7253e92017-07-04 02:59:22607
608 /**
609 * Show a toast with a bookmark |title| inserted into a label, with the
610 * title ellipsised if necessary.
611 * @param {!Promise<string>} labelPromise Promise which resolves with the
612 * label for the toast.
613 * @param {string} title Bookmark title to insert.
614 * @param {boolean} canUndo If true, shows an undo button in the toast.
615 * @private
616 */
617 showTitleToast_: function(labelPromise, title, canUndo) {
618 labelPromise.then(function(label) {
619 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
620 .map(function(p) {
621 // Make the bookmark name collapsible.
622 p.collapsible = !!p.arg;
623 return p;
624 });
625
626 bookmarks.ToastManager.getInstance().showForStringPieces(
627 pieces, canUndo);
628 });
629 },
630
631 ////////////////////////////////////////////////////////////////////////////
632 // Event handlers:
633
634 /**
635 * @param {Event} e
636 * @private
637 */
638 onOpenItemMenu_: function(e) {
639 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43640 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22641 } else {
tsergeantd16c95a2017-07-14 04:49:43642 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22643 }
644 },
645
646 /**
647 * @param {Event} e
648 * @private
649 */
650 onCommandClick_: function(e) {
651 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47652 /** @type {Command} */ (
653 Number(e.currentTarget.getAttribute('command'))),
654 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22655 this.closeCommandMenu();
656 },
657
658 /**
659 * @param {!Event} e
660 * @private
661 */
662 onKeydown_: function(e) {
663 var selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17664 if (e.target == document.body &&
665 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22666 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17667 }
tsergeantb7253e92017-07-04 02:59:22668 },
669
670 /**
671 * Close the menu on mousedown so clicks can propagate to the underlying UI.
672 * This allows the user to right click the list while a context menu is
673 * showing and get another context menu.
674 * @param {Event} e
675 * @private
676 */
677 onMenuMousedown_: function(e) {
678 if (e.path[0] != this.$.dropdown.getIfExists())
679 return;
680
681 this.closeCommandMenu();
682 },
683
684 /** @private */
685 onOpenCancelTap_: function() {
686 this.$.openDialog.get().cancel();
687 },
688
689 /** @private */
690 onOpenConfirmTap_: function() {
691 this.confirmOpenCallback_();
692 this.$.openDialog.get().close();
693 },
tsergeant2db36262017-05-15 02:47:53694 });
695
696 /** @private {bookmarks.CommandManager} */
697 CommandManager.instance_ = null;
698
699 /** @return {!bookmarks.CommandManager} */
700 CommandManager.getInstance = function() {
701 return assert(CommandManager.instance_);
702 };
703
704 return {
705 CommandManager: CommandManager,
706 };
tsergeant77365182017-05-05 04:02:33707});