// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Element which shows context menus and handles keyboard
* shortcuts.
*/
import 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './edit_dialog.js';
import './shared_style.css.js';
import './strings.m.js';
import './edit_dialog.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrActionMenuElement} from 'chrome://resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrDialogElement} from 'chrome://resources/cr_elements/cr_dialog/cr_dialog.js';
import type {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {getToastManager} from 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js';
import {assert, assertNotReached} from 'chrome://resources/js/assert.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {KeyboardShortcutList} from 'chrome://resources/js/keyboard_shortcut_list.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {PluralStringProxyImpl} from 'chrome://resources/js/plural_string_proxy.js';
import {flush, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {deselectItems, selectAll, selectFolder} from './actions.js';
import {highlightUpdatedItems, trackUpdatedItems} from './api_listener.js';
import {BookmarkManagerApiProxyImpl} from './bookmark_manager_api_proxy.js';
import type {BrowserProxy} from './browser_proxy.js';
import {BrowserProxyImpl} from './browser_proxy.js';
import {getTemplate} from './command_manager.html.js';
import {Command, IncognitoAvailability, MenuSource, OPEN_CONFIRMATION_LIMIT, ROOT_NODE_ID} from './constants.js';
import {DialogFocusManager} from './dialog_focus_manager.js';
import type {BookmarksEditDialogElement} from './edit_dialog.js';
import {StoreClientMixin} from './store_client_mixin.js';
import type {BookmarkNode, OpenCommandMenuDetail} from './types.js';
import {canEditNode, canReorderChildren, getDisplayedList} from './util.js';
const BookmarksCommandManagerElementBase = StoreClientMixin(PolymerElement);
export interface BookmarksCommandManagerElement {
$: {
dropdown: CrLazyRenderElement<CrActionMenuElement>,
editDialog: CrLazyRenderElement<BookmarksEditDialogElement>,
openDialog: CrLazyRenderElement<CrDialogElement>,
};
}
let instance: BookmarksCommandManagerElement|null = null;
export class BookmarksCommandManagerElement extends
BookmarksCommandManagerElementBase {
static get is() {
return 'bookmarks-command-manager';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
menuCommands_: {
type: Array,
computed: 'computeMenuCommands_(menuSource_)',
},
menuIds_: Object,
menuSource_: Number,
canPaste_: Boolean,
globalCanEdit_: Boolean,
};
}
/**
* Indicates where the context menu was opened from. Will be NONE if
* menu is not open, indicating that commands are from keyboard shortcuts
* or elsewhere in the UI.
*/
private menuSource_: MenuSource = MenuSource.NONE;
private confirmOpenCallback_: (() => void)|null = null;
private canPaste_: boolean;
private globalCanEdit_: boolean;
private menuIds_: Set<string>;
private menuCommands_: Command[];
private browserProxy_: BrowserProxy;
private shortcuts_: Map<Command, KeyboardShortcutList>;
private eventTracker_: EventTracker = new EventTracker();
override connectedCallback() {
super.connectedCallback();
assert(instance === null);
instance = this;
this.browserProxy_ = BrowserProxyImpl.getInstance();
this.watch('globalCanEdit_', state => state.prefs.canEdit);
this.updateFromStore();
this.shortcuts_ = new Map();
this.addShortcut_(Command.EDIT, 'F2', 'Enter');
this.addShortcut_(Command.DELETE, 'Delete', 'Delete Backspace');
this.addShortcut_(Command.OPEN, 'Enter', 'Meta|o');
this.addShortcut_(Command.OPEN_NEW_TAB, 'Ctrl|Enter', 'Meta|Enter');
this.addShortcut_(Command.OPEN_NEW_WINDOW, 'Shift|Enter');
// Note: the undo shortcut is also defined in bookmarks_ui.cc
// TODO(b/893033): de-duplicate shortcut by moving all shortcut
// definitions from JS to C++.
this.addShortcut_(Command.UNDO, 'Ctrl|z', 'Meta|z');
this.addShortcut_(Command.REDO, 'Ctrl|y Ctrl|Shift|Z', 'Meta|Shift|Z');
this.addShortcut_(Command.SELECT_ALL, 'Ctrl|a', 'Meta|a');
this.addShortcut_(Command.DESELECT_ALL, 'Escape');
this.addShortcut_(Command.CUT, 'Ctrl|x', 'Meta|x');
this.addShortcut_(Command.COPY, 'Ctrl|c', 'Meta|c');
this.addShortcut_(Command.PASTE, 'Ctrl|v', 'Meta|v');
this.eventTracker_.add(document, 'open-command-menu', (e: Event) =>
this.onOpenCommandMenu_(
e as CustomEvent<OpenCommandMenuDetail>));
this.eventTracker_.add(document, 'keydown', (e: Event) =>
this.onKeydown_(e as KeyboardEvent));
const addDocumentListenerForCommand = (eventName: string,
command: Command) => {
this.eventTracker_.add(document, eventName, (e: Event) => {
if ((e.composedPath()[0] as HTMLElement).tagName === 'INPUT') {
return;
}
const items = this.getState().selection.items;
if (this.canExecute(command, items)) {
this.handle(command, items);
}
});
};
addDocumentListenerForCommand('command-undo', Command.UNDO);
addDocumentListenerForCommand('cut', Command.CUT);
addDocumentListenerForCommand('copy', Command.COPY);
addDocumentListenerForCommand('paste', Command.PASTE);
}
override disconnectedCallback() {
super.disconnectedCallback();
instance = null;
this.eventTracker_.removeAll();
}
getMenuIdsForTesting(): Set<string> {
return this.menuIds_;
}
getMenuSourceForTesting(): MenuSource {
return this.menuSource_;
}
/**
* Display the command context menu at (|x|, |y|) in window coordinates.
* Commands will execute on |items| if given, or on the currently selected
* items.
*/
openCommandMenuAtPosition(
x: number, y: number, source: MenuSource, items?: Set<string>) {
this.menuSource_ = source;
this.menuIds_ = items || this.getState().selection.items;
const dropdown = this.$.dropdown.get();
// Ensure that the menu is fully rendered before trying to position it.
flush();
DialogFocusManager.getInstance().showDialog(
dropdown.getDialog(), function() {
dropdown.showAtPosition({top: y, left: x});
});
}
/**
* Display the command context menu positioned to cover the |target|
* element. Commands will execute on the currently selected items.
*/
openCommandMenuAtElement(target: HTMLElement, source: MenuSource) {
this.menuSource_ = source;
this.menuIds_ = this.getState().selection.items;
const dropdown = this.$.dropdown.get();
// Ensure that the menu is fully rendered before trying to position it.
flush();
DialogFocusManager.getInstance().showDialog(
dropdown.getDialog(), function() {
dropdown.showAt(target);
});
}
closeCommandMenu() {
this.menuIds_ = new Set();
this.menuSource_ = MenuSource.NONE;
this.$.dropdown.get().close();
}
////////////////////////////////////////////////////////////////////////////
// Command handlers:
/**
* Determine if the |command| can be executed with the given |itemIds|.
* Commands which appear in the context menu should be implemented
* separately using `isCommandVisible_` and `isCommandEnabled_`.
*/
canExecute(command: Command, itemIds: Set<string>): boolean {
const state = this.getState();
switch (command) {
case Command.OPEN:
return itemIds.size > 0;
case Command.UNDO:
case Command.REDO:
return this.globalCanEdit_;
case Command.SELECT_ALL:
case Command.DESELECT_ALL:
return true;
case Command.COPY:
return itemIds.size > 0;
case Command.CUT:
return itemIds.size > 0 &&
!this.containsMatchingNode_(itemIds, function(node) {
return !canEditNode(state, node.id);
});
case Command.PASTE:
return state.search.term === '' &&
canReorderChildren(state, state.selectedFolder);
default:
return this.isCommandVisible_(command, itemIds) &&
this.isCommandEnabled_(command, itemIds);
}
}
private isCommandVisible_(command: Command, itemIds: Set<string>): boolean {
switch (command) {
case Command.EDIT:
return itemIds.size === 1 && this.globalCanEdit_;
case Command.PASTE:
return this.globalCanEdit_;
case Command.CUT:
case Command.COPY:
return itemIds.size >= 1 && this.globalCanEdit_;
case Command.DELETE:
return itemIds.size > 0 && this.globalCanEdit_;
case Command.SHOW_IN_FOLDER:
return this.menuSource_ === MenuSource.ITEM && itemIds.size === 1 &&
this.getState().search.term !== '' &&
!this.containsMatchingNode_(itemIds, function(node) {
return !node.parentId || node.parentId === ROOT_NODE_ID;
});
case Command.OPEN_NEW_TAB:
case Command.OPEN_NEW_WINDOW:
case Command.OPEN_INCOGNITO:
return itemIds.size > 0;
case Command.ADD_BOOKMARK:
case Command.ADD_FOLDER:
case Command.SORT:
case Command.EXPORT:
case Command.IMPORT:
case Command.HELP_CENTER:
return true;
}
assertNotReached();
}
private isCommandEnabled_(command: Command, itemIds: Set<string>): boolean {
const state = this.getState();
switch (command) {
case Command.EDIT:
case Command.DELETE:
return !this.containsMatchingNode_(itemIds, function(node) {
return !canEditNode(state, node.id);
});
case Command.OPEN_NEW_TAB:
case Command.OPEN_NEW_WINDOW:
return this.expandIds_(itemIds).length > 0;
case Command.OPEN_INCOGNITO:
return this.expandIds_(itemIds).length > 0 &&
state.prefs.incognitoAvailability !==
IncognitoAvailability.DISABLED;
case Command.SORT:
return this.canChangeList_() &&
state.nodes[state.selectedFolder]!.children!.length > 1;
case Command.ADD_BOOKMARK:
case Command.ADD_FOLDER:
return this.canChangeList_();
case Command.IMPORT:
return this.globalCanEdit_;
case Command.PASTE:
return this.canPaste_;
default:
return true;
}
}
/**
* Returns whether the currently displayed bookmarks list can be changed.
*/
private canChangeList_(): boolean {
const state = this.getState();
return state.search.term === '' &&
canReorderChildren(state, state.selectedFolder);
}
handle(command: Command, itemIds: Set<string>) {
const state = this.getState();
switch (command) {
case Command.EDIT: {
const id = Array.from(itemIds)[0]!;
this.$.editDialog.get().showEditDialog(state.nodes[id]!);
break;
}
case Command.COPY: {
const idList = Array.from(itemIds);
BookmarkManagerApiProxyImpl.getInstance().copy(idList).then(() => {
let labelPromise: Promise<string>;
if (idList.length === 1) {
labelPromise =
Promise.resolve(loadTimeData.getString('toastItemCopied'));
} else {
labelPromise = PluralStringProxyImpl.getInstance().getPluralString(
'toastItemsCopied', idList.length);
}
this.showTitleToast_(
labelPromise, state.nodes[idList[0]!]!.title, false);
});
break;
}
case Command.SHOW_IN_FOLDER: {
const id = Array.from(itemIds)[0];
const parentId = state.nodes[id!]!.parentId;
assert(parentId);
this.dispatch(selectFolder(parentId, state.nodes));
DialogFocusManager.getInstance().clearFocus();
this.dispatchEvent(new CustomEvent(
'highlight-items', {bubbles: true, composed: true, detail: [id]}));
break;
}
case Command.DELETE: {
const idList = Array.from(this.minimizeDeletionSet_(itemIds));
const title = state.nodes[idList[0]!]!.title;
let labelPromise: Promise<string>;
if (idList.length === 1) {
labelPromise =
Promise.resolve(loadTimeData.getString('toastItemDeleted'));
} else {
labelPromise = PluralStringProxyImpl.getInstance().getPluralString(
'toastItemsDeleted', idList.length);
}
BookmarkManagerApiProxyImpl.getInstance().removeTrees(idList).then(
() => {
this.showTitleToast_(labelPromise, title, true);
});
break;
}
case Command.UNDO:
chrome.bookmarkManagerPrivate.undo();
getToastManager().hide();
break;
case Command.REDO:
chrome.bookmarkManagerPrivate.redo();
break;
case Command.OPEN_NEW_TAB:
case Command.OPEN_NEW_WINDOW:
case Command.OPEN_INCOGNITO:
this.openBookmarkIds_(this.expandIds_(itemIds), command);
break;
case Command.OPEN:
if (this.isFolder_(itemIds)) {
const folderId = Array.from(itemIds)[0]!;
this.dispatch(selectFolder(folderId, state.nodes));
} else {
this.openBookmarkIds_(Array.from(itemIds), command);
}
break;
case Command.SELECT_ALL:
const displayedIds = getDisplayedList(state);
this.dispatch(selectAll(displayedIds, state));
break;
case Command.DESELECT_ALL:
this.dispatch(deselectItems());
getAnnouncerInstance().announce(
loadTimeData.getString('itemsUnselected'));
break;
case Command.CUT:
BookmarkManagerApiProxyImpl.getInstance().cut(Array.from(itemIds));
break;
case Command.PASTE:
const selectedFolder = state.selectedFolder;
const selectedItems = state.selection.items;
trackUpdatedItems();
BookmarkManagerApiProxyImpl.getInstance()
.paste(selectedFolder, Array.from(selectedItems))
.then(highlightUpdatedItems);
break;
case Command.SORT:
chrome.bookmarkManagerPrivate.sortChildren(state.selectedFolder);
getToastManager().show(loadTimeData.getString('toastFolderSorted'));
break;
case Command.ADD_BOOKMARK:
this.$.editDialog.get().showAddDialog(false, state.selectedFolder);
break;
case Command.ADD_FOLDER:
this.$.editDialog.get().showAddDialog(true, state.selectedFolder);
break;
case Command.IMPORT:
chrome.bookmarkManagerPrivate.import();
break;
case Command.EXPORT:
chrome.bookmarkManagerPrivate.export();
break;
case Command.HELP_CENTER:
window.open('https://support.google.com/chrome/?p=bookmarks');
break;
default:
assertNotReached();
}
this.recordCommandHistogram_(
itemIds, 'BookmarkManager.CommandExecuted', command);
}
handleKeyEvent(e: KeyboardEvent, itemIds: Set<string>): boolean {
for (const commandTuple of this.shortcuts_) {
const command = commandTuple[0] as Command;
const shortcut = commandTuple[1] as KeyboardShortcutList;
if (shortcut.matchesEvent(e) && this.canExecute(command, itemIds)) {
this.handle(command, itemIds);
e.stopPropagation();
e.preventDefault();
return true;
}
}
return false;
}
////////////////////////////////////////////////////////////////////////////
// Private functions:
/**
* Register a keyboard shortcut for a command.
*/
private addShortcut_(
command: Command, shortcut: string, macShortcut?: string) {
shortcut = (isMac && macShortcut) ? macShortcut : shortcut;
this.shortcuts_.set(command, new KeyboardShortcutList(shortcut));
}
/**
* Minimize the set of |itemIds| by removing any node which has an ancestor
* node already in the set. This ensures that instead of trying to delete
* both a node and its descendant, we will only try to delete the topmost
* node, preventing an error in the bookmarkManagerPrivate.removeTrees API
* call.
*/
private minimizeDeletionSet_(itemIds: Set<string>): Set<string> {
const minimizedSet = new Set() as Set<string>;
const nodes = this.getState().nodes;
itemIds.forEach(function(itemId) {
let currentId = itemId;
while (currentId !== ROOT_NODE_ID) {
const parentId = nodes[currentId]!.parentId;
assert(parentId);
currentId = parentId;
if (itemIds.has(currentId)) {
return;
}
}
minimizedSet.add(itemId);
});
return minimizedSet;
}
/**
* Open the given |ids| in response to a |command|. May show a confirmation
* dialog before opening large numbers of URLs.
*/
private openBookmarkIds_(ids: string[], command: Command) {
assert(
command === Command.OPEN || command === Command.OPEN_NEW_TAB ||
command === Command.OPEN_NEW_WINDOW ||
command === Command.OPEN_INCOGNITO);
if (ids.length === 0) {
return;
}
const openBookmarkIdsCallback = function() {
const incognito = command === Command.OPEN_INCOGNITO;
if (command === Command.OPEN_NEW_WINDOW || incognito) {
BookmarkManagerApiProxyImpl.getInstance().openInNewWindow(
ids, incognito);
} else {
if (command === Command.OPEN) {
BookmarkManagerApiProxyImpl.getInstance().openInNewTab(
ids.shift()!, /*active=*/ true);
}
ids.forEach(function(id) {
BookmarkManagerApiProxyImpl.getInstance().openInNewTab(
id, /*active=*/ false);
});
}
};
if (ids.length <= OPEN_CONFIRMATION_LIMIT) {
openBookmarkIdsCallback();
return;
}
this.confirmOpenCallback_ = openBookmarkIdsCallback;
const dialog = this.$.openDialog.get();
dialog.querySelector('[slot=body]')!.textContent =
loadTimeData.getStringF('openDialogBody', ids.length);
DialogFocusManager.getInstance().showDialog(this.$.openDialog.get());
}
/**
* Returns all ids in the given set of nodes and their immediate children.
* Note that these will be ordered by insertion order into the |itemIds|
* set, and that it is possible to duplicate a id by passing in both the
* parent ID and child ID.
*/
private expandIds_(itemIds: Set<string>): string[] {
const result: string[] = [];
const nodes = this.getState().nodes;
itemIds.forEach(function(itemId) {
const node = nodes[itemId]!;
if (node.url) {
result.push(node.id);
} else {
node.children!.forEach(function(child) {
const childNode = nodes[child]!;
if (childNode.id && childNode.url) {
result.push(childNode.id);
}
});
}
});
return result;
}
private containsMatchingNode_(
itemIds: Set<string>, predicate: (p1: BookmarkNode) => boolean): boolean {
const nodes = this.getState().nodes;
return Array.from(itemIds).some(function(id) {
return predicate(nodes[id]!);
});
}
private isSingleBookmark_(itemIds: Set<string>): boolean {
return itemIds.size === 1 &&
this.containsMatchingNode_(itemIds, function(node) {
return !!node.url;
});
}
private isFolder_(itemIds: Set<string>): boolean {
return itemIds.size === 1 &&
this.containsMatchingNode_(itemIds, node => !node.url);
}
private getCommandLabel_(command: Command): string {
// Handle non-pluralized strings first.
let label = null;
switch (command) {
case Command.EDIT:
if (this.menuIds_.size !== 1) {
return '';
}
const id = Array.from(this.menuIds_)[0]!;
const itemUrl = this.getState().nodes[id]!.url;
label = itemUrl ? 'menuEdit' : 'menuRename';
break;
case Command.CUT:
label = 'menuCut';
break;
case Command.COPY:
label = 'menuCopy';
break;
case Command.PASTE:
label = 'menuPaste';
break;
case Command.DELETE:
label = 'menuDelete';
break;
case Command.SHOW_IN_FOLDER:
label = 'menuShowInFolder';
break;
case Command.SORT:
label = 'menuSort';
break;
case Command.ADD_BOOKMARK:
label = 'menuAddBookmark';
break;
case Command.ADD_FOLDER:
label = 'menuAddFolder';
break;
case Command.IMPORT:
label = 'menuImport';
break;
case Command.EXPORT:
label = 'menuExport';
break;
case Command.HELP_CENTER:
label = 'menuHelpCenter';
break;
}
if (label !== null) {
return loadTimeData.getString(label);
}
// Handle pluralized strings.
switch (command) {
case Command.OPEN_NEW_TAB:
return this.getPluralizedOpenAllString_(
'menuOpenAllNewTab', 'menuOpenNewTab',
'menuOpenAllNewTabWithCount');
case Command.OPEN_NEW_WINDOW:
return this.getPluralizedOpenAllString_(
'menuOpenAllNewWindow', 'menuOpenNewWindow',
'menuOpenAllNewWindowWithCount');
case Command.OPEN_INCOGNITO:
return this.getPluralizedOpenAllString_(
'menuOpenAllIncognito', 'menuOpenIncognito',
'menuOpenAllIncognitoWithCount');
}
assertNotReached();
}
private getPluralizedOpenAllString_(
case0: string, case1: string, caseOther: string): string {
const multipleNodes = this.menuIds_.size > 1 ||
this.containsMatchingNode_(this.menuIds_, node => !node.url);
const ids = this.expandIds_(this.menuIds_);
if (ids.length === 0) {
return loadTimeData.getStringF(case0, ids.length);
}
if (ids.length === 1 && !multipleNodes) {
return loadTimeData.getString(case1);
}
return loadTimeData.getStringF(caseOther, ids.length);
}
private computeMenuCommands_(): Command[] {
switch (this.menuSource_) {
case MenuSource.ITEM:
case MenuSource.TREE:
return [
Command.EDIT,
Command.SHOW_IN_FOLDER,
Command.DELETE,
// <hr>
Command.CUT,
Command.COPY,
Command.PASTE,
// <hr>
Command.OPEN_NEW_TAB,
Command.OPEN_NEW_WINDOW,
Command.OPEN_INCOGNITO,
];
case MenuSource.TOOLBAR:
return [
Command.SORT,
// <hr>
Command.ADD_BOOKMARK,
Command.ADD_FOLDER,
// <hr>
Command.IMPORT,
Command.EXPORT,
// <hr>
Command.HELP_CENTER,
];
case MenuSource.LIST:
return [
Command.ADD_BOOKMARK,
Command.ADD_FOLDER,
];
case MenuSource.NONE:
return [];
}
assertNotReached();
}
private showDividerAfter_(command: Command, itemIds: Set<string>): boolean {
switch (command) {
case Command.SORT:
case Command.ADD_FOLDER:
case Command.EXPORT:
return this.menuSource_ === MenuSource.TOOLBAR;
case Command.DELETE:
return this.globalCanEdit_;
case Command.PASTE:
return this.globalCanEdit_ || this.isSingleBookmark_(itemIds);
}
return false;
}
private recordCommandHistogram_(
itemIds: Set<string>, histogram: string, command: number) {
if (command === Command.OPEN) {
command =
this.isFolder_(itemIds) ? Command.OPEN_FOLDER : Command.OPEN_BOOKMARK;
}
this.browserProxy_.recordInHistogram(histogram, command, Command.MAX_VALUE);
}
/**
* Show a toast with a bookmark |title| inserted into a label, with the
* title ellipsised if necessary.
*/
private async showTitleToast_(
labelPromise: Promise<string>, title: string,
canUndo: boolean): Promise<void> {
const label = await labelPromise;
const pieces =
loadTimeData.getSubstitutedStringPieces(label, title).map(function(p) {
// Make the bookmark name collapsible.
const result =
p as {value: string, arg: string, collapsible: boolean};
result.collapsible = !!p.arg;
return result;
});
getToastManager().showForStringPieces(pieces, /*hideSlotted*/ !canUndo);
}
private updateCanPaste_(targetId: string): Promise<void> {
return BookmarkManagerApiProxyImpl.getInstance().canPaste(targetId).then(
result => {
this.canPaste_ = result;
});
}
////////////////////////////////////////////////////////////////////////////
// Event handlers:
private async onOpenCommandMenu_(
e: CustomEvent<OpenCommandMenuDetail>): Promise<void> {
if (e.detail.targetId) {
await this.updateCanPaste_(e.detail.targetId);
}
if (e.detail.targetElement) {
this.openCommandMenuAtElement(e.detail.targetElement!, e.detail.source);
} else {
this.openCommandMenuAtPosition(e.detail.x!, e.detail.y!, e.detail.source);
}
}
private onCommandClick_(e: Event) {
assert(this.menuIds_);
this.handle(
Number((e.currentTarget as HTMLElement).getAttribute('command')) as
Command,
this.menuIds_);
this.closeCommandMenu();
}
private onKeydown_(e: KeyboardEvent) {
const path = e.composedPath();
if ((path[0] as HTMLElement).tagName === 'INPUT') {
return;
}
if ((e.target === document.body ||
path.some(
el => (el as HTMLElement).tagName === 'BOOKMARKS-TOOLBAR')) &&
!DialogFocusManager.getInstance().hasOpenDialog()) {
this.handleKeyEvent(e, this.getState().selection.items);
}
}
/**
* Close the menu on mousedown so clicks can propagate to the underlying UI.
* This allows the user to right click the list while a context menu is
* showing and get another context menu.
*/
private onMenuMousedown_(e: Event): void {
if ((e.composedPath()[0] as HTMLElement).tagName !== 'DIALOG') {
return;
}
this.closeCommandMenu();
}
private onOpenCancelClick_() {
this.$.openDialog.get().cancel();
}
private onOpenConfirmClick_() {
assert(this.confirmOpenCallback_);
this.confirmOpenCallback_();
this.$.openDialog.get().close();
}
static getInstance(): BookmarksCommandManagerElement {
assert(instance);
return instance;
}
}
declare global {
interface HTMLElementTagNameMap {
'bookmarks-command-manager': BookmarksCommandManagerElement;
}
}
customElements.define(
BookmarksCommandManagerElement.is, BookmarksCommandManagerElement);