blob: ee7d1baab48e6d79bb4dad1af5ba8e7d0c089724 [file] [log] [blame]
// 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_components/managed_footnote/managed_footnote.js';
import 'chrome://resources/cr_elements/shared_style_css.m.js';
import 'chrome://resources/cr_elements/shared_vars_css.m.js';
import 'chrome://resources/cr_elements/cr_button/cr_button.m.js';
import 'chrome://resources/cr_elements/cr_toast/cr_toast_manager.js';
import 'chrome://resources/cr_elements/cr_splitter/cr_splitter.js';
import './folder_node.js';
import './list.js';
import './router.js';
import './shared_vars.js';
import './strings.m.js';
import './command_manager.js';
import './toolbar.js';
import {CrSplitterElement} from 'chrome://resources/cr_elements/cr_splitter/cr_splitter.js';
import {FindShortcutMixin, FindShortcutMixinInterface} from 'chrome://resources/cr_elements/find_shortcut_mixin.js';
import {StoreObserver} from 'chrome://resources/js/cr/ui/store.m.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.m.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.m.js';
import {IronScrollTargetBehavior} from 'chrome://resources/polymer/v3_0/iron-scroll-target-behavior/iron-scroll-target-behavior.js';
import {html, mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {setSearchResults} from './actions.js';
import {destroy as destroyApiListener, init as initApiListener} from './api_listener.js';
import {LOCAL_STORAGE_FOLDER_STATE_KEY, LOCAL_STORAGE_TREE_WIDTH_KEY, ROOT_NODE_ID} from './constants.js';
import {DNDManager} from './dnd_manager.js';
import {MouseFocusMixin} from './mouse_focus_behavior.js';
import {Store} from './store.js';
import {BookmarksStoreClientInterface, StoreClient} from './store_client.js';
import {BookmarksToolbarElement} from './toolbar.js';
import {BookmarksPageState, FolderOpenState} from './types.js';
import {createEmptyState, normalizeNodes} from './util.js';
const BookmarksAppElementBase =
mixinBehaviors(
[StoreClient, IronScrollTargetBehavior],
MouseFocusMixin(FindShortcutMixin(PolymerElement))) as {
new (): PolymerElement & BookmarksStoreClientInterface &
StoreObserver<BookmarksPageState>& FindShortcutMixinInterface &
IronScrollTargetBehavior
};
export interface BookmarksAppElement {
$: {
splitter: CrSplitterElement,
sidebar: HTMLDivElement,
}
}
export class BookmarksAppElement extends BookmarksAppElementBase {
static get is() {
return 'bookmarks-app';
}
static get properties() {
return {
searchTerm_: {
type: String,
observer: 'searchTermChanged_',
},
folderOpenState_: {
type: Object,
observer: 'folderOpenStateChanged_',
},
sidebarWidth_: String,
toolbarShadow_: {
type: Boolean,
reflectToAttribute: true,
},
};
}
private eventTracker_: EventTracker = new EventTracker();
private dndManager_: DNDManager|null = null;
private folderOpenState_: FolderOpenState;
private searchTerm_: string;
private sidebarWidth_: string;
private toolbarShadow_: boolean;
constructor() {
super();
// Regular expression that captures the leading slash, the content and the
// trailing slash in three different groups.
const CANONICAL_PATH_REGEX = /(^\/)([\/-\w]+)(\/$)/;
const path = location.pathname.replace(CANONICAL_PATH_REGEX, '$1$2');
if (path !== '/') { // Only queries are supported, not subpages.
window.history.replaceState(undefined /* stateObject */, '', '/');
}
}
connectedCallback() {
super.connectedCallback();
document.documentElement.classList.remove('loading');
this.watch('searchTerm_', function(state: BookmarksPageState) {
return state.search.term;
});
this.watch('folderOpenState_', function(state: BookmarksPageState) {
return state.folderOpenState;
});
chrome.bookmarks.getTree((results) => {
const nodeMap = normalizeNodes(results[0]!);
const initialState = createEmptyState();
initialState.nodes = nodeMap;
initialState.selectedFolder = nodeMap[ROOT_NODE_ID]!.children![0]!;
const folderStateString =
window.localStorage[LOCAL_STORAGE_FOLDER_STATE_KEY];
initialState.folderOpenState = folderStateString ?
new Map(JSON.parse(folderStateString)) :
new Map();
Store.getInstance().init(initialState);
initApiListener();
setTimeout(function() {
chrome.metricsPrivate.recordTime(
'BookmarkManager.ResultsRenderedTime',
Math.floor(window.performance.now()));
});
});
this.initializeSplitter_();
this.dndManager_ = new DNDManager();
this.dndManager_.init();
this.scrollTarget = this.shadowRoot!.querySelector('bookmarks-list');
}
disconnectedCallback() {
super.disconnectedCallback();
this.eventTracker_.remove(window, 'resize');
this.dndManager_!.destroy();
destroyApiListener();
}
private initializeSplitter_(): void {
const splitter = this.$.splitter;
const splitterTarget = this.$.sidebar;
// The splitter persists the size of the left component in the local store.
if (LOCAL_STORAGE_TREE_WIDTH_KEY in window.localStorage) {
splitterTarget.style.width =
window.localStorage[LOCAL_STORAGE_TREE_WIDTH_KEY];
}
this.sidebarWidth_ = getComputedStyle(splitterTarget).width;
splitter.addEventListener('resize', (e) => {
window.localStorage[LOCAL_STORAGE_TREE_WIDTH_KEY] =
splitterTarget.style.width;
this.updateSidebarWidth_();
});
this.eventTracker_.add(splitter, 'dragmove',
() => this.updateSidebarWidth_());
this.eventTracker_.add(window, 'resize', () => this.updateSidebarWidth_());
}
private updateSidebarWidth_(): void {
this.sidebarWidth_ = getComputedStyle(this.$.sidebar).width;
}
private searchTermChanged_(newValue: string, oldValue?: string) {
if (oldValue !== undefined && !newValue) {
this.dispatchEvent(new CustomEvent('iron-announce', {
bubbles: true,
composed: true,
detail: {text: loadTimeData.getString('searchCleared')}
}));
}
if (!this.searchTerm_) {
return;
}
chrome.bookmarks.search(this.searchTerm_, (results) => {
const ids = results.map(function(node) {
return node.id;
});
this.dispatch(setSearchResults(ids));
this.dispatchEvent(new CustomEvent('iron-announce', {
bubbles: true,
composed: true,
detail: {
text: ids.length > 0 ?
loadTimeData.getStringF('searchResults', this.searchTerm_) :
loadTimeData.getString('noSearchResults')
}
}));
});
}
private folderOpenStateChanged_(): void {
window.localStorage[LOCAL_STORAGE_FOLDER_STATE_KEY] =
JSON.stringify(Array.from(this.folderOpenState_));
}
// Override FindShortcutMixin methods.
handleFindShortcut(modalContextOpen: boolean): boolean {
if (modalContextOpen) {
return false;
}
this.shadowRoot!.querySelector<BookmarksToolbarElement>(
'bookmarks-toolbar')!.searchField.showAndFocus();
return true;
}
// Override FindShortcutMixin methods.
searchInputHasFocus(): boolean {
return this.shadowRoot!.querySelector<BookmarksToolbarElement>(
'bookmarks-toolbar')!.searchField.isSearchFocused();
}
private onUndoClick_(): void {
this.dispatchEvent(
new CustomEvent('command-undo', {bubbles: true, composed: true}));
}
/** Overridden from IronScrollTargetBehavior */
_scrollHandler() {
this.toolbarShadow_ = this.scrollTarget!.scrollTop !== 0;
}
static get template() {
return html`{__html_template__}`;
}
}
customElements.define(BookmarksAppElement.is, BookmarksAppElement);