blob: dee208efa921fbb70f1fb2cbb24cfdea4def5dc8 [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()} */
69 this.boundOnCommandUndo_ = function() {
70 this.handle(Command.UNDO, new Set());
71 }.bind(this);
72 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
tsergeant0292e51a2017-06-16 03:44:3586 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */
tsergeant2db36262017-05-15 02:47:5387 this.shortcuts_ = {};
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);
271 chrome.bookmarkManagerPrivate.copy(idList, function() {
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);
286 }.bind(this));
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));
292 break;
tsergeant2db36262017-05-15 02:47:53293 case Command.DELETE:
calamityefe477352017-06-07 06:44:58294 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
tsergeant0292e51a2017-06-16 03:44:35295 var title = state.nodes[idList[0]].title;
Christopher Lam75ca9102017-07-18 02:15:18296 var labelPromise;
297
298 if (idList.length == 1) {
299 labelPromise =
300 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
301 } else {
302 labelPromise = cr.sendWithPromise(
303 'getPluralString', 'toastItemsDeleted', idList.length);
304 }
305
calamityefe477352017-06-07 06:44:58306 chrome.bookmarkManagerPrivate.removeTrees(idList, function() {
tsergeantb7253e92017-07-04 02:59:22307 this.showTitleToast_(labelPromise, title, true);
calamityefe477352017-06-07 06:44:58308 }.bind(this));
tsergeant2db36262017-05-15 02:47:53309 break;
calamity2d4b5502017-05-29 03:57:58310 case Command.UNDO:
311 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58312 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58313 break;
314 case Command.REDO:
315 chrome.bookmarkManagerPrivate.redo();
316 break;
tsergeant2db36262017-05-15 02:47:53317 case Command.OPEN_NEW_TAB:
318 case Command.OPEN_NEW_WINDOW:
319 case Command.OPEN_INCOGNITO:
320 this.openUrls_(this.expandUrls_(itemIds), command);
321 break;
tsergeant6c5ad90a2017-05-19 14:12:34322 case Command.OPEN:
323 var isFolder = itemIds.size == 1 &&
324 this.containsMatchingNode_(itemIds, function(node) {
325 return !node.url;
326 });
327 if (isFolder) {
328 var folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35329 this.dispatch(
330 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34331 } else {
332 this.openUrls_(this.expandUrls_(itemIds), command);
333 }
334 break;
tsergeant679159f2017-06-16 06:58:41335 case Command.SELECT_ALL:
336 var displayedIds = bookmarks.util.getDisplayedList(state);
337 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
338 break;
339 case Command.DESELECT_ALL:
340 this.dispatch(bookmarks.actions.deselectItems());
341 break;
tsergeantb7253e92017-07-04 02:59:22342 case Command.CUT:
343 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
344 break;
345 case Command.PASTE:
346 var selectedFolder = state.selectedFolder;
347 var selectedItems = state.selection.items;
348 chrome.bookmarkManagerPrivate.paste(
349 selectedFolder, Array.from(selectedItems));
350 break;
tsergeant6c5ad90a2017-05-19 14:12:34351 default:
352 assert(false);
tsergeant2db36262017-05-15 02:47:53353 }
354 },
tsergeant13a466462017-05-15 01:21:03355
tsergeant6c3a6df2017-06-06 23:53:02356 /**
tsergeant0292e51a2017-06-16 03:44:35357 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02358 * @param {!Set<string>} itemIds
359 * @return {boolean} True if the event was handled, triggering a keyboard
360 * shortcut.
361 */
362 handleKeyEvent: function(e, itemIds) {
363 for (var commandName in this.shortcuts_) {
364 var shortcut = this.shortcuts_[commandName];
tsergeant0292e51a2017-06-16 03:44:35365 if (shortcut.matchesEvent(e) && this.canExecute(commandName, itemIds)) {
tsergeant6c3a6df2017-06-06 23:53:02366 this.handle(commandName, itemIds);
367
368 e.stopPropagation();
369 e.preventDefault();
370 return true;
371 }
372 }
373
374 return false;
375 },
376
tsergeant2db36262017-05-15 02:47:53377 ////////////////////////////////////////////////////////////////////////////
378 // Private functions:
379
380 /**
tsergeant0292e51a2017-06-16 03:44:35381 * Register a keyboard shortcut for a command.
382 * @param {Command} command Command that the shortcut will trigger.
383 * @param {string} shortcut Keyboard shortcut, using the syntax of
384 * cr/ui/command.js.
385 * @param {string=} macShortcut If set, enables a replacement shortcut for
386 * Mac.
387 */
388 addShortcut_: function(command, shortcut, macShortcut) {
389 var shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
390 this.shortcuts_[command] = new cr.ui.KeyboardShortcutList(shortcut);
391 },
392
393 /**
tsergeant2db36262017-05-15 02:47:53394 * Minimize the set of |itemIds| by removing any node which has an ancestor
395 * node already in the set. This ensures that instead of trying to delete
396 * both a node and its descendant, we will only try to delete the topmost
397 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
398 * call.
399 * @param {!Set<string>} itemIds
400 * @return {!Set<string>}
401 */
402 minimizeDeletionSet_: function(itemIds) {
403 var minimizedSet = new Set();
404 var nodes = this.getState().nodes;
405 itemIds.forEach(function(itemId) {
406 var currentId = itemId;
407 while (currentId != ROOT_NODE_ID) {
408 currentId = assert(nodes[currentId].parentId);
409 if (itemIds.has(currentId))
410 return;
411 }
412 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03413 });
tsergeant2db36262017-05-15 02:47:53414 return minimizedSet;
415 },
tsergeant13a466462017-05-15 01:21:03416
tsergeant2db36262017-05-15 02:47:53417 /**
tsergeant7fb9e13f2017-06-26 06:35:19418 * Open the given |urls| in response to a |command|. May show a confirmation
419 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53420 * @param {!Array<string>} urls
421 * @param {Command} command
422 * @private
423 */
424 openUrls_: function(urls, command) {
425 assert(
tsergeant6c5ad90a2017-05-19 14:12:34426 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53427 command == Command.OPEN_NEW_WINDOW ||
428 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03429
tsergeant2db36262017-05-15 02:47:53430 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09431 return;
tsergeantfad224f2017-05-05 04:42:09432
tsergeant7fb9e13f2017-06-26 06:35:19433 var openUrlsCallback = function() {
434 var incognito = command == Command.OPEN_INCOGNITO;
435 if (command == Command.OPEN_NEW_WINDOW || incognito) {
436 chrome.windows.create({url: urls, incognito: incognito});
437 } else {
438 if (command == Command.OPEN)
439 chrome.tabs.create({url: urls.shift(), active: true});
440 urls.forEach(function(url) {
441 chrome.tabs.create({url: url, active: false});
442 });
443 }
444 };
445
446 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
447 openUrlsCallback();
448 return;
tsergeant2db36262017-05-15 02:47:53449 }
tsergeant7fb9e13f2017-06-26 06:35:19450
451 this.confirmOpenCallback_ = openUrlsCallback;
452 var dialog = this.$.openDialog.get();
453 dialog.querySelector('.body').textContent =
454 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58455
456 bookmarks.DialogFocusManager.getInstance().showDialog(
457 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53458 },
tsergeant77365182017-05-05 04:02:33459
tsergeant2db36262017-05-15 02:47:53460 /**
461 * Returns all URLs in the given set of nodes and their immediate children.
462 * Note that these will be ordered by insertion order into the |itemIds|
463 * set, and that it is possible to duplicate a URL by passing in both the
464 * parent ID and child ID.
465 * @param {!Set<string>} itemIds
466 * @return {!Array<string>}
467 * @private
468 */
469 expandUrls_: function(itemIds) {
470 var urls = [];
471 var nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03472
tsergeant2db36262017-05-15 02:47:53473 itemIds.forEach(function(id) {
474 var node = nodes[id];
475 if (node.url) {
476 urls.push(node.url);
477 } else {
478 node.children.forEach(function(childId) {
479 var childNode = nodes[childId];
480 if (childNode.url)
481 urls.push(childNode.url);
482 });
483 }
484 });
tsergeant13a466462017-05-15 01:21:03485
tsergeant2db36262017-05-15 02:47:53486 return urls;
487 },
488
489 /**
490 * @param {!Set<string>} itemIds
491 * @param {function(BookmarkNode):boolean} predicate
492 * @return {boolean} True if any node in |itemIds| returns true for
493 * |predicate|.
494 */
495 containsMatchingNode_: function(itemIds, predicate) {
496 var nodes = this.getState().nodes;
497
498 return Array.from(itemIds).some(function(id) {
499 return predicate(nodes[id]);
500 });
501 },
502
503 /**
tsergeant2437f992017-06-13 23:54:29504 * @param {!Set<string>} itemIds
505 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
506 * node.
507 */
508 isSingleBookmark_: function(itemIds) {
509 return itemIds.size == 1 &&
510 this.containsMatchingNode_(itemIds, function(node) {
511 return !!node.url;
512 });
513 },
514
515 /**
tsergeant2db36262017-05-15 02:47:53516 * @param {Command} command
517 * @return {string}
518 * @private
519 */
520 getCommandLabel_: function(command) {
521 var multipleNodes = this.menuIds_.size > 1 ||
522 this.containsMatchingNode_(this.menuIds_, function(node) {
523 return !node.url;
524 });
525 var label;
526 switch (command) {
527 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02528 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53529 return '';
530
531 var id = Array.from(this.menuIds_)[0];
532 var itemUrl = this.getState().nodes[id].url;
533 label = itemUrl ? 'menuEdit' : 'menuRename';
534 break;
tsergeantb7253e92017-07-04 02:59:22535 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53536 label = 'menuCopyURL';
537 break;
538 case Command.DELETE:
539 label = 'menuDelete';
540 break;
tsergeantd16c95a2017-07-14 04:49:43541 case Command.SHOW_IN_FOLDER:
542 label = 'menuShowInFolder';
543 break;
tsergeant2db36262017-05-15 02:47:53544 case Command.OPEN_NEW_TAB:
545 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
546 break;
547 case Command.OPEN_NEW_WINDOW:
548 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
549 break;
550 case Command.OPEN_INCOGNITO:
551 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
552 break;
553 }
554
555 return loadTimeData.getString(assert(label));
556 },
557
558 /**
559 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14560 * @return {string}
561 * @private
562 */
563 getCommandSublabel_: function(command) {
564 var multipleNodes = this.menuIds_.size > 1 ||
565 this.containsMatchingNode_(this.menuIds_, function(node) {
566 return !node.url;
567 });
568 switch (command) {
569 case Command.OPEN_NEW_TAB:
570 var urls = this.expandUrls_(this.menuIds_);
571 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
572 default:
573 return '';
574 }
575 },
576
577 /** @private */
578 onMenuIdsChanged_: function() {
579 if (!this.menuIds_)
580 return;
581
582 this.hasAnySublabel_ = this.menuCommands_.some(function(command) {
583 return this.getCommandSublabel_(command) != '';
584 }.bind(this));
585 },
586
587 /**
588 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53589 * @return {boolean}
590 * @private
591 */
tsergeant2437f992017-06-13 23:54:29592 showDividerAfter_: function(command, itemIds) {
593 return command == Command.DELETE &&
594 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
tsergeant2db36262017-05-15 02:47:53595 },
tsergeantb7253e92017-07-04 02:59:22596
597 /**
598 * Show a toast with a bookmark |title| inserted into a label, with the
599 * title ellipsised if necessary.
600 * @param {!Promise<string>} labelPromise Promise which resolves with the
601 * label for the toast.
602 * @param {string} title Bookmark title to insert.
603 * @param {boolean} canUndo If true, shows an undo button in the toast.
604 * @private
605 */
606 showTitleToast_: function(labelPromise, title, canUndo) {
607 labelPromise.then(function(label) {
608 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
609 .map(function(p) {
610 // Make the bookmark name collapsible.
611 p.collapsible = !!p.arg;
612 return p;
613 });
614
615 bookmarks.ToastManager.getInstance().showForStringPieces(
616 pieces, canUndo);
617 });
618 },
619
620 ////////////////////////////////////////////////////////////////////////////
621 // Event handlers:
622
623 /**
624 * @param {Event} e
625 * @private
626 */
627 onOpenItemMenu_: function(e) {
628 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43629 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22630 } else {
tsergeantd16c95a2017-07-14 04:49:43631 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22632 }
633 },
634
635 /**
636 * @param {Event} e
637 * @private
638 */
639 onCommandClick_: function(e) {
640 this.handle(
641 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
642 this.closeCommandMenu();
643 },
644
645 /**
646 * @param {!Event} e
647 * @private
648 */
649 onKeydown_: function(e) {
650 var selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17651 if (e.target == document.body &&
652 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22653 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17654 }
tsergeantb7253e92017-07-04 02:59:22655 },
656
657 /**
658 * Close the menu on mousedown so clicks can propagate to the underlying UI.
659 * This allows the user to right click the list while a context menu is
660 * showing and get another context menu.
661 * @param {Event} e
662 * @private
663 */
664 onMenuMousedown_: function(e) {
665 if (e.path[0] != this.$.dropdown.getIfExists())
666 return;
667
668 this.closeCommandMenu();
669 },
670
671 /** @private */
672 onOpenCancelTap_: function() {
673 this.$.openDialog.get().cancel();
674 },
675
676 /** @private */
677 onOpenConfirmTap_: function() {
678 this.confirmOpenCallback_();
679 this.$.openDialog.get().close();
680 },
tsergeant2db36262017-05-15 02:47:53681 });
682
683 /** @private {bookmarks.CommandManager} */
684 CommandManager.instance_ = null;
685
686 /** @return {!bookmarks.CommandManager} */
687 CommandManager.getInstance = function() {
688 return assert(CommandManager.instance_);
689 };
690
691 return {
692 CommandManager: CommandManager,
693 };
tsergeant77365182017-05-05 04:02:33694});