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

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: Review comments 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
« no previous file with comments | « chrome/app/bookmarks_strings.grdp ('k') | chrome/browser/resources/md_bookmarks/constants.js » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
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 45 matching lines...) Expand 10 before | Expand all | Expand 10 after
148 151
149 /** 152 /**
150 * Determine if the |command| can be executed with the given |itemIds|. 153 * Determine if the |command| can be executed with the given |itemIds|.
151 * Commands which appear in the context menu should be implemented 154 * Commands which appear in the context menu should be implemented
152 * separately using `isCommandVisible_` and `isCommandEnabled_`. 155 * separately using `isCommandVisible_` and `isCommandEnabled_`.
153 * @param {Command} command 156 * @param {Command} command
154 * @param {!Set<string>} itemIds 157 * @param {!Set<string>} itemIds
155 * @return {boolean} 158 * @return {boolean}
156 */ 159 */
157 canExecute: function(command, itemIds) { 160 canExecute: function(command, itemIds) {
161 var state = this.getState();
158 switch (command) { 162 switch (command) {
159 case Command.OPEN: 163 case Command.OPEN:
160 return itemIds.size > 0; 164 return itemIds.size > 0;
161 case Command.UNDO: 165 case Command.UNDO:
162 case Command.REDO: 166 case Command.REDO:
163 return this.globalCanEdit_; 167 return this.globalCanEdit_;
164 case Command.SELECT_ALL: 168 case Command.SELECT_ALL:
165 case Command.DESELECT_ALL: 169 case Command.DESELECT_ALL:
166 return true; 170 return true;
171 case Command.COPY:
172 return itemIds.size > 0;
173 case Command.CUT:
174 return itemIds.size > 0 &&
175 !this.containsMatchingNode_(itemIds, function(node) {
176 return !bookmarks.util.canEditNode(state, node.id);
177 });
178 case Command.PASTE:
179 return state.search.term == '' &&
180 bookmarks.util.canReorderChildren(state, state.selectedFolder);
167 default: 181 default:
168 return this.isCommandVisible_(command, itemIds) && 182 return this.isCommandVisible_(command, itemIds) &&
169 this.isCommandEnabled_(command, itemIds); 183 this.isCommandEnabled_(command, itemIds);
170 } 184 }
171 }, 185 },
172 186
173 /** 187 /**
174 * @param {Command} command 188 * @param {Command} command
175 * @param {!Set<string>} itemIds 189 * @param {!Set<string>} itemIds
176 * @return {boolean} True if the command should be visible in the context 190 * @return {boolean} True if the command should be visible in the context
177 * menu. 191 * menu.
178 */ 192 */
179 isCommandVisible_: function(command, itemIds) { 193 isCommandVisible_: function(command, itemIds) {
180 switch (command) { 194 switch (command) {
181 case Command.EDIT: 195 case Command.EDIT:
182 return itemIds.size == 1 && this.globalCanEdit_; 196 return itemIds.size == 1 && this.globalCanEdit_;
183 case Command.COPY: 197 case Command.COPY_URL:
184 return this.isSingleBookmark_(itemIds); 198 return this.isSingleBookmark_(itemIds);
185 case Command.DELETE: 199 case Command.DELETE:
186 return itemIds.size > 0 && this.globalCanEdit_; 200 return itemIds.size > 0 && this.globalCanEdit_;
187 case Command.OPEN_NEW_TAB: 201 case Command.OPEN_NEW_TAB:
188 case Command.OPEN_NEW_WINDOW: 202 case Command.OPEN_NEW_WINDOW:
189 case Command.OPEN_INCOGNITO: 203 case Command.OPEN_INCOGNITO:
190 return itemIds.size > 0; 204 return itemIds.size > 0;
191 default: 205 default:
192 return false; 206 return false;
193 } 207 }
(...skipping 30 matching lines...) Expand all
224 * @param {!Set<string>} itemIds 238 * @param {!Set<string>} itemIds
225 */ 239 */
226 handle: function(command, itemIds) { 240 handle: function(command, itemIds) {
227 var state = this.getState(); 241 var state = this.getState();
228 switch (command) { 242 switch (command) {
229 case Command.EDIT: 243 case Command.EDIT:
230 var id = Array.from(itemIds)[0]; 244 var id = Array.from(itemIds)[0];
231 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get()) 245 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
232 .showEditDialog(state.nodes[id]); 246 .showEditDialog(state.nodes[id]);
233 break; 247 break;
248 case Command.COPY_URL:
234 case Command.COPY: 249 case Command.COPY:
235 var idList = Array.from(itemIds); 250 var idList = Array.from(itemIds);
236 chrome.bookmarkManagerPrivate.copy(idList, function() { 251 chrome.bookmarkManagerPrivate.copy(idList, function() {
237 bookmarks.ToastManager.getInstance().show( 252 var labelPromise;
238 loadTimeData.getString('toastUrlCopied'), false); 253 if (command == Command.COPY_URL) {
239 }); 254 labelPromise =
255 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
256 } else {
257 labelPromise = cr.sendWithPromise(
258 'getPluralString', 'toastItemsCopied', idList.length);
259 }
260
261 this.showTitleToast_(
262 labelPromise, state.nodes[idList[0]].title, false);
263 }.bind(this));
240 break; 264 break;
241 case Command.DELETE: 265 case Command.DELETE:
242 var idList = Array.from(this.minimizeDeletionSet_(itemIds)); 266 var idList = Array.from(this.minimizeDeletionSet_(itemIds));
243 var title = state.nodes[idList[0]].title; 267 var title = state.nodes[idList[0]].title;
244 var labelPromise = cr.sendWithPromise( 268 var labelPromise = cr.sendWithPromise(
245 'getPluralString', 'toastItemsDeleted', idList.length); 269 'getPluralString', 'toastItemsDeleted', idList.length);
246 chrome.bookmarkManagerPrivate.removeTrees(idList, function() { 270 chrome.bookmarkManagerPrivate.removeTrees(idList, function() {
247 labelPromise.then(function(label) { 271 this.showTitleToast_(labelPromise, title, true);
248 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
249 .map(function(p) {
250 // Make the bookmark name collapsible.
251 p.collapsible = !!p.arg;
252 return p;
253 });
254 bookmarks.ToastManager.getInstance().showForStringPieces(
255 pieces, true);
256 }.bind(this));
257 }.bind(this)); 272 }.bind(this));
258 break; 273 break;
259 case Command.UNDO: 274 case Command.UNDO:
260 chrome.bookmarkManagerPrivate.undo(); 275 chrome.bookmarkManagerPrivate.undo();
261 bookmarks.ToastManager.getInstance().hide(); 276 bookmarks.ToastManager.getInstance().hide();
262 break; 277 break;
263 case Command.REDO: 278 case Command.REDO:
264 chrome.bookmarkManagerPrivate.redo(); 279 chrome.bookmarkManagerPrivate.redo();
265 break; 280 break;
266 case Command.OPEN_NEW_TAB: 281 case Command.OPEN_NEW_TAB:
(...skipping 14 matching lines...) Expand all
281 this.openUrls_(this.expandUrls_(itemIds), command); 296 this.openUrls_(this.expandUrls_(itemIds), command);
282 } 297 }
283 break; 298 break;
284 case Command.SELECT_ALL: 299 case Command.SELECT_ALL:
285 var displayedIds = bookmarks.util.getDisplayedList(state); 300 var displayedIds = bookmarks.util.getDisplayedList(state);
286 this.dispatch(bookmarks.actions.selectAll(displayedIds, state)); 301 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
287 break; 302 break;
288 case Command.DESELECT_ALL: 303 case Command.DESELECT_ALL:
289 this.dispatch(bookmarks.actions.deselectItems()); 304 this.dispatch(bookmarks.actions.deselectItems());
290 break; 305 break;
306 case Command.CUT:
307 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
308 break;
309 case Command.PASTE:
310 var selectedFolder = state.selectedFolder;
311 var selectedItems = state.selection.items;
312 chrome.bookmarkManagerPrivate.paste(
313 selectedFolder, Array.from(selectedItems));
314 break;
291 default: 315 default:
292 assert(false); 316 assert(false);
293 } 317 }
294 }, 318 },
295 319
296 /** 320 /**
297 * @param {!Event} e 321 * @param {!Event} e
298 * @param {!Set<string>} itemIds 322 * @param {!Set<string>} itemIds
299 * @return {boolean} True if the event was handled, triggering a keyboard 323 * @return {boolean} True if the event was handled, triggering a keyboard
300 * shortcut. 324 * shortcut.
(...skipping 145 matching lines...) Expand 10 before | Expand all | Expand 10 after
446 * node. 470 * node.
447 */ 471 */
448 isSingleBookmark_: function(itemIds) { 472 isSingleBookmark_: function(itemIds) {
449 return itemIds.size == 1 && 473 return itemIds.size == 1 &&
450 this.containsMatchingNode_(itemIds, function(node) { 474 this.containsMatchingNode_(itemIds, function(node) {
451 return !!node.url; 475 return !!node.url;
452 }); 476 });
453 }, 477 },
454 478
455 /** 479 /**
456 * @param {Event} e
457 * @private
458 */
459 onOpenItemMenu_: function(e) {
460 if (e.detail.targetElement) {
461 this.openCommandMenuAtElement(e.detail.targetElement);
462 } else {
463 this.openCommandMenuAtPosition(e.detail.x, e.detail.y);
464 }
465 },
466
467 /**
468 * @param {Event} e
469 * @private
470 */
471 onCommandClick_: function(e) {
472 this.handle(
473 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
474 this.closeCommandMenu();
475 },
476
477 /**
478 * @param {!Event} e
479 * @private
480 */
481 onKeydown_: function(e) {
482 var selection = this.getState().selection.items;
483 if (e.target == document.body)
484 this.handleKeyEvent(e, selection);
485 },
486
487 /**
488 * Close the menu on mousedown so clicks can propagate to the underlying UI.
489 * This allows the user to right click the list while a context menu is
490 * showing and get another context menu.
491 * @param {Event} e
492 * @private
493 */
494 onMenuMousedown_: function(e) {
495 if (e.path[0] != this.$.dropdown.getIfExists())
496 return;
497
498 this.closeCommandMenu();
499 },
500
501 /** @private */
502 onOpenCancelTap_: function() {
503 this.$.openDialog.get().cancel();
504 },
505
506 /** @private */
507 onOpenConfirmTap_: function() {
508 this.confirmOpenCallback_();
509 this.$.openDialog.get().close();
510 },
511
512 /**
513 * @param {Command} command 480 * @param {Command} command
514 * @return {string} 481 * @return {string}
515 * @private 482 * @private
516 */ 483 */
517 getCommandLabel_: function(command) { 484 getCommandLabel_: function(command) {
518 var multipleNodes = this.menuIds_.size > 1 || 485 var multipleNodes = this.menuIds_.size > 1 ||
519 this.containsMatchingNode_(this.menuIds_, function(node) { 486 this.containsMatchingNode_(this.menuIds_, function(node) {
520 return !node.url; 487 return !node.url;
521 }); 488 });
522 var label; 489 var label;
523 switch (command) { 490 switch (command) {
524 case Command.EDIT: 491 case Command.EDIT:
525 if (this.menuIds_.size != 1) 492 if (this.menuIds_.size != 1)
526 return ''; 493 return '';
527 494
528 var id = Array.from(this.menuIds_)[0]; 495 var id = Array.from(this.menuIds_)[0];
529 var itemUrl = this.getState().nodes[id].url; 496 var itemUrl = this.getState().nodes[id].url;
530 label = itemUrl ? 'menuEdit' : 'menuRename'; 497 label = itemUrl ? 'menuEdit' : 'menuRename';
531 break; 498 break;
532 case Command.COPY: 499 case Command.COPY_URL:
533 label = 'menuCopyURL'; 500 label = 'menuCopyURL';
534 break; 501 break;
535 case Command.DELETE: 502 case Command.DELETE:
536 label = 'menuDelete'; 503 label = 'menuDelete';
537 break; 504 break;
538 case Command.OPEN_NEW_TAB: 505 case Command.OPEN_NEW_TAB:
539 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab'; 506 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
540 break; 507 break;
541 case Command.OPEN_NEW_WINDOW: 508 case Command.OPEN_NEW_WINDOW:
542 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow'; 509 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after
580 547
581 /** 548 /**
582 * @param {Command} command 549 * @param {Command} command
583 * @return {boolean} 550 * @return {boolean}
584 * @private 551 * @private
585 */ 552 */
586 showDividerAfter_: function(command, itemIds) { 553 showDividerAfter_: function(command, itemIds) {
587 return command == Command.DELETE && 554 return command == Command.DELETE &&
588 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)); 555 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds));
589 }, 556 },
557
558 /**
559 * Show a toast with a bookmark |title| inserted into a label, with the
560 * title ellipsised if necessary.
561 * @param {!Promise<string>} labelPromise Promise which resolves with the
562 * label for the toast.
563 * @param {string} title Bookmark title to insert.
564 * @param {boolean} canUndo If true, shows an undo button in the toast.
565 * @private
566 */
567 showTitleToast_: function(labelPromise, title, canUndo) {
568 labelPromise.then(function(label) {
569 var pieces = loadTimeData.getSubstitutedStringPieces(label, title)
570 .map(function(p) {
571 // Make the bookmark name collapsible.
572 p.collapsible = !!p.arg;
573 return p;
574 });
575
576 bookmarks.ToastManager.getInstance().showForStringPieces(
577 pieces, canUndo);
578 });
579 },
580
581 ////////////////////////////////////////////////////////////////////////////
582 // Event handlers:
583
584 /**
585 * @param {Event} e
586 * @private
587 */
588 onOpenItemMenu_: function(e) {
589 if (e.detail.targetElement) {
590 this.openCommandMenuAtElement(e.detail.targetElement);
591 } else {
592 this.openCommandMenuAtPosition(e.detail.x, e.detail.y);
593 }
594 },
595
596 /**
597 * @param {Event} e
598 * @private
599 */
600 onCommandClick_: function(e) {
601 this.handle(
602 e.currentTarget.getAttribute('command'), assert(this.menuIds_));
603 this.closeCommandMenu();
604 },
605
606 /**
607 * @param {!Event} e
608 * @private
609 */
610 onKeydown_: function(e) {
611 var selection = this.getState().selection.items;
612 if (e.target == document.body)
613 this.handleKeyEvent(e, selection);
614 },
615
616 /**
617 * Close the menu on mousedown so clicks can propagate to the underlying UI.
618 * This allows the user to right click the list while a context menu is
619 * showing and get another context menu.
620 * @param {Event} e
621 * @private
622 */
623 onMenuMousedown_: function(e) {
624 if (e.path[0] != this.$.dropdown.getIfExists())
625 return;
626
627 this.closeCommandMenu();
628 },
629
630 /** @private */
631 onOpenCancelTap_: function() {
632 this.$.openDialog.get().cancel();
633 },
634
635 /** @private */
636 onOpenConfirmTap_: function() {
637 this.confirmOpenCallback_();
638 this.$.openDialog.get().close();
639 },
590 }); 640 });
591 641
592 /** @private {bookmarks.CommandManager} */ 642 /** @private {bookmarks.CommandManager} */
593 CommandManager.instance_ = null; 643 CommandManager.instance_ = null;
594 644
595 /** @return {!bookmarks.CommandManager} */ 645 /** @return {!bookmarks.CommandManager} */
596 CommandManager.getInstance = function() { 646 CommandManager.getInstance = function() {
597 return assert(CommandManager.instance_); 647 return assert(CommandManager.instance_);
598 }; 648 };
599 649
600 return { 650 return {
601 CommandManager: CommandManager, 651 CommandManager: CommandManager,
602 }; 652 };
603 }); 653 });
OLDNEW
« no previous file with comments | « chrome/app/bookmarks_strings.grdp ('k') | chrome/browser/resources/md_bookmarks/constants.js » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698