| // Copyright 2021 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.m.js'; |
| import 'chrome://resources/cr_elements/shared_vars_css.m.js'; |
| import 'chrome://resources/cr_elements/mwb_element_shared_style.css.js'; |
| |
| import {getFaviconForPageURL} from 'chrome://resources/js/icon.js'; |
| import {DomRepeatEvent, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js'; |
| |
| import {getTemplate} from './bookmark_folder.html.js'; |
| import {BookmarksApiProxy, BookmarksApiProxyImpl} from './bookmarks_api_proxy.js'; |
| |
| export interface BookmarkFolderElement { |
| $: { |
| children: HTMLElement, |
| }; |
| } |
| |
| // Event name for open state of a folder being changed. |
| export const FOLDER_OPEN_CHANGED_EVENT = 'bookmark-folder-open-changed'; |
| |
| export class BookmarkFolderElement extends PolymerElement { |
| static get is() { |
| return 'bookmark-folder'; |
| } |
| |
| static get template() { |
| return getTemplate(); |
| } |
| |
| static get properties() { |
| return { |
| childDepth_: { |
| type: Number, |
| value: 1, |
| }, |
| |
| depth: { |
| type: Number, |
| observer: 'onDepthChanged_', |
| value: 0, |
| }, |
| |
| folder: Object, |
| |
| open_: { |
| type: Boolean, |
| value: false, |
| computed: 'computeIsOpen_(openFolders, folder.id)', |
| }, |
| |
| openFolders: Array, |
| }; |
| } |
| |
| private childDepth_: number; |
| depth: number; |
| folder: chrome.bookmarks.BookmarkTreeNode; |
| private open_: boolean; |
| openFolders: string[]; |
| private bookmarksApi_: BookmarksApiProxy = |
| BookmarksApiProxyImpl.getInstance(); |
| |
| static get observers() { |
| return [ |
| 'onChildrenLengthChanged_(folder.children.length)', |
| ]; |
| } |
| |
| private getAriaExpanded_(): string|undefined { |
| if (!this.folder.children || this.folder.children.length === 0) { |
| // Remove the attribute for empty folders that cannot be expanded. |
| return undefined; |
| } |
| |
| return this.open_ ? 'true' : 'false'; |
| } |
| |
| private onBookmarkAuxClick_( |
| event: DomRepeatEvent<chrome.bookmarks.BookmarkTreeNode, MouseEvent>) { |
| if (event.button !== 1) { |
| // Not a middle click. |
| return; |
| } |
| |
| event.preventDefault(); |
| event.stopPropagation(); |
| this.bookmarksApi_.openBookmark(event.model.item.id!, this.depth, { |
| middleButton: true, |
| altKey: event.altKey, |
| ctrlKey: event.ctrlKey, |
| metaKey: event.metaKey, |
| shiftKey: event.shiftKey, |
| }); |
| } |
| |
| private onBookmarkClick_( |
| event: DomRepeatEvent<chrome.bookmarks.BookmarkTreeNode, MouseEvent>) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| this.bookmarksApi_.openBookmark(event.model.item.id!, this.depth, { |
| middleButton: false, |
| altKey: event.altKey, |
| ctrlKey: event.ctrlKey, |
| metaKey: event.metaKey, |
| shiftKey: event.shiftKey, |
| }); |
| } |
| |
| private onBookmarkContextMenu_( |
| event: DomRepeatEvent<chrome.bookmarks.BookmarkTreeNode, MouseEvent>) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| this.bookmarksApi_.showContextMenu( |
| event.model.item.id, event.clientX, event.clientY); |
| } |
| |
| private onFolderContextMenu_(event: MouseEvent) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| this.bookmarksApi_.showContextMenu( |
| this.folder.id, event.clientX, event.clientY); |
| } |
| |
| private getBookmarkIcon_(url: string): string { |
| return getFaviconForPageURL(url, false); |
| } |
| |
| private onChildrenLengthChanged_() { |
| if (this.folder.children) { |
| this.style.setProperty( |
| '--child-count', this.folder.children!.length.toString()); |
| } else { |
| this.style.setProperty('--child-count', '0'); |
| } |
| } |
| |
| private onDepthChanged_() { |
| this.childDepth_ = this.depth + 1; |
| this.style.setProperty('--node-depth', `${this.depth}`); |
| this.style.setProperty('--child-depth', `${this.childDepth_}`); |
| } |
| |
| private onFolderClick_(event: Event) { |
| event.preventDefault(); |
| event.stopPropagation(); |
| |
| if (!this.folder.children || this.folder.children.length === 0) { |
| // No reason to open if there are no children to show. |
| return; |
| } |
| |
| this.dispatchEvent(new CustomEvent(FOLDER_OPEN_CHANGED_EVENT, { |
| bubbles: true, |
| composed: true, |
| detail: { |
| id: this.folder.id, |
| open: !this.open_, |
| }, |
| })); |
| |
| chrome.metricsPrivate.recordUserAction( |
| this.open_ ? 'SidePanel.Bookmarks.FolderOpen' : |
| 'SidePanel.Bookmarks.FolderClose'); |
| } |
| |
| private computeIsOpen_() { |
| return Boolean(this.openFolders) && |
| this.openFolders.includes(this.folder.id); |
| } |
| |
| private getFocusableRows_(): HTMLElement[] { |
| return Array.from( |
| this.shadowRoot!.querySelectorAll('.row, bookmark-folder')); |
| } |
| |
| getFocusableElement(path: chrome.bookmarks.BookmarkTreeNode[]): (HTMLElement| |
| null) { |
| const currentNode = path.shift(); |
| if (currentNode) { |
| const currentNodeId = currentNode.id; |
| const currentNodeElement = |
| this.shadowRoot!.querySelector(`#bookmark-${currentNodeId}`) as ( |
| HTMLElement | null); |
| if (currentNodeElement && |
| currentNodeElement.classList.contains('bookmark')) { |
| // Found a bookmark item. |
| return currentNodeElement; |
| } |
| |
| if (currentNodeElement && |
| currentNodeElement instanceof BookmarkFolderElement) { |
| // Bookmark item may be a grandchild or be deeper. Iterate through |
| // child BookmarkFolderElements until the bookmark item is found. |
| const nestedElement = currentNodeElement.getFocusableElement(path); |
| if (nestedElement) { |
| return nestedElement; |
| } |
| } |
| } |
| |
| // If all else fails, return the focusable folder row. |
| return this.shadowRoot!.querySelector('#folder'); |
| } |
| |
| moveFocus(delta: -1|1): boolean { |
| const currentFocus = this.shadowRoot!.activeElement; |
| if (currentFocus instanceof BookmarkFolderElement && |
| currentFocus.moveFocus(delta)) { |
| // If focus is already inside a nested folder, delegate the focus to the |
| // nested folder and return early if successful. |
| return true; |
| } |
| |
| let moveFocusTo = null; |
| const focusableRows = this.getFocusableRows_(); |
| if (currentFocus) { |
| // If focus is in this folder, move focus to the next or previous |
| // focusable row. |
| const currentFocusIndex = |
| focusableRows.indexOf(currentFocus as HTMLElement); |
| moveFocusTo = focusableRows[currentFocusIndex + delta]; |
| } else { |
| // If focus is not in this folder yet, move focus to either end. |
| moveFocusTo = delta === 1 ? focusableRows[0] : |
| focusableRows[focusableRows.length - 1]; |
| } |
| |
| if (moveFocusTo instanceof BookmarkFolderElement) { |
| return moveFocusTo.moveFocus(delta); |
| } else if (moveFocusTo) { |
| moveFocusTo.focus(); |
| return true; |
| } else { |
| return false; |
| } |
| } |
| } |
| |
| declare global { |
| interface HTMLElementTagNameMap { |
| 'bookmark-folder': BookmarkFolderElement; |
| } |
| } |
| |
| customElements.define(BookmarkFolderElement.is, BookmarkFolderElement); |
| |
| interface DraggableElement extends HTMLElement { |
| dataBookmark: chrome.bookmarks.BookmarkTreeNode; |
| } |
| |
| export function getBookmarkFromElement(element: HTMLElement): |
| chrome.bookmarks.BookmarkTreeNode { |
| return (element as DraggableElement).dataBookmark; |
| } |
| |
| export function isValidDropTarget(element: HTMLElement) { |
| return element.id === 'folder' || element.classList.contains('bookmark'); |
| } |
| |
| export function isBookmarkFolderElement(element: HTMLElement): boolean { |
| return element.id === 'folder'; |
| } |