// Copyright 2016 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import 'chrome://resources/cr_elements/cr_icons.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import './shared_style.css.js';
import './strings.m.js';
import type {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js';
import {assert} from 'chrome://resources/js/assert.js';
import {focusWithoutInk} from 'chrome://resources/js/focus_without_ink.js';
import {getFaviconForPageURL} from 'chrome://resources/js/icon.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {isMac} from 'chrome://resources/js/platform.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {selectItem} from './actions.js';
import {BookmarksCommandManagerElement} from './command_manager.js';
import {Command, MenuSource} from './constants.js';
import {getTemplate} from './item.html.js';
import {StoreClientMixin} from './store_client_mixin.js';
import type {BookmarkNode} from './types.js';
const BookmarksItemElementBase = StoreClientMixin(PolymerElement);
export interface BookmarksItemElement {
$: {
icon: HTMLDivElement,
menuButton: CrIconButtonElement,
};
}
export class BookmarksItemElement extends BookmarksItemElementBase {
static get is() {
return 'bookmarks-item';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
itemId: {
type: String,
observer: 'onItemIdChanged_',
},
ironListTabIndex: Number,
item_: {
type: Object,
observer: 'onItemChanged_',
},
isSelectedItem_: {
type: Boolean,
reflectToAttribute: true,
},
isMultiSelect_: Boolean,
isFolder_: Boolean,
lastTouchPoints_: Number,
};
}
itemId: string;
private item_: BookmarkNode;
private isSelectedItem_: boolean;
private isMultiSelect_: boolean;
private isFolder_: boolean;
private lastTouchPoints_: number;
static get observers() {
return [
'updateFavicon_(item_.url)',
];
}
override ready() {
super.ready();
this.addEventListener('click', e => this.onClick_(e as MouseEvent));
this.addEventListener('dblclick', e => this.onDblClick_(e as MouseEvent));
this.addEventListener('contextmenu', e => this.onContextMenu_(e));
this.addEventListener('keydown', e => this.onKeydown_(e as KeyboardEvent));
this.addEventListener(
'auxclick', e => this.onMiddleClick_(e as MouseEvent));
this.addEventListener(
'mousedown', e => this.cancelMiddleMouseBehavior_(e as MouseEvent));
this.addEventListener(
'mouseup', e => this.cancelMiddleMouseBehavior_(e as MouseEvent));
this.addEventListener(
'touchstart', e => this.onTouchStart_(e as TouchEvent));
}
override connectedCallback() {
super.connectedCallback();
this.watch('item_', state => state.nodes[this.itemId]);
this.watch(
'isSelectedItem_', state => state.selection.items.has(this.itemId));
this.watch('isMultiSelect_', state => state.selection.items.size > 1);
this.updateFromStore();
}
setIsSelectedItemForTesting(selected: boolean) {
this.isSelectedItem_ = selected;
}
focusMenuButton() {
focusWithoutInk(this.$.menuButton);
}
getDropTarget(): BookmarksItemElement {
return this;
}
private onContextMenu_(e: MouseEvent) {
e.preventDefault();
e.stopPropagation();
// Prevent context menu from appearing after a drag, but allow opening the
// context menu through 2 taps
const capabilities = (e as unknown as {
sourceCapabilities: {firesTouchEvents?: boolean},
}).sourceCapabilities;
if (capabilities && capabilities.firesTouchEvents &&
this.lastTouchPoints_ !== 2) {
return;
}
this.focus();
if (!this.isSelectedItem_) {
this.selectThisItem_();
}
this.dispatchEvent(new CustomEvent('open-command-menu', {
bubbles: true,
composed: true,
detail: {
x: e.clientX,
y: e.clientY,
source: MenuSource.ITEM,
targetId: this.itemId,
},
}));
}
private onMenuButtonClick_(e: Event) {
e.stopPropagation();
e.preventDefault();
// Skip selecting the item if this item is part of a multi-selected group.
if (!this.isMultiSelectMenu_()) {
this.selectThisItem_();
}
this.dispatchEvent(new CustomEvent('open-command-menu', {
bubbles: true,
composed: true,
detail: {
targetElement: e.target,
source: MenuSource.ITEM,
targetId: this.itemId,
},
}));
}
private selectThisItem_() {
this.dispatch(selectItem(this.itemId, this.getState(), {
clear: true,
range: false,
toggle: false,
}));
}
private getItemUrl_(): string {
return this.item_.url || '';
}
private onItemIdChanged_() {
// TODO(tsergeant): Add a histogram to measure whether this assertion fails
// for real users.
assert(this.getState().nodes[this.itemId]);
this.updateFromStore();
}
private onItemChanged_() {
this.isFolder_ = !this.item_.url;
this.setAttribute(
'aria-label',
this.item_.title || this.item_.url ||
loadTimeData.getString('folderLabel'));
}
private onClick_(e: MouseEvent) {
// Ignore double clicks so that Ctrl double-clicking an item won't deselect
// the item before opening.
if (e.detail !== 2) {
const addKey = isMac ? e.metaKey : e.ctrlKey;
this.dispatch(selectItem(this.itemId, this.getState(), {
clear: !addKey,
range: e.shiftKey,
toggle: addKey && !e.shiftKey,
}));
}
e.stopPropagation();
e.preventDefault();
}
private onKeydown_(e: KeyboardEvent) {
if (e.key === 'ArrowLeft') {
this.focus();
} else if (e.key === 'ArrowRight') {
this.$.menuButton.focus();
} else if (e.key === ' ') {
this.dispatch(selectItem(this.itemId, this.getState(), {
clear: false,
range: false,
toggle: true,
}));
}
}
private onDblClick_(_e: MouseEvent) {
if (!this.isSelectedItem_) {
this.selectThisItem_();
}
const commandManager = BookmarksCommandManagerElement.getInstance();
const itemSet = this.getState().selection.items;
if (commandManager.canExecute(Command.OPEN, itemSet)) {
commandManager.handle(Command.OPEN, itemSet);
}
}
private onMiddleClick_(e: MouseEvent) {
if (e.button !== 1) {
return;
}
this.selectThisItem_();
if (this.isFolder_) {
return;
}
const commandManager = BookmarksCommandManagerElement.getInstance();
const itemSet = this.getState().selection.items;
const command = e.shiftKey ? Command.OPEN : Command.OPEN_NEW_TAB;
if (commandManager.canExecute(command, itemSet)) {
commandManager.handle(command, itemSet);
}
}
private onTouchStart_(e: TouchEvent) {
this.lastTouchPoints_ = e.touches.length;
}
/**
* Prevent default middle-mouse behavior. On Windows, this prevents autoscroll
* (during mousedown), and on Linux this prevents paste (during mouseup).
*/
private cancelMiddleMouseBehavior_(e: MouseEvent) {
if (e.button === 1) {
e.preventDefault();
}
}
private updateFavicon_(url: string) {
this.$.icon.className =
url ? 'website-icon' : 'folder-icon icon-folder-open';
this.$.icon.style.backgroundImage =
url ? getFaviconForPageURL(url, false) : '';
}
private getButtonAriaLabel_(): string {
if (!this.item_) {
return ''; // Item hasn't loaded, skip for now.
}
if (this.isMultiSelectMenu_()) {
return loadTimeData.getStringF('moreActionsMultiButtonAxLabel');
}
return loadTimeData.getStringF(
'moreActionsButtonAxLabel', this.item_.title);
}
/**
* This item is part of a group selection.
*/
private isMultiSelectMenu_(): boolean {
return this.isSelectedItem_ && this.isMultiSelect_;
}
}
declare global {
interface HTMLElementTagNameMap {
'bookmarks-item': BookmarksItemElement;
}
}
customElements.define(BookmarksItemElement.is, BookmarksItemElement);