| // Copyright 2016 The Chromium Authors. All rights reserved. |
| // 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.m.js'; |
| import 'chrome://resources/cr_elements/shared_vars_css.m.js'; |
| import './shared_style.css.js'; |
| import './strings.m.js'; |
| |
| import {CrIconButtonElement} from 'chrome://resources/cr_elements/cr_icon_button/cr_icon_button.js'; |
| import {assert} from 'chrome://resources/js/assert_ts.js'; |
| import {isMac} from 'chrome://resources/js/cr.m.js'; |
| import {focusWithoutInk} from 'chrome://resources/js/cr/ui/focus_without_ink.m.js'; |
| import {getFaviconForPageURL} from 'chrome://resources/js/icon.js'; |
| import {loadTimeData} from 'chrome://resources/js/load_time_data.m.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 {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'; |
| 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); |