blob: 020f92b183db528e3abccaad7f823473e91ebffb [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
92 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o');
93 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'));
276 } else {
277 labelPromise = cr.sendWithPromise(
278 'getPluralString', 'toastItemsCopied', idList.length);
279 }
280
281 this.showTitleToast_(
282 labelPromise, state.nodes[idList[0]].title, false);
283 }.bind(this));
tsergeant2db36262017-05-15 02:47:53284 break;
tsergeantd16c95a2017-07-14 04:49:43285 case Command.SHOW_IN_FOLDER:
286 var id = Array.from(itemIds)[0];
287 this.dispatch(bookmarks.actions.selectFolder(
288 assert(state.nodes[id].parentId), state.nodes));
289 break;
tsergeant2db36262017-05-15 02:47:53290 case Command.DELETE:
calamityefe477352017-06-07 06:44:58291 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
tsergeant0292e51a2017-06-16 03:44:35292 var title = state.nodes[idList[0]].title;
calamitye0917642017-06-09 07:34:35293 var labelPromise = cr.sendWithPromise(
294 'getPluralString', 'toastItemsDeleted', idList.length);
calamityefe477352017-06-07 06:44:58295 chrome.bookmarkManagerPrivate.removeTrees(idList, function() {
tsergeantb7253e92017-07-04 02:59:22296 this.showTitleToast_(labelPromise, title, true);
calamityefe477352017-06-07 06:44:58297 }.bind(this));
tsergeant2db36262017-05-15 02:47:53298 break;
calamity2d4b5502017-05-29 03:57:58299 case Command.UNDO:
300 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58301 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58302 break;
303 case Command.REDO:
304 chrome.bookmarkManagerPrivate.redo();
305 break;
tsergeant2db36262017-05-15 02:47:53306 case Command.OPEN_NEW_TAB:
307 case Command.OPEN_NEW_WINDOW:
308 case Command.OPEN_INCOGNITO:
309 this.openUrls_(this.expandUrls_(itemIds), command);
310 break;
tsergeant6c5ad90a2017-05-19 14:12:34311 case Command.OPEN:
312 var isFolder = itemIds.size == 1 &&
313 this.containsMatchingNode_(itemIds, function(node) {
314 return !node.url;
315 });
316 if (isFolder) {
317 var folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35318 this.dispatch(
319 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34320 } else {
321 this.openUrls_(this.expandUrls_(itemIds), command);
322 }
323 break;
tsergeant679159f2017-06-16 06:58:41324 case Command.SELECT_ALL:
325 var displayedIds = bookmarks.util.getDisplayedList(state);
326 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
327 break;
328 case Command.DESELECT_ALL:
329 this.dispatch(bookmarks.actions.deselectItems());
330 break;
tsergeantb7253e92017-07-04 02:59:22331 case Command.CUT:
332 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
333 break;
334 case Command.PASTE:
335 var selectedFolder = state.selectedFolder;
336 var selectedItems = state.selection.items;
337 chrome.bookmarkManagerPrivate.paste(
338 selectedFolder, Array.from(selectedItems));
339 break;
tsergeant6c5ad90a2017-05-19 14:12:34340 default:
341 assert(false);
tsergeant2db36262017-05-15 02:47:53342 }
343 },
tsergeant13a466462017-05-15 01:21:03344
tsergeant6c3a6df2017-06-06 23:53:02345 /**
tsergeant0292e51a2017-06-16 03:44:35346 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02347 * @param {!Set<string>} itemIds
348 * @return {boolean} True if the event was handled, triggering a keyboard
349 * shortcut.
350 */
351 handleKeyEvent: function(e, itemIds) {
352 for (var commandName in this.shortcuts_) {
353 var shortcut = this.shortcuts_[commandName];
tsergeant0292e51a2017-06-16 03:44:35354 if (shortcut.matchesEvent(e) && this.canExecute(commandName, itemIds)) {
tsergeant6c3a6df2017-06-06 23:53:02355 this.handle(commandName, itemIds);
356
357 e.stopPropagation();
358 e.preventDefault();
359 return true;
360 }
361 }
362
363 return false;
364 },
365
tsergeant2db36262017-05-15 02:47:53366 ////////////////////////////////////////////////////////////////////////////
367 // Private functions:
368
369 /**
tsergeant0292e51a2017-06-16 03:44:35370 * Register a keyboard shortcut for a command.
371 * @param {Command} command Command that the shortcut will trigger.
372 * @param {string} shortcut Keyboard shortcut, using the syntax of
373 * cr/ui/command.js.
374 * @param {string=} macShortcut If set, enables a replacement shortcut for
375 * Mac.
376 */
377 addShortcut_: function(command, shortcut, macShortcut) {
378 var shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
379 this.shortcuts_[command] = new cr.ui.KeyboardShortcutList(shortcut);
380 },
381
382 /**
tsergeant2db36262017-05-15 02:47:53383 * Minimize the set of |itemIds| by removing any node which has an ancestor
384 * node already in the set. This ensures that instead of trying to delete
385 * both a node and its descendant, we will only try to delete the topmost
386 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
387 * call.
388 * @param {!Set<string>} itemIds
389 * @return {!Set<string>}
390 */
391 minimizeDeletionSet_: function(itemIds) {
392 var minimizedSet = new Set();
393 var nodes = this.getState().nodes;
394 itemIds.forEach(function(itemId) {
395 var currentId = itemId;
396 while (currentId != ROOT_NODE_ID) {
397 currentId = assert(nodes[currentId].parentId);
398 if (itemIds.has(currentId))
399 return;
400 }
401 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03402 });
tsergeant2db36262017-05-15 02:47:53403 return minimizedSet;
404 },
tsergeant13a466462017-05-15 01:21:03405
tsergeant2db36262017-05-15 02:47:53406 /**
tsergeant7fb9e13f2017-06-26 06:35:19407 * Open the given |urls| in response to a |command|. May show a confirmation
408 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53409 * @param {!Array<string>} urls
410 * @param {Command} command
411 * @private
412 */
413 openUrls_: function(urls, command) {
414 assert(
tsergeant6c5ad90a2017-05-19 14:12:34415 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53416 command == Command.OPEN_NEW_WINDOW ||
417 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03418
tsergeant2db36262017-05-15 02:47:53419 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09420 return;
tsergeantfad224f2017-05-05 04:42:09421
tsergeant7fb9e13f2017-06-26 06:35:19422 var openUrlsCallback = function() {
423 var incognito = command == Command.OPEN_INCOGNITO;
424 if (command == Command.OPEN_NEW_WINDOW || incognito) {
425 chrome.windows.create({url: urls, incognito: incognito});
426 } else {
427 if (command == Command.OPEN)
428 chrome.tabs.create({url: urls.shift(), active: true});
429 urls.forEach(function(url) {
430 chrome.tabs.create({url: url, active: false});
431 });
432 }
433 };
434
435 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
436 openUrlsCallback();
437 return;
tsergeant2db36262017-05-15 02:47:53438 }
tsergeant7fb9e13f2017-06-26 06:35:19439
440 this.confirmOpenCallback_ = openUrlsCallback;
441 var dialog = this.$.openDialog.get();
442 dialog.querySelector('.body').textContent =
443 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58444
445 bookmarks.DialogFocusManager.getInstance().showDialog(
446 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53447 },
tsergeant77365182017-05-05 04:02:33448
tsergeant2db36262017-05-15 02:47:53449 /**
450 * Returns all URLs in the given set of nodes and their immediate children.
451 * Note that these will be ordered by insertion order into the |itemIds|
452 * set, and that it is possible to duplicate a URL by passing in both the
453 * parent ID and child ID.
454 * @param {!Set<string>} itemIds
455 * @return {!Array<string>}
456 * @private
457 */
458 expandUrls_: function(itemIds) {
459 var urls = [];
460 var nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03461
tsergeant2db36262017-05-15 02:47:53462 itemIds.forEach(function(id) {
463 var node = nodes[id];
464 if (node.url) {
465 urls.push(node.url);
466 } else {
467 node.children.forEach(function(childId) {
468 var childNode = nodes[childId];
469 if (childNode.url)
470 urls.push(childNode.url);
471 });
472 }
473 });
tsergeant13a466462017-05-15 01:21:03474
tsergeant2db36262017-05-15 02:47:53475 return urls;
476 },
477
478 /**
479 * @param {!Set<string>} itemIds
480 * @param {function(BookmarkNode):boolean} predicate
481 * @return {boolean} True if any node in |itemIds| returns true for
482 * |predicate|.
483 */
484 containsMatchingNode_: function(itemIds, predicate) {
485 var nodes = this.getState().nodes;
486
487 return Array.from(itemIds).some(function(id) {
488 return predicate(nodes[id]);
489 });
490 },
491
492 /**
tsergeant2437f992017-06-13 23:54:29493 * @param {!Set<string>} itemIds
494 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
495 * node.
496 */
497 isSingleBookmark_: function(itemIds) {
498 return itemIds.size == 1 &&
499 this.containsMatchingNode_(itemIds, function(node) {
500 return !!node.url;
501 });
502 },
503
504 /**
tsergeant2db36262017-05-15 02:47:53505 * @param {Command} command
506 * @return {string}
507 * @private
508 */
509 getCommandLabel_: function(command) {
510 var multipleNodes = this.menuIds_.size > 1 ||
511 this.containsMatchingNode_(this.menuIds_, function(node) {
512 return !node.url;
513 });
514 var label;
515 switch (command) {
516 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02517 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53518 return '';
519
520 var id = Array.from(this.menuIds_)[0];
521 var itemUrl = this.getState().nodes[id].url;
522 label = itemUrl ? 'menuEdit' : 'menuRename';
523 break;
tsergeantb7253e92017-07-04 02:59:22524 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53525 label = 'menuCopyURL';
526 break;
527 case Command.DELETE:
528 label = 'menuDelete';
529 break;
tsergeantd16c95a2017-07-14 04:49:43530 case Command.SHOW_IN_FOLDER:
531 label = 'menuShowInFolder';
532 break;
tsergeant2db36262017-05-15 02:47:53533 case Command.OPEN_NEW_TAB:
534 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
535 break;
536 case Command.OPEN_NEW_WINDOW:
537 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
538 break;
539 case Command.OPEN_INCOGNITO:
540 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
541 break;
542 }
543
544 return loadTimeData.getString(assert(label));
545 },
546
547 /**
548 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14549 * @return {string}
550 * @private
551 */
552 getCommandSublabel_: function(command) {
553 var multipleNodes = this.menuIds_.size > 1 ||
554 this.containsMatchingNode_(this.menuIds_, function(node) {
555 return !node.url;
556 });
557 switch (command) {
558 case Command.OPEN_NEW_TAB:
559 var urls = this.expandUrls_(this.menuIds_);
560 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
561 default:
562 return '';
563 }
564 },
565
566 /** @private */
567 onMenuIdsChanged_: function() {
568 if (!this.menuIds_)
569 return;
570
571 this.hasAnySublabel_ = this.menuCommands_.some(function(command) {
572 return this.getCommandSublabel_(command) != '';
573 }.bind(this));
574 },
575
576 /**
577 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53578 * @return {boolean}
579 * @private
580 */
tsergeant2437f992017-06-13 23:54:29581 showDividerAfter_: function(command, itemIds) {
582 return command == Command.DELETE &&
583 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
tsergeant2db36262017-05-15 02:47:53584 },
tsergeantb7253e92017-07-04 02:59:22585
586 /**
587 * Show a toast with a bookmark |title| inserted into a label, with the
588 * title ellipsised if necessary.
589 * @param {!Promise<string>} labelPromise Promise which resolves with the
590 * label for the toast.
591 * @param {string} title Bookmark title to insert.
592 * @param {boolean} canUndo If true, shows an undo button in the toast.
593 * @private
594 */
595 showTitleToast_: function(labelPromise, title, canUndo) {
596 labelPromise.then(function(label) {
597 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
598 .map(function(p) {
599 // Make the bookmark name collapsible.
600 p.collapsible = !!p.arg;
601 return p;
602 });
603
604 bookmarks.ToastManager.getInstance().showForStringPieces(
605 pieces, canUndo);
606 });
607 },
608
609 ////////////////////////////////////////////////////////////////////////////
610 // Event handlers:
611
612 /**
613 * @param {Event} e
614 * @private
615 */
616 onOpenItemMenu_: function(e) {
617 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43618 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22619 } else {
tsergeantd16c95a2017-07-14 04:49:43620 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22621 }
622 },
623
624 /**
625 * @param {Event} e
626 * @private
627 */
628 onCommandClick_: function(e) {
629 this.handle(
630 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
631 this.closeCommandMenu();
632 },
633
634 /**
635 * @param {!Event} e
636 * @private
637 */
638 onKeydown_: function(e) {
639 var selection = this.getState().selection.items;
640 if (e.target == document.body)
641 this.handleKeyEvent(e, selection);
642 },
643
644 /**
645 * Close the menu on mousedown so clicks can propagate to the underlying UI.
646 * This allows the user to right click the list while a context menu is
647 * showing and get another context menu.
648 * @param {Event} e
649 * @private
650 */
651 onMenuMousedown_: function(e) {
652 if (e.path[0] != this.$.dropdown.getIfExists())
653 return;
654
655 this.closeCommandMenu();
656 },
657
658 /** @private */
659 onOpenCancelTap_: function() {
660 this.$.openDialog.get().cancel();
661 },
662
663 /** @private */
664 onOpenConfirmTap_: function() {
665 this.confirmOpenCallback_();
666 this.$.openDialog.get().close();
667 },
tsergeant2db36262017-05-15 02:47:53668 });
669
670 /** @private {bookmarks.CommandManager} */
671 CommandManager.instance_ = null;
672
673 /** @return {!bookmarks.CommandManager} */
674 CommandManager.getInstance = function() {
675 return assert(CommandManager.instance_);
676 };
677
678 return {
679 CommandManager: CommandManager,
680 };
tsergeant77365182017-05-05 04:02:33681});