blob: 51469fa096d6613f4341bacd7b7245d85d2044c7 [file] [log] [blame]
// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../strings.m.js';
import './commerce/shopping_list.js';
import './icons.html.js';
import './power_bookmarks_context_menu.js';
import './power_bookmark_row.js';
import './power_bookmarks_context_menu.js';
import './power_bookmarks_edit_dialog.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_empty_state.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_filter_chip.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_footer.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_heading.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_icons.html.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_list_item_badge.js';
import '//bookmarks-side-panel.top-chrome/shared/sp_shared_style.css.js';
import '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import '//resources/cr_elements/cr_button/cr_button.js';
import '//resources/cr_elements/cr_dialog/cr_dialog.js';
import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '//resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import '//resources/cr_elements/cr_toast/cr_toast.js';
import '//resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
import '//resources/cr_elements/cr_toolbar/cr_toolbar_selection_overlay.js';
import '//resources/cr_elements/icons.html.js';
import '//resources/polymer/v3_0/iron-list/iron-list.js';
import {ShoppingListApiProxy, ShoppingListApiProxyImpl} from '//bookmarks-side-panel.top-chrome/shared/commerce/shopping_list_api_proxy.js';
import {BookmarkProductInfo} from '//bookmarks-side-panel.top-chrome/shared/shopping_list.mojom-webui.js';
import {SpEmptyStateElement} from '//bookmarks-side-panel.top-chrome/shared/sp_empty_state.js';
import {startColorChangeUpdater} from '//resources/cr_components/color_change_listener/colors_css_updater.js';
import {getInstance as getAnnouncerInstance} from '//resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import {CrDialogElement} from '//resources/cr_elements/cr_dialog/cr_dialog.js';
import {CrLazyRenderElement} from '//resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {CrToastElement} from '//resources/cr_elements/cr_toast/cr_toast.js';
import {CrToolbarSearchFieldElement} from '//resources/cr_elements/cr_toolbar/cr_toolbar_search_field.js';
import {FocusOutlineManager} from '//resources/js/focus_outline_manager.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import {PluralStringProxyImpl} from '//resources/js/plural_string_proxy.js';
import {listenOnce} from '//resources/js/util_ts.js';
import {IronListElement} from '//resources/polymer/v3_0/iron-list/iron-list.js';
import {afterNextRender, DomRepeatEvent, PolymerElement} from '//resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {ActionSource, SortOrder, ViewType} from './bookmarks.mojom-webui.js';
import {BookmarksApiProxy, BookmarksApiProxyImpl} from './bookmarks_api_proxy.js';
import {PowerBookmarksContextMenuElement} from './power_bookmarks_context_menu.js';
import {PowerBookmarksDragManager} from './power_bookmarks_drag_manager.js';
import {PowerBookmarksEditDialogElement} from './power_bookmarks_edit_dialog.js';
import {getTemplate} from './power_bookmarks_list.html.js';
import {editingDisabledByPolicy, Label, PowerBookmarksService} from './power_bookmarks_service.js';
const ADD_FOLDER_ACTION_UMA = 'Bookmarks.FolderAddedFromSidePanel';
const ADD_URL_ACTION_UMA = 'Bookmarks.AddedFromSidePanel';
function getBookmarkName(bookmark: chrome.bookmarks.BookmarkTreeNode): string {
return bookmark.title || bookmark.url || '';
}
// These values are persisted to logs. Entries should not be renumbered and
// numeric values should never be reused. This must be kept in sync with
// BookmarksSidePanelSearchCTREvent in tools/metrics/histograms/enums.xml.
export enum SearchAction {
SHOWN = 0,
SEARCHED = 1,
// Must be last.
COUNT = 2,
}
export interface SortOption {
sortOrder: SortOrder;
label: string;
lowerLabel: string;
}
export interface PowerBookmarksListElement {
$: {
bookmarks: HTMLElement,
contextMenu: PowerBookmarksContextMenuElement,
deletionToast: CrLazyRenderElement<CrToastElement>,
powerBookmarksContainer: HTMLElement,
searchField: CrToolbarSearchFieldElement,
shownBookmarksIronList: IronListElement,
sortMenu: CrActionMenuElement,
editDialog: PowerBookmarksEditDialogElement,
disabledFeatureDialog: CrDialogElement,
topLevelEmptyState: SpEmptyStateElement,
folderEmptyState: SpEmptyStateElement,
};
}
export class PowerBookmarksListElement extends PolymerElement {
static get is() {
return 'power-bookmarks-list';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
shownBookmarks_: {
type: Array,
value: () => [],
},
compact_: {
type: Boolean,
value: () => loadTimeData.getInteger('viewType') === 0,
},
activeFolderPath_: {
type: Array,
value: () => [],
},
labels_: {
type: Array,
value: () => [],
computed: 'computePriceTrackingLabel_(trackedProductInfos_.*)',
},
activeSortIndex_: {
type: Number,
value: () => loadTimeData.getInteger('sortOrder'),
},
sortTypes_: {
type: Array,
value: () =>
[{
sortOrder: SortOrder.kNewest,
label: loadTimeData.getString('sortNewest'),
lowerLabel: loadTimeData.getString('sortNewestLower'),
},
{
sortOrder: SortOrder.kOldest,
label: loadTimeData.getString('sortOldest'),
lowerLabel: loadTimeData.getString('sortOldestLower'),
},
{
sortOrder: SortOrder.kLastOpened,
label: loadTimeData.getString('sortLastOpened'),
lowerLabel: loadTimeData.getString('sortLastOpenedLower'),
},
{
sortOrder: SortOrder.kAlphabetical,
label: loadTimeData.getString('sortAlphabetically'),
lowerLabel: loadTimeData.getString('sortAlphabetically'),
},
{
sortOrder: SortOrder.kReverseAlphabetical,
label: loadTimeData.getString('sortReverseAlphabetically'),
lowerLabel: loadTimeData.getString('sortReverseAlphabetically'),
}],
},
editing_: {
type: Boolean,
value: false,
},
selectedBookmarks_: {
type: Array,
value: () => [],
},
guestMode_: {
type: Boolean,
value: loadTimeData.getBoolean('guestMode'),
reflectToAttribute: true,
},
renamingId_: {
type: String,
value: '',
},
deletionDescription_: {
type: String,
value: '',
},
/* If container containing shown bookmarks has scrollbars. */
hasScrollbars_: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
};
}
static get observers() {
return [
'updateShownBookmarks_(activeFolderPath_.*, labels_.*, ' +
'activeSortIndex_, searchQuery_)',
];
}
private bookmarksApi_: BookmarksApiProxy =
BookmarksApiProxyImpl.getInstance();
private shoppingListApi_: ShoppingListApiProxy =
ShoppingListApiProxyImpl.getInstance();
private shoppingListenerIds_: number[] = [];
private shownBookmarks_: chrome.bookmarks.BookmarkTreeNode[];
private trackedProductInfos_ = new Map<string, BookmarkProductInfo>();
private availableProductInfos_ = new Map<string, BookmarkProductInfo>();
private bookmarksService_: PowerBookmarksService =
new PowerBookmarksService(this);
private bookmarksDragManager_: PowerBookmarksDragManager =
new PowerBookmarksDragManager(this);
private focusOutlineManager_: FocusOutlineManager;
private compact_: boolean;
private activeFolderPath_: chrome.bookmarks.BookmarkTreeNode[];
private labels_: Label[];
private compactDescriptions_ = new Map<string, string>();
private expandedDescriptions_ = new Map<string, string>();
private imageUrls_ = new Map<string, string>();
private activeSortIndex_: number;
private sortTypes_: SortOption[];
private searchQuery_: string|undefined;
private currentUrl_: string|undefined;
private editing_: boolean;
private selectedBookmarks_: chrome.bookmarks.BookmarkTreeNode[];
private guestMode_: boolean;
private renamingId_: string;
private deletionDescription_: string;
private shownBookmarksResizeObserver_?: ResizeObserver;
private hasScrollbars_: boolean;
private contextMenuBookmark_: chrome.bookmarks.BookmarkTreeNode|undefined;
constructor() {
super();
startColorChangeUpdater();
}
override connectedCallback() {
super.connectedCallback();
this.setAttribute('role', 'application');
listenOnce(this.$.powerBookmarksContainer, 'dom-change', () => {
setTimeout(() => this.bookmarksApi_.showUi(), 0);
});
this.focusOutlineManager_ = FocusOutlineManager.forDocument(document);
this.bookmarksService_.startListening();
this.shoppingListApi_.getAllPriceTrackedBookmarkProductInfo().then(res => {
res.productInfos.forEach(
product => this.set(
`trackedProductInfos_.${product.bookmarkId.toString()}`,
product));
});
this.shoppingListApi_.getAllShoppingBookmarkProductInfo().then(res => {
res.productInfos.forEach(
product => this.setAvailableProductInfo_(product));
});
const callbackRouter = this.shoppingListApi_.getCallbackRouter();
this.shoppingListenerIds_.push(
callbackRouter.priceTrackedForBookmark.addListener(
(product: BookmarkProductInfo) =>
this.onBookmarkPriceTracked_(product)),
callbackRouter.priceUntrackedForBookmark.addListener(
(bookmarkId: bigint) =>
this.onBookmarkPriceUntracked_(bookmarkId.toString())),
);
if (document.documentElement.hasAttribute('chrome-refresh-2023')) {
this.shownBookmarksResizeObserver_ =
new ResizeObserver(this.onShownBookmarksResize_.bind(this));
this.shownBookmarksResizeObserver_.observe(this.$.bookmarks);
}
this.bookmarksDragManager_.startObserving();
this.recordMetricsOnConnected_();
}
override disconnectedCallback() {
this.bookmarksService_.stopListening();
this.shoppingListenerIds_.forEach(
id => this.shoppingListApi_.getCallbackRouter().removeListener(id));
if (this.shownBookmarksResizeObserver_) {
this.shownBookmarksResizeObserver_.disconnect();
this.shownBookmarksResizeObserver_ = undefined;
}
this.bookmarksDragManager_.stopObserving();
}
setCurrentUrl(url: string) {
this.currentUrl_ = url;
}
setCompactDescription(
bookmark: chrome.bookmarks.BookmarkTreeNode, description: string) {
this.set(`compactDescriptions_.${bookmark.id}`, description);
}
setExpandedDescription(
bookmark: chrome.bookmarks.BookmarkTreeNode, description: string) {
this.set(`expandedDescriptions_.${bookmark.id}`, description);
}
setImageUrl(bookmark: chrome.bookmarks.BookmarkTreeNode, url: string) {
this.set(`imageUrls_.${bookmark.id.toString()}`, url);
}
onBookmarksLoaded() {
this.updateShownBookmarks_();
}
onBookmarkChanged(id: string, changedInfo: chrome.bookmarks.ChangeInfo) {
const visibleIndex = this.visibleIndex_(id);
if (visibleIndex > -1) {
Object.keys(changedInfo).forEach(key => {
this.notifyPath(`shownBookmarks_.${visibleIndex}.${key}`);
});
}
this.updateShoppingData_();
}
onBookmarkCreated(
bookmark: chrome.bookmarks.BookmarkTreeNode,
parent: chrome.bookmarks.BookmarkTreeNode) {
const bookmarksToShow = this.getBookmarksToShow_(bookmark, parent);
if (bookmarksToShow.length > 0) {
this.shownBookmarks_.unshift(...bookmarksToShow);
this.bookmarksService_.sortBookmarks(
this.shownBookmarks_, this.activeSortIndex_);
this.shownBookmarks_ = this.shownBookmarks_.slice();
const bookmarkIndex = this.shownBookmarks_.indexOf(bookmarksToShow[0]);
this.$.shownBookmarksIronList.scrollToIndex(bookmarkIndex);
if (bookmark.url) {
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkCreated', getBookmarkName(bookmark)));
} else {
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkFolderCreated', getBookmarkName(bookmark)));
}
}
this.updateShoppingData_();
}
onBookmarkMoved(
bookmark: chrome.bookmarks.BookmarkTreeNode,
oldParent: chrome.bookmarks.BookmarkTreeNode,
newParent: chrome.bookmarks.BookmarkTreeNode) {
const bookmarksToShow = this.getBookmarksToShow_(bookmark, newParent);
const shouldUpdateUIAdded = bookmarksToShow.length > 0;
const shouldUpdateUIRemoved = this.visibleParent_(oldParent);
const shouldUpdateUIReordered =
shouldUpdateUIAdded && shouldUpdateUIRemoved;
if (shouldUpdateUIReordered) {
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkReordered', getBookmarkName(bookmark)));
} else if (shouldUpdateUIAdded) {
const scrollIndex = this.$.shownBookmarksIronList.firstVisibleIndex;
this.shownBookmarks_.unshift(...bookmarksToShow);
this.bookmarksService_.sortBookmarks(
this.shownBookmarks_, this.activeSortIndex_);
this.shownBookmarks_ = this.shownBookmarks_.slice();
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkMoved', getBookmarkName(bookmark),
getBookmarkName(newParent)));
this.$.shownBookmarksIronList.scrollToIndex(scrollIndex);
} else if (shouldUpdateUIRemoved) {
const scrollIndex = this.$.shownBookmarksIronList.firstVisibleIndex;
this.splice('shownBookmarks_', this.visibleIndex_(bookmark.id), 1);
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkMoved', getBookmarkName(bookmark),
getBookmarkName(newParent)));
// If the new parent folder is visible, notify to ensure its displayed
// child count is updated.
const visibleIndex = this.visibleIndex_(newParent.id);
if (visibleIndex > -1) {
this.notifyPath(`shownBookmarks_.${visibleIndex}.children`);
}
this.$.shownBookmarksIronList.scrollToIndex(scrollIndex);
}
}
onBookmarkRemoved(bookmark: chrome.bookmarks.BookmarkTreeNode) {
const scrollIndex = this.$.shownBookmarksIronList.firstVisibleIndex;
const visibleIndex = this.visibleIndex_(bookmark.id);
if (visibleIndex > -1) {
this.splice('shownBookmarks_', visibleIndex, 1);
getAnnouncerInstance().announce(loadTimeData.getStringF(
'bookmarkDeleted', getBookmarkName(bookmark)));
}
this.set(`trackedProductInfos_.${bookmark.id}`, null);
this.availableProductInfos_.delete(bookmark.id);
if (visibleIndex > -1) {
this.$.shownBookmarksIronList.scrollToIndex(scrollIndex);
}
}
isPriceTracked(bookmark: chrome.bookmarks.BookmarkTreeNode): boolean {
return !!this.get(`trackedProductInfos_.${bookmark.id}`);
}
getProductImageUrl(bookmark: chrome.bookmarks.BookmarkTreeNode): string {
const bookmarkProductInfo = this.availableProductInfos_.get(bookmark.id);
if (bookmarkProductInfo) {
return bookmarkProductInfo.info.imageUrl.url;
} else {
return '';
}
}
/** PowerBookmarksDragDelegate */
onFinishDrop(dropTarget: chrome.bookmarks.BookmarkTreeNode): void {
this.focusBookmark_(dropTarget.id);
// Show the focus state immediately after dropping a bookmark to indicate
// where the bookmark was moved to, and remove the state immediately after
// the next mouse event.
this.focusOutlineManager_.visible = true;
document.addEventListener('mousedown', () => {
this.focusOutlineManager_.visible = false;
}, {once: true});
}
private canDrag_() {
return !this.editing_ && !this.renamingId_ && !this.searchQuery_ &&
!this.hasActiveLabels_();
}
private focusBookmark_(id: string) {
const bookmarkElement =
this.shadowRoot!.querySelector(`#bookmark-${id}`) as HTMLElement;
if (bookmarkElement) {
bookmarkElement.focus();
}
}
private isPriceTrackingEligible_(bookmark: chrome.bookmarks.BookmarkTreeNode):
boolean {
return !!this.availableProductInfos_.get(bookmark.id);
}
private onBookmarkPriceTracked_(product: BookmarkProductInfo) {
this.set(`trackedProductInfos_.${product.bookmarkId.toString()}`, product);
}
private onBookmarkPriceUntracked_(bookmarkId: string) {
this.set(`trackedProductInfos_.${bookmarkId}`, null);
}
// TODO(emshack): Once there is more than one bookmark power, remove this
// logic and always display the price tracking label button.
private computePriceTrackingLabel_() {
const showLabel =
Object.keys(this.trackedProductInfos_)
.some(key => this.get(`trackedProductInfos_.${key}`) !== null);
if (showLabel) {
// Reuse the current price tracking label if one exists, to maintain its
// active state.
const currentLabel = this.get('labels_.0');
return [currentLabel ? currentLabel : {
label: loadTimeData.getString('priceTrackingLabel'),
icon: 'bookmarks:price-tracking',
active: false,
}];
} else {
return [];
}
}
/**
* Returns the index of the given node id in the currently shown bookmarks,
* or -1 if not shown.
*/
private visibleIndex_(nodeId: string): number {
return this.shownBookmarks_.findIndex(b => b.id === nodeId);
}
/**
* Returns true if the given node is either the current active folder or a
* root folder that isn't shown itself while the all bookmarks list is shown.
*/
private visibleParent_(parent: chrome.bookmarks.BookmarkTreeNode): boolean {
const activeFolder = this.getActiveFolder_();
return (!activeFolder && parent.parentId === '0' &&
this.visibleIndex_(parent.id) === -1) ||
parent === activeFolder;
}
private getBookmarksToShow_(
bookmark: chrome.bookmarks.BookmarkTreeNode,
parent: chrome.bookmarks.BookmarkTreeNode):
chrome.bookmarks.BookmarkTreeNode[] {
if (!this.visibleParent_(parent)) {
return [];
}
return this.bookmarksService_.applySearchQueryAndLabels(
this.labels_, this.searchQuery_, [bookmark]);
}
private getActiveFolder_(): chrome.bookmarks.BookmarkTreeNode|undefined {
if (this.activeFolderPath_.length) {
return this.activeFolderPath_[this.activeFolderPath_.length - 1];
}
return undefined;
}
private getBackButtonLabel_(): string {
const activeFolder = this.getActiveFolder_();
const parentFolder = this.bookmarksService_.findBookmarkWithId(
activeFolder ? activeFolder.parentId : undefined);
return loadTimeData.getStringF(
'backButtonLabel', this.getFolderLabel_(parentFolder));
}
private getBookmarksListRole_(): string {
return this.editing_ ? 'listbox' : 'list';
}
private getBookmarkDescription_(bookmark: chrome.bookmarks.BookmarkTreeNode):
string|undefined {
if (this.compact_) {
return this.get(`compactDescriptions_.${bookmark.id}`);
} else {
const url = this.get(`expandedDescriptions_.${bookmark.id}`);
if (this.searchQuery_ && url && bookmark.parentId) {
const parentFolder =
this.bookmarksService_.findBookmarkWithId(bookmark.parentId);
const folderLabel = this.getFolderLabel_(parentFolder);
return loadTimeData.getStringF(
'urlFolderDescription', url, folderLabel);
} else {
return url;
}
}
}
private getBookmarkMenuA11yLabel_(url: string, title: string): string {
if (url) {
return loadTimeData.getStringF('bookmarkMenuLabel', title);
} else {
return loadTimeData.getStringF('folderMenuLabel', title);
}
}
private getBookmarkA11yLabel_(url: string, title: string): string {
if (this.editing_) {
if (url) {
return loadTimeData.getStringF('selectBookmarkLabel', title);
}
return loadTimeData.getStringF('selectFolderLabel', title);
}
if (url) {
return loadTimeData.getStringF('openBookmarkLabel', title);
}
return loadTimeData.getStringF('openFolderLabel', title);
}
private getBookmarkA11yDescription_(
bookmark: chrome.bookmarks.BookmarkTreeNode): string {
let description = '';
if (this.isPriceTracked(bookmark)) {
description += loadTimeData.getStringF(
'a11yDescriptionPriceTracking', this.getCurrentPrice_(bookmark));
const previousPrice = this.getPreviousPrice_(bookmark);
if (previousPrice) {
description += loadTimeData.getStringF(
'a11yDescriptionPriceChange', previousPrice);
}
}
return description;
}
private getBookmarkImageUrls_(bookmark: chrome.bookmarks.BookmarkTreeNode):
string[] {
const imageUrls: string[] = [];
if (bookmark.url) {
const imageUrl = this.get(`imageUrls_.${bookmark.id.toString()}`);
if (imageUrl) {
imageUrls.push(imageUrl);
}
} else if (this.canEdit_(bookmark) && bookmark.children) {
bookmark.children.forEach((child) => {
const childImageUrl: string =
this.get(`imageUrls_.${child.id.toString()}`);
if (childImageUrl) {
imageUrls.push(childImageUrl);
}
});
}
return imageUrls;
}
private getBookmarkForceHover_(bookmark: chrome.bookmarks.BookmarkTreeNode):
boolean {
return bookmark === this.contextMenuBookmark_;
}
private getActiveFolderLabel_(): string {
return this.getFolderLabel_(this.getActiveFolder_());
}
private getFolderLabel_(folder: chrome.bookmarks.BookmarkTreeNode|
undefined): string {
if (folder && folder.id !== loadTimeData.getString('otherBookmarksId') &&
folder.id !== loadTimeData.getString('mobileBookmarksId')) {
return folder!.title;
} else {
return loadTimeData.getString('allBookmarks');
}
}
private getSortLabel_(): string {
return this.sortTypes_[this.activeSortIndex_]!.label;
}
private renamingItem_(id: string) {
return id === this.renamingId_;
}
private updateShoppingData_() {
this.availableProductInfos_.clear();
this.shoppingListApi_.getAllShoppingBookmarkProductInfo().then(res => {
res.productInfos.forEach(
product => this.setAvailableProductInfo_(product));
});
}
private setAvailableProductInfo_(productInfo: BookmarkProductInfo) {
const bookmarkId = productInfo.bookmarkId.toString();
this.availableProductInfos_.set(bookmarkId, productInfo);
if (productInfo.info.imageUrl.url !== '') {
const bookmark = this.bookmarksService_.findBookmarkWithId(bookmarkId)!;
this.setImageUrl(bookmark, productInfo.info.imageUrl.url);
}
}
/**
* Update the list of bookmarks and folders displayed to the user.
*/
private updateShownBookmarks_() {
this.shownBookmarks_ = this.bookmarksService_.filterBookmarks(
this.getActiveFolder_(), this.activeSortIndex_, this.searchQuery_,
this.labels_);
this.bookmarksService_.refreshDataForBookmarks(this.shownBookmarks_);
}
private recordMetricsOnConnected_() {
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.SortTypeShown',
this.sortTypes_[this.activeSortIndex_].sortOrder, SortOrder.kCount);
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.ViewTypeShown',
this.compact_ ? ViewType.kCompact : ViewType.kExpanded,
ViewType.kCount);
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.Search.CTR', SearchAction.SHOWN,
SearchAction.COUNT);
}
private canAddCurrentUrl_(): boolean {
return this.bookmarksService_.canAddUrl(
this.currentUrl_, this.getActiveFolder_());
}
private canEdit_(bookmark: chrome.bookmarks.BookmarkTreeNode): boolean {
return bookmark.id !== loadTimeData.getString('bookmarksBarId') &&
bookmark.id !== loadTimeData.getString('managedBookmarksFolderId');
}
private getSortMenuItemLabel_(sortType: SortOption): string {
return loadTimeData.getStringF('sortByType', sortType.label);
}
private getSortMenuItemLowerLabel_(sortType: SortOption): string {
return loadTimeData.getStringF('sortByType', sortType.lowerLabel);
}
private sortMenuItemIsSelected_(sortType: SortOption): boolean {
return this.sortTypes_[this.activeSortIndex_].sortOrder ===
sortType.sortOrder;
}
/**
* Invoked when the user clicks a power bookmarks row. This will either
* display children in the case of a folder row, or open the URL in the case
* of a bookmark row.
*/
private onRowClicked_(
event: CustomEvent<
{bookmark: chrome.bookmarks.BookmarkTreeNode, event: MouseEvent}>) {
event.preventDefault();
event.stopPropagation();
if (!this.editing_) {
if (event.detail.bookmark.children) {
this.push('activeFolderPath_', event.detail.bookmark);
// Cancel search when changing active folder.
this.$.searchField.setValue('');
afterNextRender(this, () => {
this.$.shownBookmarksIronList.focusItem(0);
});
} else {
this.bookmarksApi_.openBookmark(
event.detail.bookmark.id, this.activeFolderPath_.length, {
middleButton: false,
altKey: event.detail.event.altKey,
ctrlKey: event.detail.event.ctrlKey,
metaKey: event.detail.event.metaKey,
shiftKey: event.detail.event.shiftKey,
},
ActionSource.kBookmark);
}
}
}
private onRowSelectedChange_(
event: CustomEvent<
{bookmark: chrome.bookmarks.BookmarkTreeNode, checked: boolean}>) {
event.preventDefault();
event.stopPropagation();
if (event.detail.checked) {
this.unshift('selectedBookmarks_', event.detail.bookmark);
} else {
this.splice(
'selectedBookmarks_',
this.selectedBookmarks_.findIndex(b => b === event.detail.bookmark),
1);
}
}
private async onBookmarksEdited_(event: CustomEvent<{
bookmarks: chrome.bookmarks.BookmarkTreeNode[],
name: string|undefined,
url: string|undefined,
folderId: string,
newFolders: chrome.bookmarks.BookmarkTreeNode[],
}>) {
event.preventDefault();
event.stopPropagation();
let parentId = event.detail.folderId;
for (const folder of event.detail.newFolders) {
chrome.metricsPrivate.recordUserAction(ADD_FOLDER_ACTION_UMA);
const newFolder =
await this.bookmarksApi_.createFolder(folder.parentId!, folder.title);
folder.children!.forEach(child => child.parentId = newFolder.id);
if (folder.id === parentId) {
parentId = newFolder.id;
}
}
this.bookmarksApi_.editBookmarks(
event.detail.bookmarks.map(bookmark => bookmark.id), event.detail.name,
event.detail.url, parentId);
this.selectedBookmarks_ = [];
this.editing_ = false;
}
private setRenamingId_(event: CustomEvent<{id: string}>) {
this.renamingId_ = event.detail.id;
}
private onRename_(
event: CustomEvent<
{bookmark: chrome.bookmarks.BookmarkTreeNode, value: string}>) {
this.bookmarksApi_.renameBookmark(
event.detail.bookmark.id, event.detail.value);
this.renamingId_ = '';
}
private hasActiveLabels_(): boolean {
for (const label of this.labels_) {
if (label.active) {
return true;
}
}
return false;
}
private shouldShowEmptySearchState_(): boolean {
return this.hasActiveLabels_() || !!this.searchQuery_;
}
private shouldShowTopLevelEmptyState_(): boolean {
return this.guestMode_ ||
(this.shownBookmarks_.length === 0 &&
(!!this.searchQuery_ || this.activeFolderPath_.length === 0));
}
private shouldHideCard_(): boolean {
return this.guestMode_ ||
(this.shouldHideHeader_() && this.shownBookmarks_.length === 0);
}
private shouldHideHeader_(): boolean {
return this.hasActiveLabels_() || !!this.searchQuery_;
}
private getSelectedDescription_() {
return loadTimeData.getStringF(
'selectedBookmarkCount', this.selectedBookmarks_.length);
}
/**
* Returns the appropriate filter button icon depending on whether the given
* label is active.
*/
private getLabelIcon_(label: Label): string {
if (label.active) {
return 'bookmarks:check';
} else {
return label.icon;
}
}
/**
* Toggles the given label between active and inactive.
*/
private onLabelClicked_(event: DomRepeatEvent<Label>) {
event.preventDefault();
event.stopPropagation();
const label = event.model.item;
this.set(`labels_.${event.model.index}.active`, !label.active);
}
/**
* Moves the displayed folders up one level when the back button is clicked.
*/
private onBackClicked_() {
this.pop('activeFolderPath_');
}
private onSearchChanged_(e: CustomEvent<string>) {
this.searchQuery_ = e.detail.toLocaleLowerCase();
}
private onSearchBlurred_() {
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.Search.CTR', SearchAction.SEARCHED,
SearchAction.COUNT);
}
private onShowContextMenuClicked_(
event: CustomEvent<
{bookmark: chrome.bookmarks.BookmarkTreeNode, event: MouseEvent}>) {
event.preventDefault();
event.stopPropagation();
const priceTracked = this.isPriceTracked(event.detail.bookmark);
const priceTrackingEligible =
this.isPriceTrackingEligible_(event.detail.bookmark);
this.contextMenuBookmark_ = event.detail.bookmark;
if (event.detail.event.button === 0) {
this.$.contextMenu.showAt(
event.detail.event, [this.contextMenuBookmark_], priceTracked,
priceTrackingEligible);
} else {
this.$.contextMenu.showAtPosition(
event.detail.event, [this.contextMenuBookmark_], priceTracked,
priceTrackingEligible);
}
}
private getParentFolder_(): chrome.bookmarks.BookmarkTreeNode {
return this.getActiveFolder_() ||
this.bookmarksService_.findBookmarkWithId(
loadTimeData.getString('otherBookmarksId'))!;
}
private onShowSortMenuClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.$.sortMenu.showAt(event.target as HTMLElement);
}
private onAddNewFolderClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
const newParent = this.getParentFolder_();
if (editingDisabledByPolicy([newParent])) {
this.showDisabledFeatureDialog_();
return;
}
chrome.metricsPrivate.recordUserAction(ADD_FOLDER_ACTION_UMA);
this.bookmarksApi_
.createFolder(newParent.id, loadTimeData.getString('newFolderTitle'))
.then((newFolder) => {
this.renamingId_ = newFolder.id;
});
}
private onBulkEditClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.editing_ = !this.editing_;
if (!this.editing_) {
this.selectedBookmarks_ = [];
}
}
private onDeleteClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (editingDisabledByPolicy(this.selectedBookmarks_)) {
this.showDisabledFeatureDialog_();
return;
}
this.bookmarksApi_
.deleteBookmarks(this.selectedBookmarks_.map(bookmark => bookmark.id))
.then(() => {
this.showDeletionToastWithCount_(this.selectedBookmarks_.length);
this.selectedBookmarks_ = [];
this.editing_ = false;
});
}
private onContextMenuEditClicked_(
event: CustomEvent<{bookmarks: chrome.bookmarks.BookmarkTreeNode[]}>) {
event.preventDefault();
event.stopPropagation();
if (editingDisabledByPolicy(event.detail.bookmarks)) {
this.showDisabledFeatureDialog_();
return;
}
this.showEditDialog_(
event.detail.bookmarks, event.detail.bookmarks.length > 1);
}
private onContextMenuDeleteClicked_(
event: CustomEvent<{bookmarks: chrome.bookmarks.BookmarkTreeNode[]}>) {
event.preventDefault();
event.stopPropagation();
this.showDeletionToastWithCount_(event.detail.bookmarks.length);
this.selectedBookmarks_ = [];
this.editing_ = false;
}
private onContextMenuClosed_() {
this.contextMenuBookmark_ = undefined;
}
private showDeletionToastWithCount_(deletionCount: number) {
PluralStringProxyImpl.getInstance()
.getPluralString('bookmarkDeletionCount', deletionCount)
.then(pluralString => {
this.deletionDescription_ = pluralString;
this.$.deletionToast.get().show();
});
}
private showDisabledFeatureDialog_() {
this.$.disabledFeatureDialog.showModal();
}
private closeDisabledFeatureDialog_() {
this.$.disabledFeatureDialog.close();
}
private onUndoClicked_() {
this.bookmarksApi_.undo();
this.$.deletionToast.get().hide();
}
private onMoveClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
if (editingDisabledByPolicy(this.selectedBookmarks_)) {
this.showDisabledFeatureDialog_();
return;
}
this.showEditDialog_(this.selectedBookmarks_, true);
}
private showEditDialog_(
bookmarks: chrome.bookmarks.BookmarkTreeNode[], moveOnly: boolean) {
this.$.editDialog.showDialog(
this.activeFolderPath_, this.bookmarksService_.getTopLevelBookmarks(),
bookmarks, moveOnly);
}
private onEditMenuClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.$.contextMenu.showAt(
event, this.selectedBookmarks_.slice(), false, false);
}
private onSortTypeClicked_(event: DomRepeatEvent<SortOption>) {
event.preventDefault();
event.stopPropagation();
this.$.sortMenu.close();
this.activeSortIndex_ = event.model.index;
this.bookmarksApi_.setSortOrder(event.model.item.sortOrder);
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.SortTypeShown', event.model.item.sortOrder,
SortOrder.kCount);
}
private onVisualViewClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.$.sortMenu.close();
this.compact_ = false;
this.$.shownBookmarksIronList.notifyResize();
this.bookmarksApi_.setViewType(ViewType.kExpanded);
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.ViewTypeShown', ViewType.kExpanded,
ViewType.kCount);
}
private onCompactViewClicked_(event: MouseEvent) {
event.preventDefault();
event.stopPropagation();
this.$.sortMenu.close();
this.compact_ = true;
this.$.shownBookmarksIronList.notifyResize();
this.bookmarksApi_.setViewType(ViewType.kCompact);
chrome.metricsPrivate.recordEnumerationValue(
'PowerBookmarks.SidePanel.ViewTypeShown', ViewType.kCompact,
ViewType.kCount);
}
private onAddTabClicked_() {
const newParent = this.getParentFolder_();
if (editingDisabledByPolicy([newParent])) {
this.showDisabledFeatureDialog_();
return;
}
chrome.metricsPrivate.recordUserAction(ADD_URL_ACTION_UMA);
this.bookmarksApi_.bookmarkCurrentTabInFolder(newParent.id);
}
private hideAddTabButton_() {
return this.editing_ || this.guestMode_;
}
private disableBackButton_(): boolean {
return !this.activeFolderPath_.length || this.editing_;
}
private getEmptyTitle_(): string {
if (this.guestMode_) {
return loadTimeData.getString('emptyTitleGuest');
} else if (this.shouldShowEmptySearchState_()) {
return loadTimeData.getString('emptyTitleSearch');
} else {
return loadTimeData.getString('emptyTitle');
}
}
private getEmptyBody_(): string {
if (this.guestMode_) {
return loadTimeData.getString('emptyBodyGuest');
} else if (this.shouldShowEmptySearchState_()) {
return loadTimeData.getString('emptyBodySearch');
} else {
return loadTimeData.getString('emptyBody');
}
}
private getEmptyImagePath_(): string {
return this.shouldShowEmptySearchState_() ? '' :
'./images/bookmarks_empty.svg';
}
private getEmptyImagePathDark_(): string {
return this.shouldShowEmptySearchState_() ?
'' :
'./images/bookmarks_empty_dark.svg';
}
/**
* Whether the given price-tracked bookmark should display as if discounted.
*/
private showDiscountedPrice_(bookmark: chrome.bookmarks.BookmarkTreeNode):
boolean {
const bookmarkProductInfo = this.get(`trackedProductInfos_.${bookmark.id}`);
if (bookmarkProductInfo) {
return bookmarkProductInfo.info.previousPrice.length > 0;
}
return false;
}
private getCurrentPrice_(bookmark: chrome.bookmarks.BookmarkTreeNode):
string {
const bookmarkProductInfo = this.get(`trackedProductInfos_.${bookmark.id}`);
if (bookmarkProductInfo) {
return bookmarkProductInfo.info.currentPrice;
} else {
return '';
}
}
private getPreviousPrice_(bookmark: chrome.bookmarks.BookmarkTreeNode):
string {
const bookmarkProductInfo = this.get(`trackedProductInfos_.${bookmark.id}`);
if (bookmarkProductInfo) {
return bookmarkProductInfo.info.previousPrice;
} else {
return '';
}
}
private onShownBookmarksResize_() {
// The iron-list of `shownBookmarks_` is in a dynamically sized card.
// Any time the size changes, let iron-list know so that iron-list can
// properly adjust to its possibly new height.
this.$.shownBookmarksIronList.notifyResize();
this.hasScrollbars_ =
this.$.bookmarks.scrollHeight > this.$.bookmarks.offsetHeight;
}
}
declare global {
interface HTMLElementTagNameMap {
'power-bookmarks-list': PowerBookmarksListElement;
}
}
customElements.define(PowerBookmarksListElement.is, PowerBookmarksListElement);