Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(618)

Side by Side Diff: chrome/browser/resources/md_bookmarks/command_manager.js

Issue 2948943002: MD Bookmarks: Add full support for cut/copy/paste keyboard shortcuts (Closed)
Patch Set: Rebase Created 3 years, 5 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
OLDNEW
1 // Copyright 2017 The Chromium Authors. All rights reserved. 1 // Copyright 2017 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be 2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file. 3 // found in the LICENSE file.
4 4
5 /** 5 /**
6 * @fileoverview Element which shows context menus and handles keyboard 6 * @fileoverview Element which shows context menus and handles keyboard
7 * shortcuts. 7 * shortcuts.
8 */ 8 */
9 cr.define('bookmarks', function() { 9 cr.define('bookmarks', function() {
10 10
11 var CommandManager = Polymer({ 11 var CommandManager = Polymer({
12 is: 'bookmarks-command-manager', 12 is: 'bookmarks-command-manager',
13 13
14 behaviors: [ 14 behaviors: [
15 bookmarks.StoreClient, 15 bookmarks.StoreClient,
16 ], 16 ],
17 17
18 properties: { 18 properties: {
19 /** @private {!Array<Command>} */ 19 /** @private {!Array<Command>} */
20 menuCommands_: { 20 menuCommands_: {
21 type: Array, 21 type: Array,
22 value: function() { 22 value: function() {
23 return [ 23 return [
24 Command.EDIT, 24 Command.EDIT,
25 Command.COPY, 25 Command.COPY_URL,
26 Command.DELETE, 26 Command.DELETE,
27 // <hr> 27 // <hr>
28 Command.OPEN_NEW_TAB, 28 Command.OPEN_NEW_TAB,
29 Command.OPEN_NEW_WINDOW, 29 Command.OPEN_NEW_WINDOW,
30 Command.OPEN_INCOGNITO, 30 Command.OPEN_INCOGNITO,
31 ]; 31 ];
32 }, 32 },
33 }, 33 },
34 34
35 /** @private {Set<string>} */ 35 /** @private {Set<string>} */
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
71 document.addEventListener('command-undo', this.boundOnCommandUndo_); 71 document.addEventListener('command-undo', this.boundOnCommandUndo_);
72 72
73 /** @private {function(!Event)} */ 73 /** @private {function(!Event)} */
74 this.boundOnKeydown_ = this.onKeydown_.bind(this); 74 this.boundOnKeydown_ = this.onKeydown_.bind(this);
75 document.addEventListener('keydown', this.boundOnKeydown_); 75 document.addEventListener('keydown', this.boundOnKeydown_);
76 76
77 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */ 77 /** @private {Object<Command, cr.ui.KeyboardShortcutList>} */
78 this.shortcuts_ = {}; 78 this.shortcuts_ = {};
79 79
80 this.addShortcut_(Command.EDIT, 'F2', 'Enter'); 80 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
81 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
82 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace'); 81 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
83 82
84 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o'); 83 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|ArrowDown Meta|o');
85 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter'); 84 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
86 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter'); 85 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
87 86
88 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z'); 87 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
89 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z'); 88 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
90 89
91 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a'); 90 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
92 this.addShortcut_(Command.DESELECT_ALL, 'Escape'); 91 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
92
93 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
94 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
95 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
93 }, 96 },
94 97
95 detached: function() { 98 detached: function() {
96 CommandManager.instance_ = null; 99 CommandManager.instance_ = null;
97 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_); 100 document.removeEventListener('open-item-menu', this.boundOnOpenItemMenu_);
98 document.removeEventListener('command-undo', this.boundOnCommandUndo_); 101 document.removeEventListener('command-undo', this.boundOnCommandUndo_);
99 document.removeEventListener('keydown', this.boundOnKeydown_); 102 document.removeEventListener('keydown', this.boundOnKeydown_);
100 }, 103 },
101 104
102 /** 105 /**
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
140 143
141 /** 144 /**
142 * Determine if the |command| can be executed with the given |itemIds|. 145 * Determine if the |command| can be executed with the given |itemIds|.
143 * Commands which appear in the context menu should be implemented 146 * Commands which appear in the context menu should be implemented
144 * separately using `isCommandVisible_` and `isCommandEnabled_`. 147 * separately using `isCommandVisible_` and `isCommandEnabled_`.
145 * @param {Command} command 148 * @param {Command} command
146 * @param {!Set<string>} itemIds 149 * @param {!Set<string>} itemIds
147 * @return {boolean} 150 * @return {boolean}
148 */ 151 */
149 canExecute: function(command, itemIds) { 152 canExecute: function(command, itemIds) {
153 var state = this.getState();
150 switch (command) { 154 switch (command) {
151 case Command.OPEN: 155 case Command.OPEN:
152 return itemIds.size > 0; 156 return itemIds.size > 0;
153 case Command.UNDO: 157 case Command.UNDO:
154 case Command.REDO: 158 case Command.REDO:
155 return this.globalCanEdit_; 159 return this.globalCanEdit_;
156 case Command.SELECT_ALL: 160 case Command.SELECT_ALL:
157 case Command.DESELECT_ALL: 161 case Command.DESELECT_ALL:
158 return true; 162 return true;
163 case Command.COPY:
164 return itemIds.size > 0;
165 case Command.CUT:
166 return itemIds.size > 0 &&
167 !this.containsMatchingNode_(itemIds, function(node) {
168 return !bookmarks.util.canEditNode(state, node.id);
169 });
170 case Command.PASTE:
171 return state.search.term == '' &&
172 bookmarks.util.canReorderChildren(state, state.selectedFolder);
159 default: 173 default:
160 return this.isCommandVisible_(command, itemIds) && 174 return this.isCommandVisible_(command, itemIds) &&
161 this.isCommandEnabled_(command, itemIds); 175 this.isCommandEnabled_(command, itemIds);
162 } 176 }
163 }, 177 },
164 178
165 /** 179 /**
166 * @param {Command} command 180 * @param {Command} command
167 * @param {!Set<string>} itemIds 181 * @param {!Set<string>} itemIds
168 * @return {boolean} True if the command should be visible in the context 182 * @return {boolean} True if the command should be visible in the context
169 * menu. 183 * menu.
170 */ 184 */
171 isCommandVisible_: function(command, itemIds) { 185 isCommandVisible_: function(command, itemIds) {
172 switch (command) { 186 switch (command) {
173 case Command.EDIT: 187 case Command.EDIT:
174 return itemIds.size == 1 && this.globalCanEdit_; 188 return itemIds.size == 1 && this.globalCanEdit_;
175 case Command.COPY: 189 case Command.COPY_URL:
176 return this.isSingleBookmark_(itemIds); 190 return this.isSingleBookmark_(itemIds);
177 case Command.DELETE: 191 case Command.DELETE:
178 return itemIds.size > 0 && this.globalCanEdit_; 192 return itemIds.size > 0 && this.globalCanEdit_;
179 case Command.OPEN_NEW_TAB: 193 case Command.OPEN_NEW_TAB:
180 case Command.OPEN_NEW_WINDOW: 194 case Command.OPEN_NEW_WINDOW:
181 case Command.OPEN_INCOGNITO: 195 case Command.OPEN_INCOGNITO:
182 return itemIds.size > 0; 196 return itemIds.size > 0;
183 default: 197 default:
184 return false; 198 return false;
185 } 199 }
(...skipping 30 matching lines...) Expand all
216 * @param {!Set<string>} itemIds 230 * @param {!Set<string>} itemIds
217 */ 231 */
218 handle: function(command, itemIds) { 232 handle: function(command, itemIds) {
219 var state = this.getState(); 233 var state = this.getState();
220 switch (command) { 234 switch (command) {
221 case Command.EDIT: 235 case Command.EDIT:
222 var id = Array.from(itemIds)[0]; 236 var id = Array.from(itemIds)[0];
223 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get()) 237 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
224 .showEditDialog(state.nodes[id]); 238 .showEditDialog(state.nodes[id]);
225 break; 239 break;
240 case Command.COPY_URL:
226 case Command.COPY: 241 case Command.COPY:
227 var idList = Array.from(itemIds); 242 var idList = Array.from(itemIds);
228 chrome.bookmarkManagerPrivate.copy(idList, function() { 243 chrome.bookmarkManagerPrivate.copy(idList, function() {
229 bookmarks.ToastManager.getInstance().show( 244 var labelPromise;
230 loadTimeData.getString('toastUrlCopied'), false); 245 if (command == Command.COPY_URL) {
231 }); 246 labelPromise =
247 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
248 } else {
249 labelPromise = cr.sendWithPromise(
250 'getPluralString', 'toastItemsCopied', idList.length);
251 }
252
253 this.showTitleToast_(
254 labelPromise, state.nodes[idList[0]].title, false);
255 }.bind(this));
232 break; 256 break;
233 case Command.DELETE: 257 case Command.DELETE:
234 var idList = Array.from(this.minimizeDeletionSet_(itemIds)); 258 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
235 var title = state.nodes[idList[0]].title; 259 var title = state.nodes[idList[0]].title;
236 var labelPromise = cr.sendWithPromise( 260 var labelPromise = cr.sendWithPromise(
237 'getPluralString', 'toastItemsDeleted', idList.length); 261 'getPluralString', 'toastItemsDeleted', idList.length);
238 chrome.bookmarkManagerPrivate.removeTrees(idList, function() { 262 chrome.bookmarkManagerPrivate.removeTrees(idList, function() {
239 labelPromise.then(function(label) { 263 this.showTitleToast_(labelPromise, title, true);
240 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
241 .map(function(p) {
242 // Make the bookmark name collapsible.
243 p.collapsible = !!p.arg;
244 return p;
245 });
246 bookmarks.ToastManager.getInstance().showForStringPieces(
247 pieces, true);
248 }.bind(this));
249 }.bind(this)); 264 }.bind(this));
250 break; 265 break;
251 case Command.UNDO: 266 case Command.UNDO:
252 chrome.bookmarkManagerPrivate.undo(); 267 chrome.bookmarkManagerPrivate.undo();
253 bookmarks.ToastManager.getInstance().hide(); 268 bookmarks.ToastManager.getInstance().hide();
254 break; 269 break;
255 case Command.REDO: 270 case Command.REDO:
256 chrome.bookmarkManagerPrivate.redo(); 271 chrome.bookmarkManagerPrivate.redo();
257 break; 272 break;
258 case Command.OPEN_NEW_TAB: 273 case Command.OPEN_NEW_TAB:
(...skipping 14 matching lines...) Expand all
273 this.openUrls_(this.expandUrls_(itemIds), command); 288 this.openUrls_(this.expandUrls_(itemIds), command);
274 } 289 }
275 break; 290 break;
276 case Command.SELECT_ALL: 291 case Command.SELECT_ALL:
277 var displayedIds = bookmarks.util.getDisplayedList(state); 292 var displayedIds = bookmarks.util.getDisplayedList(state);
278 this.dispatch(bookmarks.actions.selectAll(displayedIds, state)); 293 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
279 break; 294 break;
280 case Command.DESELECT_ALL: 295 case Command.DESELECT_ALL:
281 this.dispatch(bookmarks.actions.deselectItems()); 296 this.dispatch(bookmarks.actions.deselectItems());
282 break; 297 break;
298 case Command.CUT:
299 var idList = Array.from(itemIds);
300 chrome.bookmarkManagerPrivate.cut(idList);
calamity 2017/07/03 03:40:05 nit: inline var.
tsergeant 2017/07/03 05:28:29 Done.
301 break;
302 case Command.PASTE:
303 var selectedFolder = state.selectedFolder;
304 var selectedItems = state.selection.items;
305 chrome.bookmarkManagerPrivate.paste(
306 selectedFolder, Array.from(selectedItems));
calamity 2017/07/03 03:40:05 We should scroll to the pasted node here.
tsergeant 2017/07/03 05:28:29 This little comment reveals a slightly unkempt yak
tsergeant 2017/07/04 01:32:11 Filed https://crbug.com/738958
307 break;
283 default: 308 default:
284 assert(false); 309 assert(false);
285 } 310 }
286 }, 311 },
287 312
288 /** 313 /**
289 * @param {!Event} e 314 * @param {!Event} e
290 * @param {!Set<string>} itemIds 315 * @param {!Set<string>} itemIds
291 * @return {boolean} True if the event was handled, triggering a keyboard 316 * @return {boolean} True if the event was handled, triggering a keyboard
292 * shortcut. 317 * shortcut.
(...skipping 143 matching lines...) Expand 10 before | Expand all | Expand 10 after
436 * node. 461 * node.
437 */ 462 */
438 isSingleBookmark_: function(itemIds) { 463 isSingleBookmark_: function(itemIds) {
439 return itemIds.size == 1 && 464 return itemIds.size == 1 &&
440 this.containsMatchingNode_(itemIds, function(node) { 465 this.containsMatchingNode_(itemIds, function(node) {
441 return !!node.url; 466 return !!node.url;
442 }); 467 });
443 }, 468 },
444 469
445 /** 470 /**
446 * @param {Event} e
447 * @private
448 */
449 onOpenItemMenu_: function(e) {
450 if (e.detail.targetElement) {
451 this.openCommandMenuAtElement(e.detail.targetElement);
452 } else {
453 this.openCommandMenuAtPosition(e.detail.x, e.detail.y);
454 }
455 },
456
457 /**
458 * @param {Event} e
459 * @private
460 */
461 onCommandClick_: function(e) {
462 this.handle(
463 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
464 this.closeCommandMenu();
465 },
466
467 /**
468 * @param {!Event} e
469 * @private
470 */
471 onKeydown_: function(e) {
472 var selection = this.getState().selection.items;
473 if (e.target == document.body)
474 this.handleKeyEvent(e, selection);
475 },
476
477 /**
478 * Close the menu on mousedown so clicks can propagate to the underlying UI.
479 * This allows the user to right click the list while a context menu is
480 * showing and get another context menu.
481 * @param {Event} e
482 * @private
483 */
484 onMenuMousedown_: function(e) {
485 if (e.path[0] != this.$.dropdown.getIfExists())
486 return;
487
488 this.closeCommandMenu();
489 },
490
491 /** @private */
492 onOpenCancelTap_: function() {
493 this.$.openDialog.get().cancel();
494 },
495
496 /** @private */
497 onOpenConfirmTap_: function() {
498 this.confirmOpenCallback_();
499 this.$.openDialog.get().close();
500 },
501
502 /**
503 * @param {Command} command 471 * @param {Command} command
504 * @return {string} 472 * @return {string}
505 * @private 473 * @private
506 */ 474 */
507 getCommandLabel_: function(command) { 475 getCommandLabel_: function(command) {
508 var multipleNodes = this.menuIds_.size > 1 || 476 var multipleNodes = this.menuIds_.size > 1 ||
509 this.containsMatchingNode_(this.menuIds_, function(node) { 477 this.containsMatchingNode_(this.menuIds_, function(node) {
510 return !node.url; 478 return !node.url;
511 }); 479 });
512 var label; 480 var label;
513 switch (command) { 481 switch (command) {
514 case Command.EDIT: 482 case Command.EDIT:
515 if (this.menuIds_.size != 1) 483 if (this.menuIds_.size != 1)
516 return ''; 484 return '';
517 485
518 var id = Array.from(this.menuIds_)[0]; 486 var id = Array.from(this.menuIds_)[0];
519 var itemUrl = this.getState().nodes[id].url; 487 var itemUrl = this.getState().nodes[id].url;
520 label = itemUrl ? 'menuEdit' : 'menuRename'; 488 label = itemUrl ? 'menuEdit' : 'menuRename';
521 break; 489 break;
522 case Command.COPY: 490 case Command.COPY_URL:
523 label = 'menuCopyURL'; 491 label = 'menuCopyURL';
524 break; 492 break;
525 case Command.DELETE: 493 case Command.DELETE:
526 label = 'menuDelete'; 494 label = 'menuDelete';
527 break; 495 break;
528 case Command.OPEN_NEW_TAB: 496 case Command.OPEN_NEW_TAB:
529 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab'; 497 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
530 break; 498 break;
531 case Command.OPEN_NEW_WINDOW: 499 case Command.OPEN_NEW_WINDOW:
532 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow'; 500 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
570 538
571 /** 539 /**
572 * @param {Command} command 540 * @param {Command} command
573 * @return {boolean} 541 * @return {boolean}
574 * @private 542 * @private
575 */ 543 */
576 showDividerAfter_: function(command, itemIds) { 544 showDividerAfter_: function(command, itemIds) {
577 return command == Command.DELETE && 545 return command == Command.DELETE &&
578 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)); 546 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
579 }, 547 },
548
549 /**
550 * Show a toast with a bookmark |title| inserted into a label, with the
551 * title ellipsised if necessary.
552 * @param {!Promise<string>} labelPromise Promise which resolves with the
553 * label for the toast.
554 * @param {string} title Bookmark title to insert.
555 * @param {boolean} canUndo If true, shows an undo button in the toast.
556 * @private
557 */
558 showTitleToast_: function(labelPromise, title, canUndo) {
559 labelPromise.then(function(label) {
560 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
561 .map(function(p) {
562 // Make the bookmark name collapsible.
563 p.collapsible = !!p.arg;
564 return p;
565 });
566
567 bookmarks.ToastManager.getInstance().showForStringPieces(
568 pieces, canUndo);
569 });
570 },
571
572 ////////////////////////////////////////////////////////////////////////////
573 // Event handlers:
574
575 /**
576 * @param {Event} e
577 * @private
578 */
579 onOpenItemMenu_: function(e) {
580 if (e.detail.targetElement) {
581 this.openCommandMenuAtElement(e.detail.targetElement);
582 } else {
583 this.openCommandMenuAtPosition(e.detail.x, e.detail.y);
584 }
585 },
586
587 /**
588 * @param {Event} e
589 * @private
590 */
591 onCommandClick_: function(e) {
592 this.handle(
593 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
594 this.closeCommandMenu();
595 },
596
597 /**
598 * @param {!Event} e
599 * @private
600 */
601 onKeydown_: function(e) {
602 var selection = this.getState().selection.items;
603 if (e.target == document.body)
604 this.handleKeyEvent(e, selection);
605 },
606
607 /**
608 * Close the menu on mousedown so clicks can propagate to the underlying UI.
609 * This allows the user to right click the list while a context menu is
610 * showing and get another context menu.
611 * @param {Event} e
612 * @private
613 */
614 onMenuMousedown_: function(e) {
615 if (e.path[0] != this.$.dropdown.getIfExists())
616 return;
617
618 this.closeCommandMenu();
619 },
620
621 /** @private */
622 onOpenCancelTap_: function() {
623 this.$.openDialog.get().cancel();
624 },
625
626 /** @private */
627 onOpenConfirmTap_: function() {
628 this.confirmOpenCallback_();
629 this.$.openDialog.get().close();
630 },
580 }); 631 });
581 632
582 /** @private {bookmarks.CommandManager} */ 633 /** @private {bookmarks.CommandManager} */
583 CommandManager.instance_ = null; 634 CommandManager.instance_ = null;
584 635
585 /** @return {!bookmarks.CommandManager} */ 636 /** @return {!bookmarks.CommandManager} */
586 CommandManager.getInstance = function() { 637 CommandManager.getInstance = function() {
587 return assert(CommandManager.instance_); 638 return assert(CommandManager.instance_);
588 }; 639 };
589 640
590 return { 641 return {
591 CommandManager: CommandManager, 642 CommandManager: CommandManager,
592 }; 643 };
593 }); 644 });
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698