blob: f91cfb161a3c13bf2243df8aa02d20d2ee970bc2 [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
dpapad111a34902017-09-12 16:51:1011 const CommandManager = Polymer({
tsergeant2db36262017-05-15 02:47:5312 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,
Christopher Lam043c7cb2018-01-09 04:14:1422 computed: 'computeMenuCommands_(menuSource_)',
tsergeant13a466462017-05-15 01:21:0323 },
tsergeant2db36262017-05-15 02:47:5324
tsergeant2437f992017-06-13 23:54:2925 /** @private {Set<string>} */
calamity64e2012a2017-06-21 09:57:1426 menuIds_: {
27 type: Object,
calamity64e2012a2017-06-21 09:57:1428 },
29
30 /** @private */
31 hasAnySublabel_: {
32 type: Boolean,
33 reflectToAttribute: true,
Christopher Lam043c7cb2018-01-09 04:14:1434 computed: 'computeHasAnySublabel_(menuCommands_, menuIds_)',
calamity64e2012a2017-06-21 09:57:1435 },
tsergeant2437f992017-06-13 23:54:2936
Christopher Lam043c7cb2018-01-09 04:14:1437 /**
38 * Indicates where the context menu was opened from. Will be NONE if
39 * menu is not open, indicating that commands are from keyboard shortcuts
40 * or elsewhere in the UI.
41 * @private {MenuSource}
42 */
43 menuSource_: MenuSource.NONE,
44
tsergeant2437f992017-06-13 23:54:2945 /** @private */
46 globalCanEdit_: Boolean,
tsergeant13a466462017-05-15 01:21:0347 },
48
tsergeant7fb9e13f2017-06-26 06:35:1949 /** @private {?Function} */
50 confirmOpenCallback_: null,
51
tsergeant2db36262017-05-15 02:47:5352 attached: function() {
53 assert(CommandManager.instance_ == null);
54 CommandManager.instance_ = this;
tsergeant77365182017-05-05 04:02:3355
tsergeant2437f992017-06-13 23:54:2956 this.watch('globalCanEdit_', function(state) {
57 return state.prefs.canEdit;
58 });
59 this.updateFromStore();
60
Tim Sergeanta2233c812017-07-26 03:02:4761 /** @private {!Map<Command, cr.ui.KeyboardShortcutList>} */
62 this.shortcuts_ = new Map();
tsergeant0292e51a2017-06-16 03:44:3563
64 this.addShortcut_(Command.EDIT, 'F2', 'Enter');
tsergeant0292e51a2017-06-16 03:44:3565 this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
66
Tim Sergeant40d2d2c2017-07-20 01:37:0667 this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
tsergeant0292e51a2017-06-16 03:44:3568 this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
69 this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
70
71 this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
72 this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
tsergeant679159f2017-06-16 06:58:4173
74 this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
75 this.addShortcut_(Command.DESELECT_ALL, 'Escape');
tsergeantb7253e92017-07-04 02:59:2276
77 this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
78 this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
79 this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
Christopher Lamf4a16fd2018-02-01 01:47:1180
81 /** @private {!Map<string, Function>} */
82 this.boundListeners_ = new Map();
83
84 const addDocumentListener = (eventName, handler) => {
85 assert(!this.boundListeners_.has(eventName));
86 const boundListener = handler.bind(this);
87 this.boundListeners_.set(eventName, boundListener);
88 document.addEventListener(eventName, boundListener);
89 };
90 addDocumentListener('open-command-menu', this.onOpenCommandMenu_);
91 addDocumentListener('keydown', this.onKeydown_);
92
93 const addDocumentListenerForCommand = (eventName, command) => {
94 addDocumentListener(eventName, (e) => {
95 if (e.path[0].tagName == 'INPUT')
96 return;
97
98 const items = this.getState().selection.items;
99 if (this.canExecute(command, items))
100 this.handle(command, items);
101 });
102 };
103 addDocumentListenerForCommand('command-undo', Command.UNDO);
104 addDocumentListenerForCommand('cut', Command.CUT);
105 addDocumentListenerForCommand('copy', Command.COPY);
106 addDocumentListenerForCommand('paste', Command.PASTE);
tsergeant2db36262017-05-15 02:47:53107 },
tsergeant77365182017-05-05 04:02:33108
tsergeant2db36262017-05-15 02:47:53109 detached: function() {
110 CommandManager.instance_ = null;
Christopher Lamf4a16fd2018-02-01 01:47:11111 this.boundListeners_.forEach(
112 (handler, eventName) =>
113 document.removeEventListener(eventName, handler));
tsergeant2db36262017-05-15 02:47:53114 },
tsergeant77365182017-05-05 04:02:33115
tsergeant2db36262017-05-15 02:47:53116 /**
117 * Display the command context menu at (|x|, |y|) in window co-ordinates.
tsergeanta274e0412017-06-16 05:22:28118 * Commands will execute on |items| if given, or on the currently selected
119 * items.
tsergeant2db36262017-05-15 02:47:53120 * @param {number} x
121 * @param {number} y
tsergeantd16c95a2017-07-14 04:49:43122 * @param {MenuSource} source
tsergeanta274e0412017-06-16 05:22:28123 * @param {Set<string>=} items
tsergeant2db36262017-05-15 02:47:53124 */
tsergeantd16c95a2017-07-14 04:49:43125 openCommandMenuAtPosition: function(x, y, source, items) {
126 this.menuSource_ = source;
tsergeanta274e0412017-06-16 05:22:28127 this.menuIds_ = items || this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58128
dpapad111a34902017-09-12 16:51:10129 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27130 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
131 // Ensure that the menu is fully rendered before trying to position it.
132 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58133 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24134 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58135 dropdown.showAtPosition({top: y, left: x});
136 });
tsergeant2db36262017-05-15 02:47:53137 },
tsergeant77365182017-05-05 04:02:33138
tsergeant2db36262017-05-15 02:47:53139 /**
140 * Display the command context menu positioned to cover the |target|
141 * element. Commands will execute on the currently selected items.
142 * @param {!Element} target
tsergeantd16c95a2017-07-14 04:49:43143 * @param {MenuSource} source
tsergeant2db36262017-05-15 02:47:53144 */
tsergeantd16c95a2017-07-14 04:49:43145 openCommandMenuAtElement: function(target, source) {
146 this.menuSource_ = source;
tsergeant2db36262017-05-15 02:47:53147 this.menuIds_ = this.getState().selection.items;
calamity57b2e8fd2017-06-29 18:46:58148
dpapad111a34902017-09-12 16:51:10149 const dropdown =
tsergeant9b9aa15f2017-06-22 03:22:27150 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get());
151 // Ensure that the menu is fully rendered before trying to position it.
152 Polymer.dom.flush();
calamity57b2e8fd2017-06-29 18:46:58153 bookmarks.DialogFocusManager.getInstance().showDialog(
Christopher Lam42944a02018-03-16 04:11:24154 dropdown.getDialog(), function() {
calamity57b2e8fd2017-06-29 18:46:58155 dropdown.showAt(target);
156 });
tsergeant2db36262017-05-15 02:47:53157 },
tsergeant77365182017-05-05 04:02:33158
tsergeant2db36262017-05-15 02:47:53159 closeCommandMenu: function() {
tsergeant4707d172017-06-05 05:47:02160 this.menuIds_ = new Set();
tsergeantd16c95a2017-07-14 04:49:43161 this.menuSource_ = MenuSource.NONE;
tsergeant9b9aa15f2017-06-22 03:22:27162 /** @type {!CrActionMenuElement} */ (this.$.dropdown.get()).close();
tsergeant2db36262017-05-15 02:47:53163 },
tsergeant77365182017-05-05 04:02:33164
tsergeant2db36262017-05-15 02:47:53165 ////////////////////////////////////////////////////////////////////////////
166 // Command handlers:
tsergeant77365182017-05-05 04:02:33167
tsergeant2db36262017-05-15 02:47:53168 /**
169 * Determine if the |command| can be executed with the given |itemIds|.
170 * Commands which appear in the context menu should be implemented
171 * separately using `isCommandVisible_` and `isCommandEnabled_`.
172 * @param {Command} command
173 * @param {!Set<string>} itemIds
174 * @return {boolean}
175 */
176 canExecute: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10177 const state = this.getState();
tsergeant6c5ad90a2017-05-19 14:12:34178 switch (command) {
179 case Command.OPEN:
180 return itemIds.size > 0;
calamity2d4b5502017-05-29 03:57:58181 case Command.UNDO:
182 case Command.REDO:
tsergeant2437f992017-06-13 23:54:29183 return this.globalCanEdit_;
tsergeant679159f2017-06-16 06:58:41184 case Command.SELECT_ALL:
185 case Command.DESELECT_ALL:
186 return true;
tsergeantb7253e92017-07-04 02:59:22187 case Command.COPY:
188 return itemIds.size > 0;
189 case Command.CUT:
190 return itemIds.size > 0 &&
191 !this.containsMatchingNode_(itemIds, function(node) {
192 return !bookmarks.util.canEditNode(state, node.id);
193 });
194 case Command.PASTE:
195 return state.search.term == '' &&
196 bookmarks.util.canReorderChildren(state, state.selectedFolder);
tsergeant6c5ad90a2017-05-19 14:12:34197 default:
198 return this.isCommandVisible_(command, itemIds) &&
199 this.isCommandEnabled_(command, itemIds);
200 }
tsergeant2db36262017-05-15 02:47:53201 },
tsergeant13a466462017-05-15 01:21:03202
tsergeant2db36262017-05-15 02:47:53203 /**
204 * @param {Command} command
205 * @param {!Set<string>} itemIds
206 * @return {boolean} True if the command should be visible in the context
207 * menu.
208 */
209 isCommandVisible_: function(command, itemIds) {
210 switch (command) {
211 case Command.EDIT:
tsergeant2437f992017-06-13 23:54:29212 return itemIds.size == 1 && this.globalCanEdit_;
tsergeantb7253e92017-07-04 02:59:22213 case Command.COPY_URL:
tsergeant2437f992017-06-13 23:54:29214 return this.isSingleBookmark_(itemIds);
tsergeant2db36262017-05-15 02:47:53215 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29216 return itemIds.size > 0 && this.globalCanEdit_;
tsergeantd16c95a2017-07-14 04:49:43217 case Command.SHOW_IN_FOLDER:
Christopher Lam043c7cb2018-01-09 04:14:14218 return this.menuSource_ == MenuSource.ITEM && itemIds.size == 1 &&
tsergeantd16c95a2017-07-14 04:49:43219 this.getState().search.term != '' &&
220 !this.containsMatchingNode_(itemIds, function(node) {
221 return !node.parentId || node.parentId == ROOT_NODE_ID;
222 });
tsergeant2db36262017-05-15 02:47:53223 case Command.OPEN_NEW_TAB:
224 case Command.OPEN_NEW_WINDOW:
225 case Command.OPEN_INCOGNITO:
226 return itemIds.size > 0;
Christopher Lam043c7cb2018-01-09 04:14:14227 case Command.ADD_BOOKMARK:
228 case Command.ADD_FOLDER:
229 case Command.SORT:
230 case Command.EXPORT:
231 case Command.IMPORT:
Christopher Lam34be14992018-01-17 11:01:28232 case Command.HELP_CENTER:
Christopher Lam043c7cb2018-01-09 04:14:14233 return true;
tsergeantf1ffc892017-05-05 07:43:43234 }
Christopher Lam34be14992018-01-17 11:01:28235 return assert(false);
tsergeant2db36262017-05-15 02:47:53236 },
tsergeantf1ffc892017-05-05 07:43:43237
tsergeant2db36262017-05-15 02:47:53238 /**
239 * @param {Command} command
240 * @param {!Set<string>} itemIds
241 * @return {boolean} True if the command should be clickable in the context
242 * menu.
243 */
244 isCommandEnabled_: function(command, itemIds) {
Christopher Lam043c7cb2018-01-09 04:14:14245 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53246 switch (command) {
tsergeant2437f992017-06-13 23:54:29247 case Command.EDIT:
248 case Command.DELETE:
tsergeant2437f992017-06-13 23:54:29249 return !this.containsMatchingNode_(itemIds, function(node) {
250 return !bookmarks.util.canEditNode(state, node.id);
251 });
tsergeant2db36262017-05-15 02:47:53252 case Command.OPEN_NEW_TAB:
253 case Command.OPEN_NEW_WINDOW:
tsergeant2db36262017-05-15 02:47:53254 return this.expandUrls_(itemIds).length > 0;
tsergeant4707d172017-06-05 05:47:02255 case Command.OPEN_INCOGNITO:
256 return this.expandUrls_(itemIds).length > 0 &&
Christopher Lam043c7cb2018-01-09 04:14:14257 state.prefs.incognitoAvailability !=
tsergeant4707d172017-06-05 05:47:02258 IncognitoAvailability.DISABLED;
Christopher Lam043c7cb2018-01-09 04:14:14259 case Command.SORT:
260 return this.canChangeList_() &&
261 state.nodes[state.selectedFolder].children.length > 1;
262 case Command.ADD_BOOKMARK:
263 case Command.ADD_FOLDER:
264 return this.canChangeList_();
265 case Command.IMPORT:
266 return this.globalCanEdit_;
tsergeant2db36262017-05-15 02:47:53267 default:
268 return true;
269 }
270 },
tsergeant13a466462017-05-15 01:21:03271
tsergeant2db36262017-05-15 02:47:53272 /**
Christopher Lam043c7cb2018-01-09 04:14:14273 * Returns whether the currently displayed bookmarks list can be changed.
274 * @private
275 * @return {boolean}
276 */
277 canChangeList_: function() {
278 const state = this.getState();
279 return state.search.term == '' &&
280 bookmarks.util.canReorderChildren(state, state.selectedFolder);
281 },
282
283 /**
tsergeant2db36262017-05-15 02:47:53284 * @param {Command} command
285 * @param {!Set<string>} itemIds
286 */
287 handle: function(command, itemIds) {
dpapad111a34902017-09-12 16:51:10288 const state = this.getState();
tsergeant2db36262017-05-15 02:47:53289 switch (command) {
dpapad111a34902017-09-12 16:51:10290 case Command.EDIT: {
291 let id = Array.from(itemIds)[0];
tsergeant2db36262017-05-15 02:47:53292 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
tsergeant0292e51a2017-06-16 03:44:35293 .showEditDialog(state.nodes[id]);
tsergeant2db36262017-05-15 02:47:53294 break;
dpapad111a34902017-09-12 16:51:10295 }
tsergeantb7253e92017-07-04 02:59:22296 case Command.COPY_URL:
dpapad111a34902017-09-12 16:51:10297 case Command.COPY: {
298 let idList = Array.from(itemIds);
dpapad325bf2f12017-07-26 18:47:34299 chrome.bookmarkManagerPrivate.copy(idList, () => {
dpapad111a34902017-09-12 16:51:10300 let labelPromise;
tsergeantb7253e92017-07-04 02:59:22301 if (command == Command.COPY_URL) {
302 labelPromise =
303 Promise.resolve(loadTimeData.getString('toastUrlCopied'));
Christopher Lam75ca9102017-07-18 02:15:18304 } else if (idList.length == 1) {
305 labelPromise =
306 Promise.resolve(loadTimeData.getString('toastItemCopied'));
tsergeantb7253e92017-07-04 02:59:22307 } else {
308 labelPromise = cr.sendWithPromise(
309 'getPluralString', 'toastItemsCopied', idList.length);
310 }
311
312 this.showTitleToast_(
313 labelPromise, state.nodes[idList[0]].title, false);
dpapad325bf2f12017-07-26 18:47:34314 });
tsergeant2db36262017-05-15 02:47:53315 break;
dpapad111a34902017-09-12 16:51:10316 }
317 case Command.SHOW_IN_FOLDER: {
318 let id = Array.from(itemIds)[0];
tsergeantd16c95a2017-07-14 04:49:43319 this.dispatch(bookmarks.actions.selectFolder(
320 assert(state.nodes[id].parentId), state.nodes));
tsergeantf42530c2017-07-28 02:44:57321 bookmarks.DialogFocusManager.getInstance().clearFocus();
322 this.fire('highlight-items', [id]);
tsergeantd16c95a2017-07-14 04:49:43323 break;
dpapad111a34902017-09-12 16:51:10324 }
325 case Command.DELETE: {
326 let idList = Array.from(this.minimizeDeletionSet_(itemIds));
327 const title = state.nodes[idList[0]].title;
328 let labelPromise;
Christopher Lam75ca9102017-07-18 02:15:18329
330 if (idList.length == 1) {
331 labelPromise =
332 Promise.resolve(loadTimeData.getString('toastItemDeleted'));
333 } else {
334 labelPromise = cr.sendWithPromise(
335 'getPluralString', 'toastItemsDeleted', idList.length);
336 }
337
dpapad325bf2f12017-07-26 18:47:34338 chrome.bookmarkManagerPrivate.removeTrees(idList, () => {
tsergeantb7253e92017-07-04 02:59:22339 this.showTitleToast_(labelPromise, title, true);
dpapad325bf2f12017-07-26 18:47:34340 });
tsergeant2db36262017-05-15 02:47:53341 break;
dpapad111a34902017-09-12 16:51:10342 }
calamity2d4b5502017-05-29 03:57:58343 case Command.UNDO:
344 chrome.bookmarkManagerPrivate.undo();
calamityefe477352017-06-07 06:44:58345 bookmarks.ToastManager.getInstance().hide();
calamity2d4b5502017-05-29 03:57:58346 break;
347 case Command.REDO:
348 chrome.bookmarkManagerPrivate.redo();
349 break;
tsergeant2db36262017-05-15 02:47:53350 case Command.OPEN_NEW_TAB:
351 case Command.OPEN_NEW_WINDOW:
352 case Command.OPEN_INCOGNITO:
353 this.openUrls_(this.expandUrls_(itemIds), command);
354 break;
tsergeant6c5ad90a2017-05-19 14:12:34355 case Command.OPEN:
dpapad111a34902017-09-12 16:51:10356 const isFolder = itemIds.size == 1 &&
tsergeant6c5ad90a2017-05-19 14:12:34357 this.containsMatchingNode_(itemIds, function(node) {
358 return !node.url;
359 });
360 if (isFolder) {
dpapad111a34902017-09-12 16:51:10361 const folderId = Array.from(itemIds)[0];
tsergeant0292e51a2017-06-16 03:44:35362 this.dispatch(
363 bookmarks.actions.selectFolder(folderId, state.nodes));
tsergeant6c5ad90a2017-05-19 14:12:34364 } else {
365 this.openUrls_(this.expandUrls_(itemIds), command);
366 }
367 break;
tsergeant679159f2017-06-16 06:58:41368 case Command.SELECT_ALL:
dpapad111a34902017-09-12 16:51:10369 const displayedIds = bookmarks.util.getDisplayedList(state);
tsergeant679159f2017-06-16 06:58:41370 this.dispatch(bookmarks.actions.selectAll(displayedIds, state));
371 break;
372 case Command.DESELECT_ALL:
373 this.dispatch(bookmarks.actions.deselectItems());
374 break;
tsergeantb7253e92017-07-04 02:59:22375 case Command.CUT:
376 chrome.bookmarkManagerPrivate.cut(Array.from(itemIds));
377 break;
378 case Command.PASTE:
dpapad111a34902017-09-12 16:51:10379 const selectedFolder = state.selectedFolder;
380 const selectedItems = state.selection.items;
tsergeantf42530c2017-07-28 02:44:57381 bookmarks.ApiListener.trackUpdatedItems();
tsergeantb7253e92017-07-04 02:59:22382 chrome.bookmarkManagerPrivate.paste(
tsergeantf42530c2017-07-28 02:44:57383 selectedFolder, Array.from(selectedItems),
384 bookmarks.ApiListener.highlightUpdatedItems);
tsergeantb7253e92017-07-04 02:59:22385 break;
Christopher Lam043c7cb2018-01-09 04:14:14386 case Command.SORT:
387 chrome.bookmarkManagerPrivate.sortChildren(
388 assert(state.selectedFolder));
389 bookmarks.ToastManager.getInstance().show(
390 loadTimeData.getString('toastFolderSorted'), true);
391 break;
392 case Command.ADD_BOOKMARK:
393 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
394 .showAddDialog(false, assert(state.selectedFolder));
395 break;
396 case Command.ADD_FOLDER:
397 /** @type {!BookmarksEditDialogElement} */ (this.$.editDialog.get())
398 .showAddDialog(true, assert(state.selectedFolder));
399 break;
400 case Command.IMPORT:
401 chrome.bookmarks.import();
402 break;
403 case Command.EXPORT:
404 chrome.bookmarks.export();
405 break;
Christopher Lam34be14992018-01-17 11:01:28406 case Command.HELP_CENTER:
407 window.open('https://support.google.com/chrome/?p=bookmarks');
408 break;
tsergeant6c5ad90a2017-05-19 14:12:34409 default:
410 assert(false);
tsergeant2db36262017-05-15 02:47:53411 }
Tim Sergeanta2233c812017-07-26 03:02:47412
413 bookmarks.util.recordEnumHistogram(
414 'BookmarkManager.CommandExecuted', command, Command.MAX_VALUE);
tsergeant2db36262017-05-15 02:47:53415 },
tsergeant13a466462017-05-15 01:21:03416
tsergeant6c3a6df2017-06-06 23:53:02417 /**
tsergeant0292e51a2017-06-16 03:44:35418 * @param {!Event} e
tsergeant6c3a6df2017-06-06 23:53:02419 * @param {!Set<string>} itemIds
420 * @return {boolean} True if the event was handled, triggering a keyboard
421 * shortcut.
422 */
423 handleKeyEvent: function(e, itemIds) {
dpapad111a34902017-09-12 16:51:10424 for (const commandTuple of this.shortcuts_) {
425 const command = /** @type {Command} */ (commandTuple[0]);
426 const shortcut =
Tim Sergeanta2233c812017-07-26 03:02:47427 /** @type {cr.ui.KeyboardShortcutList} */ (commandTuple[1]);
428 if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
429 this.handle(command, itemIds);
tsergeant6c3a6df2017-06-06 23:53:02430
Tim Sergeanta2233c812017-07-26 03:02:47431 bookmarks.util.recordEnumHistogram(
432 'BookmarkManager.CommandExecutedFromKeyboard', command,
433 Command.MAX_VALUE);
tsergeant6c3a6df2017-06-06 23:53:02434 e.stopPropagation();
435 e.preventDefault();
436 return true;
437 }
438 }
439
440 return false;
441 },
442
tsergeant2db36262017-05-15 02:47:53443 ////////////////////////////////////////////////////////////////////////////
444 // Private functions:
445
446 /**
tsergeant0292e51a2017-06-16 03:44:35447 * Register a keyboard shortcut for a command.
448 * @param {Command} command Command that the shortcut will trigger.
449 * @param {string} shortcut Keyboard shortcut, using the syntax of
450 * cr/ui/command.js.
451 * @param {string=} macShortcut If set, enables a replacement shortcut for
452 * Mac.
453 */
454 addShortcut_: function(command, shortcut, macShortcut) {
dpapad111a34902017-09-12 16:51:10455 shortcut = (cr.isMac && macShortcut) ? macShortcut : shortcut;
Tim Sergeanta2233c812017-07-26 03:02:47456 this.shortcuts_.set(command, new cr.ui.KeyboardShortcutList(shortcut));
tsergeant0292e51a2017-06-16 03:44:35457 },
458
459 /**
tsergeant2db36262017-05-15 02:47:53460 * Minimize the set of |itemIds| by removing any node which has an ancestor
461 * node already in the set. This ensures that instead of trying to delete
462 * both a node and its descendant, we will only try to delete the topmost
463 * node, preventing an error in the bookmarkManagerPrivate.removeTrees API
464 * call.
465 * @param {!Set<string>} itemIds
466 * @return {!Set<string>}
467 */
468 minimizeDeletionSet_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10469 const minimizedSet = new Set();
470 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53471 itemIds.forEach(function(itemId) {
dpapad111a34902017-09-12 16:51:10472 let currentId = itemId;
tsergeant2db36262017-05-15 02:47:53473 while (currentId != ROOT_NODE_ID) {
474 currentId = assert(nodes[currentId].parentId);
475 if (itemIds.has(currentId))
476 return;
477 }
478 minimizedSet.add(itemId);
tsergeant13a466462017-05-15 01:21:03479 });
tsergeant2db36262017-05-15 02:47:53480 return minimizedSet;
481 },
tsergeant13a466462017-05-15 01:21:03482
tsergeant2db36262017-05-15 02:47:53483 /**
tsergeant7fb9e13f2017-06-26 06:35:19484 * Open the given |urls| in response to a |command|. May show a confirmation
485 * dialog before opening large numbers of URLs.
tsergeant2db36262017-05-15 02:47:53486 * @param {!Array<string>} urls
487 * @param {Command} command
488 * @private
489 */
490 openUrls_: function(urls, command) {
491 assert(
tsergeant6c5ad90a2017-05-19 14:12:34492 command == Command.OPEN || command == Command.OPEN_NEW_TAB ||
tsergeant2db36262017-05-15 02:47:53493 command == Command.OPEN_NEW_WINDOW ||
494 command == Command.OPEN_INCOGNITO);
tsergeant13a466462017-05-15 01:21:03495
tsergeant2db36262017-05-15 02:47:53496 if (urls.length == 0)
tsergeantfad224f2017-05-05 04:42:09497 return;
tsergeantfad224f2017-05-05 04:42:09498
dpapad111a34902017-09-12 16:51:10499 const openUrlsCallback = function() {
500 const incognito = command == Command.OPEN_INCOGNITO;
tsergeant7fb9e13f2017-06-26 06:35:19501 if (command == Command.OPEN_NEW_WINDOW || incognito) {
502 chrome.windows.create({url: urls, incognito: incognito});
503 } else {
504 if (command == Command.OPEN)
505 chrome.tabs.create({url: urls.shift(), active: true});
506 urls.forEach(function(url) {
507 chrome.tabs.create({url: url, active: false});
508 });
509 }
510 };
511
512 if (urls.length <= OPEN_CONFIRMATION_LIMIT) {
513 openUrlsCallback();
514 return;
tsergeant2db36262017-05-15 02:47:53515 }
tsergeant7fb9e13f2017-06-26 06:35:19516
517 this.confirmOpenCallback_ = openUrlsCallback;
dpapad111a34902017-09-12 16:51:10518 const dialog = this.$.openDialog.get();
Dave Schuyler81c6fb82017-08-01 20:19:16519 dialog.querySelector('[slot=body]').textContent =
tsergeant7fb9e13f2017-06-26 06:35:19520 loadTimeData.getStringF('openDialogBody', urls.length);
calamity57b2e8fd2017-06-29 18:46:58521
522 bookmarks.DialogFocusManager.getInstance().showDialog(
523 this.$.openDialog.get());
tsergeant2db36262017-05-15 02:47:53524 },
tsergeant77365182017-05-05 04:02:33525
tsergeant2db36262017-05-15 02:47:53526 /**
527 * Returns all URLs in the given set of nodes and their immediate children.
528 * Note that these will be ordered by insertion order into the |itemIds|
529 * set, and that it is possible to duplicate a URL by passing in both the
530 * parent ID and child ID.
531 * @param {!Set<string>} itemIds
532 * @return {!Array<string>}
533 * @private
534 */
535 expandUrls_: function(itemIds) {
dpapad111a34902017-09-12 16:51:10536 const urls = [];
537 const nodes = this.getState().nodes;
tsergeant13a466462017-05-15 01:21:03538
tsergeant2db36262017-05-15 02:47:53539 itemIds.forEach(function(id) {
dpapad111a34902017-09-12 16:51:10540 const node = nodes[id];
tsergeant2db36262017-05-15 02:47:53541 if (node.url) {
542 urls.push(node.url);
543 } else {
544 node.children.forEach(function(childId) {
dpapad111a34902017-09-12 16:51:10545 const childNode = nodes[childId];
tsergeant2db36262017-05-15 02:47:53546 if (childNode.url)
547 urls.push(childNode.url);
548 });
549 }
550 });
tsergeant13a466462017-05-15 01:21:03551
tsergeant2db36262017-05-15 02:47:53552 return urls;
553 },
554
555 /**
556 * @param {!Set<string>} itemIds
557 * @param {function(BookmarkNode):boolean} predicate
558 * @return {boolean} True if any node in |itemIds| returns true for
559 * |predicate|.
560 */
561 containsMatchingNode_: function(itemIds, predicate) {
dpapad111a34902017-09-12 16:51:10562 const nodes = this.getState().nodes;
tsergeant2db36262017-05-15 02:47:53563
564 return Array.from(itemIds).some(function(id) {
565 return predicate(nodes[id]);
566 });
567 },
568
569 /**
tsergeant2437f992017-06-13 23:54:29570 * @param {!Set<string>} itemIds
571 * @return {boolean} True if |itemIds| is a single bookmark (non-folder)
572 * node.
573 */
574 isSingleBookmark_: function(itemIds) {
575 return itemIds.size == 1 &&
576 this.containsMatchingNode_(itemIds, function(node) {
577 return !!node.url;
578 });
579 },
580
581 /**
tsergeant2db36262017-05-15 02:47:53582 * @param {Command} command
583 * @return {string}
584 * @private
585 */
586 getCommandLabel_: function(command) {
dpapad111a34902017-09-12 16:51:10587 const multipleNodes = this.menuIds_.size > 1 ||
tsergeant2db36262017-05-15 02:47:53588 this.containsMatchingNode_(this.menuIds_, function(node) {
589 return !node.url;
590 });
dpapad111a34902017-09-12 16:51:10591 let label;
tsergeant2db36262017-05-15 02:47:53592 switch (command) {
593 case Command.EDIT:
tsergeant4707d172017-06-05 05:47:02594 if (this.menuIds_.size != 1)
tsergeant2db36262017-05-15 02:47:53595 return '';
596
dpapad111a34902017-09-12 16:51:10597 const id = Array.from(this.menuIds_)[0];
598 const itemUrl = this.getState().nodes[id].url;
tsergeant2db36262017-05-15 02:47:53599 label = itemUrl ? 'menuEdit' : 'menuRename';
600 break;
tsergeantb7253e92017-07-04 02:59:22601 case Command.COPY_URL:
tsergeant2db36262017-05-15 02:47:53602 label = 'menuCopyURL';
603 break;
604 case Command.DELETE:
605 label = 'menuDelete';
606 break;
tsergeantd16c95a2017-07-14 04:49:43607 case Command.SHOW_IN_FOLDER:
608 label = 'menuShowInFolder';
609 break;
tsergeant2db36262017-05-15 02:47:53610 case Command.OPEN_NEW_TAB:
611 label = multipleNodes ? 'menuOpenAllNewTab' : 'menuOpenNewTab';
612 break;
613 case Command.OPEN_NEW_WINDOW:
614 label = multipleNodes ? 'menuOpenAllNewWindow' : 'menuOpenNewWindow';
615 break;
616 case Command.OPEN_INCOGNITO:
617 label = multipleNodes ? 'menuOpenAllIncognito' : 'menuOpenIncognito';
618 break;
Christopher Lam043c7cb2018-01-09 04:14:14619 case Command.SORT:
620 label = 'menuSort';
621 break;
622 case Command.ADD_BOOKMARK:
623 label = 'menuAddBookmark';
624 break;
625 case Command.ADD_FOLDER:
626 label = 'menuAddFolder';
627 break;
628 case Command.IMPORT:
629 label = 'menuImport';
630 break;
631 case Command.EXPORT:
632 label = 'menuExport';
633 break;
Christopher Lam34be14992018-01-17 11:01:28634 case Command.HELP_CENTER:
635 label = 'menuHelpCenter';
636 break;
tsergeant2db36262017-05-15 02:47:53637 }
Christopher Lam043c7cb2018-01-09 04:14:14638 assert(label);
tsergeant2db36262017-05-15 02:47:53639
640 return loadTimeData.getString(assert(label));
641 },
642
643 /**
644 * @param {Command} command
calamity64e2012a2017-06-21 09:57:14645 * @return {string}
646 * @private
647 */
648 getCommandSublabel_: function(command) {
dpapad111a34902017-09-12 16:51:10649 const multipleNodes = this.menuIds_.size > 1 ||
calamity64e2012a2017-06-21 09:57:14650 this.containsMatchingNode_(this.menuIds_, function(node) {
651 return !node.url;
652 });
653 switch (command) {
654 case Command.OPEN_NEW_TAB:
dpapad111a34902017-09-12 16:51:10655 const urls = this.expandUrls_(this.menuIds_);
calamity64e2012a2017-06-21 09:57:14656 return multipleNodes && urls.length > 0 ? String(urls.length) : '';
657 default:
658 return '';
659 }
660 },
661
662 /** @private */
Christopher Lam043c7cb2018-01-09 04:14:14663 computeMenuCommands_: function() {
664 switch (this.menuSource_) {
665 case MenuSource.ITEM:
666 case MenuSource.TREE:
667 return [
668 Command.EDIT,
669 Command.COPY_URL,
670 Command.SHOW_IN_FOLDER,
671 Command.DELETE,
672 // <hr>
673 Command.OPEN_NEW_TAB,
674 Command.OPEN_NEW_WINDOW,
675 Command.OPEN_INCOGNITO,
676 ];
677 case MenuSource.TOOLBAR:
678 return [
679 Command.SORT,
680 // <hr>
681 Command.ADD_BOOKMARK,
682 Command.ADD_FOLDER,
683 // <hr>
684 Command.IMPORT,
685 Command.EXPORT,
Christopher Lam34be14992018-01-17 11:01:28686 // <hr>
687 Command.HELP_CENTER,
Christopher Lam043c7cb2018-01-09 04:14:14688 ];
Christopher Lamfde61782018-01-11 04:27:07689 case MenuSource.LIST:
690 return [
691 Command.ADD_BOOKMARK,
692 Command.ADD_FOLDER,
693 ];
Christopher Lam043c7cb2018-01-09 04:14:14694 case MenuSource.NONE:
695 return [];
696 }
697 assert(false);
698 },
699
Christopher Lam430ef002018-01-18 10:08:50700 /**
701 * @return {boolean}
702 * @private
703 */
Christopher Lam043c7cb2018-01-09 04:14:14704 computeHasAnySublabel_: function() {
calamity64e2012a2017-06-21 09:57:14705 if (!this.menuIds_)
Christopher Lam430ef002018-01-18 10:08:50706 return false;
calamity64e2012a2017-06-21 09:57:14707
Christopher Lam430ef002018-01-18 10:08:50708 return this.menuCommands_.some(
dpapad325bf2f12017-07-26 18:47:34709 (command) => this.getCommandSublabel_(command) != '');
calamity64e2012a2017-06-21 09:57:14710 },
711
712 /**
713 * @param {Command} command
tsergeant2db36262017-05-15 02:47:53714 * @return {boolean}
715 * @private
716 */
tsergeant2437f992017-06-13 23:54:29717 showDividerAfter_: function(command, itemIds) {
Christopher Lam34be14992018-01-17 11:01:28718 return ((command == Command.SORT || command == Command.ADD_FOLDER ||
719 command == Command.EXPORT) &&
Christopher Lam043c7cb2018-01-09 04:14:14720 this.menuSource_ == MenuSource.TOOLBAR) ||
721 (command == Command.DELETE &&
722 (this.globalCanEdit_ || this.isSingleBookmark_(itemIds)));
tsergeant2db36262017-05-15 02:47:53723 },
tsergeantb7253e92017-07-04 02:59:22724
725 /**
726 * Show a toast with a bookmark |title| inserted into a label, with the
727 * title ellipsised if necessary.
728 * @param {!Promise<string>} labelPromise Promise which resolves with the
729 * label for the toast.
730 * @param {string} title Bookmark title to insert.
731 * @param {boolean} canUndo If true, shows an undo button in the toast.
732 * @private
733 */
734 showTitleToast_: function(labelPromise, title, canUndo) {
735 labelPromise.then(function(label) {
dpapad111a34902017-09-12 16:51:10736 const pieces = loadTimeData.getSubstitutedStringPieces(label, title)
737 .map(function(p) {
738 // Make the bookmark name collapsible.
739 p.collapsible = !!p.arg;
740 return p;
741 });
tsergeantb7253e92017-07-04 02:59:22742
743 bookmarks.ToastManager.getInstance().showForStringPieces(
744 pieces, canUndo);
745 });
746 },
747
748 ////////////////////////////////////////////////////////////////////////////
749 // Event handlers:
750
751 /**
752 * @param {Event} e
753 * @private
754 */
Christopher Lam043c7cb2018-01-09 04:14:14755 onOpenCommandMenu_: function(e) {
tsergeantb7253e92017-07-04 02:59:22756 if (e.detail.targetElement) {
tsergeantd16c95a2017-07-14 04:49:43757 this.openCommandMenuAtElement(e.detail.targetElement, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22758 } else {
tsergeantd16c95a2017-07-14 04:49:43759 this.openCommandMenuAtPosition(e.detail.x, e.detail.y, e.detail.source);
tsergeantb7253e92017-07-04 02:59:22760 }
Christopher Lamed175322018-01-18 14:54:49761 bookmarks.util.recordEnumHistogram(
762 'BookmarkManager.CommandMenuOpened', e.detail.source,
763 MenuSource.NUM_VALUES);
tsergeantb7253e92017-07-04 02:59:22764 },
765
766 /**
767 * @param {Event} e
768 * @private
769 */
770 onCommandClick_: function(e) {
771 this.handle(
Tim Sergeanta2233c812017-07-26 03:02:47772 /** @type {Command} */ (
773 Number(e.currentTarget.getAttribute('command'))),
774 assert(this.menuIds_));
tsergeantb7253e92017-07-04 02:59:22775 this.closeCommandMenu();
776 },
777
778 /**
779 * @param {!Event} e
780 * @private
781 */
782 onKeydown_: function(e) {
dpapad111a34902017-09-12 16:51:10783 const selection = this.getState().selection.items;
Tim Sergeant109279fe2017-07-20 03:07:17784 if (e.target == document.body &&
785 !bookmarks.DialogFocusManager.getInstance().hasOpenDialog()) {
tsergeantb7253e92017-07-04 02:59:22786 this.handleKeyEvent(e, selection);
Tim Sergeant109279fe2017-07-20 03:07:17787 }
tsergeantb7253e92017-07-04 02:59:22788 },
789
790 /**
791 * Close the menu on mousedown so clicks can propagate to the underlying UI.
792 * This allows the user to right click the list while a context menu is
793 * showing and get another context menu.
794 * @param {Event} e
795 * @private
796 */
797 onMenuMousedown_: function(e) {
Christopher Lamf65fe25c2018-04-18 08:59:49798 if (e.path[0].tagName != 'DIALOG')
tsergeantb7253e92017-07-04 02:59:22799 return;
800
801 this.closeCommandMenu();
802 },
803
804 /** @private */
805 onOpenCancelTap_: function() {
806 this.$.openDialog.get().cancel();
807 },
808
809 /** @private */
810 onOpenConfirmTap_: function() {
811 this.confirmOpenCallback_();
812 this.$.openDialog.get().close();
813 },
tsergeant2db36262017-05-15 02:47:53814 });
815
816 /** @private {bookmarks.CommandManager} */
817 CommandManager.instance_ = null;
818
819 /** @return {!bookmarks.CommandManager} */
820 CommandManager.getInstance = function() {
821 return assert(CommandManager.instance_);
822 };
823
824 return {
825 CommandManager: CommandManager,
826 };
tsergeant77365182017-05-05 04:02:33827});