// 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_components/history_clusters/clusters.js';
import 'chrome://resources/cr_components/history_embeddings/filter_chips.js';
import 'chrome://resources/cr_components/history_embeddings/history_embeddings.js';
import 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import 'chrome://resources/cr_elements/cr_shared_style.css.js';
import 'chrome://resources/cr_elements/cr_shared_vars.css.js';
import 'chrome://resources/cr_elements/cr_tabs/cr_tabs.js';
import 'chrome://resources/polymer/v3_0/iron-media-query/iron-media-query.js';
import 'chrome://resources/cr_elements/cr_page_selector/cr_page_selector.js';
import './history_embeddings_promo.js';
import './history_list.js';
import './history_toolbar.js';
import './query_manager.js';
import './shared_style.css.js';
import './side_bar.js';
import './strings.m.js';
import './product_specifications_lists.js';
import {HelpBubbleMixin} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin.js';
import type {HelpBubbleMixinInterface} from 'chrome://resources/cr_components/help_bubble/help_bubble_mixin.js';
import {HistoryResultType} from 'chrome://resources/cr_components/history/constants.js';
import {HistoryEmbeddingsBrowserProxyImpl} from 'chrome://resources/cr_components/history_embeddings/browser_proxy.js';
import type {Suggestion} from 'chrome://resources/cr_components/history_embeddings/filter_chips.js';
import type {HistoryEmbeddingsMoreActionsClickEvent} from 'chrome://resources/cr_components/history_embeddings/history_embeddings.js';
import {getInstance as getAnnouncerInstance} from 'chrome://resources/cr_elements/cr_a11y_announcer/cr_a11y_announcer.js';
import type {CrDrawerElement} from 'chrome://resources/cr_elements/cr_drawer/cr_drawer.js';
import type {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import type {CrPageSelectorElement} from 'chrome://resources/cr_elements/cr_page_selector/cr_page_selector.js';
import type {FindShortcutMixinInterface} from 'chrome://resources/cr_elements/find_shortcut_mixin.js';
import {FindShortcutMixin} from 'chrome://resources/cr_elements/find_shortcut_mixin.js';
import type {WebUiListenerMixinInterface} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {WebUiListenerMixin} from 'chrome://resources/cr_elements/web_ui_listener_mixin.js';
import {assert} from 'chrome://resources/js/assert.js';
import {EventTracker} from 'chrome://resources/js/event_tracker.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getTrustedScriptURL} from 'chrome://resources/js/static_types.js';
import {hasKeyModifiers} from 'chrome://resources/js/util.js';
import {IronScrollTargetBehavior} from 'chrome://resources/polymer/v3_0/iron-scroll-target-behavior/iron-scroll-target-behavior.js';
import {mixinBehaviors, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {getTemplate} from './app.html.js';
import type {BrowserService} from './browser_service.js';
import {BrowserServiceImpl} from './browser_service.js';
import {HistoryPageViewHistogram} from './constants.js';
import type {ForeignSession, QueryResult, QueryState} from './externs.js';
import type {HistoryListElement} from './history_list.js';
import type {HistoryToolbarElement} from './history_toolbar.js';
import {convertDateToQueryValue} from './query_manager.js';
import {Page, TABBED_PAGES} from './router.js';
import type {HistoryRouterElement} from './router.js';
import type {FooterInfo, HistorySideBarElement} from './side_bar.js';
let lazyLoadPromise: Promise<void>|null = null;
export function ensureLazyLoaded(): Promise<void> {
if (!lazyLoadPromise) {
const script = document.createElement('script');
script.type = 'module';
script.src = getTrustedScriptURL`./lazy_load.js` as unknown as string;
document.body.appendChild(script);
lazyLoadPromise = Promise.all([
customElements.whenDefined('history-synced-device-manager'),
customElements.whenDefined('product-specifications-lists'),
customElements.whenDefined('cr-action-menu'),
customElements.whenDefined('cr-button'),
customElements.whenDefined('cr-checkbox'),
customElements.whenDefined('cr-dialog'),
customElements.whenDefined('cr-drawer'),
customElements.whenDefined('cr-icon-button'),
customElements.whenDefined('cr-toolbar-selection-overlay'),
]) as unknown as Promise<void>;
}
return lazyLoadPromise;
}
// Adds click/auxclick listeners for any link on the page. If the link points
// to a chrome: or file: url, then calls into the browser to do the
// navigation. Note: This method is *not* re-entrant. Every call to it, will
// re-add listeners on |document|. It's up to callers to ensure this is only
// called once.
export function listenForPrivilegedLinkClicks() {
['click', 'auxclick'].forEach(function(eventName) {
document.addEventListener(eventName, function(evt: Event) {
const e = evt as MouseEvent;
// Ignore buttons other than left and middle.
if (e.button > 1 || e.defaultPrevented) {
return;
}
const eventPath = e.composedPath() as HTMLElement[];
let anchor: HTMLAnchorElement|null = null;
if (eventPath) {
for (let i = 0; i < eventPath.length; i++) {
const element = eventPath[i];
if (element.tagName === 'A' && (element as HTMLAnchorElement).href) {
anchor = element as HTMLAnchorElement;
break;
}
}
}
// Fallback if Event.path is not available.
let el = e.target as HTMLElement;
if (!anchor && el.nodeType === Node.ELEMENT_NODE &&
el.webkitMatchesSelector('A, A *')) {
while (el.tagName !== 'A') {
el = el.parentElement as HTMLElement;
}
anchor = el as HTMLAnchorElement;
}
if (!anchor) {
return;
}
if ((anchor.protocol === 'file:' || anchor.protocol === 'about:') &&
(e.button === 0 || e.button === 1)) {
BrowserServiceImpl.getInstance().navigateToUrl(
anchor.href, anchor.target, e);
e.preventDefault();
}
});
});
}
export interface HistoryAppElement {
$: {
'content': CrPageSelectorElement,
'content-side-bar': HistorySideBarElement,
'drawer': CrLazyRenderElement<CrDrawerElement>,
'history': HistoryListElement,
'tabs-container': Element,
'tabs-content': CrPageSelectorElement,
'toolbar': HistoryToolbarElement,
tabsScrollContainer: HTMLElement,
router: HistoryRouterElement,
historyEmbeddingsContainer: HTMLElement,
};
}
const HistoryAppElementBase = mixinBehaviors(
[IronScrollTargetBehavior],
HelpBubbleMixin(FindShortcutMixin(
WebUiListenerMixin(PolymerElement)))) as {
new (): PolymerElement & HelpBubbleMixinInterface &
FindShortcutMixinInterface & IronScrollTargetBehavior &
WebUiListenerMixinInterface,
};
export class HistoryAppElement extends HistoryAppElementBase {
static get is() {
return 'history-app';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
enableHistoryEmbeddings_: {
type: Boolean,
value: () => loadTimeData.getBoolean('enableHistoryEmbeddings'),
reflectToAttribute: true,
},
contentPage_: {
type: String,
value: Page.HISTORY,
},
tabsContentPage_: {
type: String,
value: Page.HISTORY,
},
// The id of the currently selected page.
selectedPage_: {
type: String,
observer: 'selectedPageChanged_',
},
queryResult_: Object,
// Updated on synced-device-manager attach by chrome.sending
// 'otherDevicesInitialized'.
isUserSignedIn_: Boolean,
pendingDelete_: Boolean,
toolbarShadow_: {
type: Boolean,
reflectToAttribute: true,
notify: true,
},
queryState_: Object,
// True if the window is narrow enough for the page to have a drawer.
hasDrawer_: {
type: Boolean,
observer: 'hasDrawerChanged_',
},
footerInfo: {
type: Object,
value() {
return {
managed: loadTimeData.getBoolean('isManaged'),
otherFormsOfHistory: false,
};
},
},
historyClustersEnabled_: {
type: Boolean,
value: () => loadTimeData.getBoolean('isHistoryClustersEnabled'),
},
historyClustersVisible_: {
type: Boolean,
value: () => loadTimeData.getBoolean('isHistoryClustersVisible'),
},
lastSelectedTab_: {
type: Number,
value: () => loadTimeData.getInteger('lastSelectedTab'),
},
showHistoryClusters_: {
type: Boolean,
computed:
'computeShowHistoryClusters_(historyClustersEnabled_, historyClustersVisible_)',
reflectToAttribute: true,
},
showTabs_: {
type: Boolean,
computed:
'computeShowTabs_(showHistoryClusters_, enableHistoryEmbeddings_)',
},
// The index of the currently selected tab.
selectedTab_: {
type: Number,
observer: 'selectedTabChanged_',
},
tabsIcons_: {
type: Array,
value: () =>
['images/list.svg', 'chrome://resources/images/icon_journeys.svg'],
},
tabsNames_: {
type: Array,
value: () => {
return [
loadTimeData.getString('historyListTabLabel'),
loadTimeData.getString('historyClustersTabLabel'),
];
},
},
scrollTarget_: Object,
queryStateAfterDate_: {
type: Object,
computed: 'computeQueryStateAfterDate_(queryState_.*)',
},
hasHistoryEmbeddingsResults_: {
type: Boolean,
value: false,
reflectToAttribute: true,
},
compareHistoryEnabled_: Boolean,
tabContentScrollOffset_: Number,
};
}
footerInfo: FooterInfo;
private browserService_: BrowserService = BrowserServiceImpl.getInstance();
private enableHistoryEmbeddings_: boolean;
private eventTracker_: EventTracker = new EventTracker();
private hasDrawer_: boolean;
private historyClustersEnabled_: boolean;
private historyClustersVisible_: boolean;
private isUserSignedIn_: boolean = loadTimeData.getBoolean('isUserSignedIn');
private lastSelectedTab_: number;
private contentPage_: string;
private tabsContentPage_: string;
private pendingDelete_: boolean;
private queryResult_: QueryResult;
private queryState_: QueryState;
private selectedPage_: string;
private selectedTab_: number;
private lastRecordedSelectedPageHistogramValue_: HistoryPageViewHistogram =
HistoryPageViewHistogram.END;
private showHistoryClusters_: boolean;
private tabsIcons_: string[];
private tabsNames_: string[];
private toolbarShadow_: boolean;
private historyClustersViewStartTime_: Date|null = null;
private scrollTarget_: HTMLElement;
private queryStateAfterDate_?: Date;
private hasHistoryEmbeddingsResults_: boolean;
private compareHistoryEnabled_: boolean =
loadTimeData.getBoolean('compareHistoryEnabled');
private historyEmbeddingsResizeObserver_?: ResizeObserver;
private tabContentScrollOffset_: number = 0;
private dataFromNativeBeforeInput_: string|null = null;
private numCharsTypedInSearch_: number = 0;
constructor() {
super();
this.queryResult_ = {
info: undefined,
results: undefined,
sessionList: undefined,
};
listenForPrivilegedLinkClicks();
}
override connectedCallback() {
super.connectedCallback();
this.eventTracker_.add(
document, 'keydown', (e: Event) => this.onKeyDown_(e as KeyboardEvent));
this.eventTracker_.add(
document, 'visibilitychange', this.onVisibilityChange_.bind(this));
this.eventTracker_.add(
document, 'record-history-link-click',
this.onRecordHistoryLinkClick_.bind(this));
this.addWebUiListener(
'sign-in-state-changed',
(signedIn: boolean) => this.onSignInStateChanged_(signedIn));
this.addWebUiListener(
'has-other-forms-changed',
(hasOtherForms: boolean) =>
this.onHasOtherFormsChanged_(hasOtherForms));
this.addWebUiListener(
'foreign-sessions-changed',
(sessionList: ForeignSession[]) =>
this.setForeignSessions_(sessionList));
this.shadowRoot!.querySelector('history-query-manager')!.initialize();
this.browserService_!.getForeignSessions().then(
sessionList => this.setForeignSessions_(sessionList));
}
override ready() {
super.ready();
this.addEventListener('cr-toolbar-menu-click', this.onCrToolbarMenuClick_);
this.addEventListener('delete-selected', this.deleteSelected);
this.addEventListener('history-checkbox-select', this.checkboxSelected);
this.addEventListener(
'product-spec-item-select',
this.productSpecificationsCheckboxSelected_);
this.addEventListener('history-close-drawer', this.closeDrawer_);
this.addEventListener('history-view-changed', this.historyViewChanged_);
this.addEventListener('unselect-all', this.unselectAll);
if (loadTimeData.getBoolean('enableHistoryEmbeddings')) {
this.registerHelpBubble(
'kHistorySearchInputElementId', this.$.toolbar.searchField);
// TODO(crbug.com/40075330): There might be a race condition if the call
// to show the help bubble comes immediately after registering the
// anchor.
setTimeout(() => {
HistoryEmbeddingsBrowserProxyImpl.getInstance().maybeShowFeaturePromo();
}, 1000);
}
}
private getShowResultsByGroup_() {
return this.selectedPage_ === Page.HISTORY_CLUSTERS;
}
private onShowResultsByGroupChanged_(e: CustomEvent<{value: boolean}>) {
const showResultsByGroup = e.detail.value;
if (showResultsByGroup) {
this.selectedTab_ = TABBED_PAGES.indexOf(Page.HISTORY_CLUSTERS);
} else {
this.selectedTab_ = TABBED_PAGES.indexOf(Page.HISTORY);
}
}
override disconnectedCallback() {
super.disconnectedCallback();
this.eventTracker_.removeAll();
if (this.historyEmbeddingsResizeObserver_) {
this.historyEmbeddingsResizeObserver_.disconnect();
this.historyEmbeddingsResizeObserver_ = undefined;
}
}
private fire_(eventName: string, detail?: any) {
this.dispatchEvent(
new CustomEvent(eventName, {bubbles: true, composed: true, detail}));
}
private computeShowHistoryClusters_(): boolean {
return this.historyClustersEnabled_ && this.historyClustersVisible_;
}
private computeShowTabs_(): boolean {
return this.showHistoryClusters_ && !this.enableHistoryEmbeddings_;
}
private historyClustersSelected_(
_selectedPage: string, _showHistoryClusters: boolean): boolean {
return this.selectedPage_ === Page.HISTORY_CLUSTERS &&
this.showHistoryClusters_;
}
private comparisonTablesSelected_(_selectedPage: string): boolean {
return this.compareHistoryEnabled_ &&
this.selectedPage_ === Page.PRODUCT_SPECIFICATIONS_LISTS;
}
private onFirstRender_() {
// Focus the search field on load. Done here to ensure the history page
// is rendered before we try to take focus.
const searchField = this.$.toolbar.searchField;
if (!searchField.narrow) {
searchField.getSearchInput().focus();
}
// Lazily load the remainder of the UI.
ensureLazyLoaded().then(function() {
requestIdleCallback(function() {
// https://github.com/microsoft/TypeScript/issues/13569
(document as any).fonts.load('bold 12px Roboto');
});
});
}
/** Overridden from IronScrollTargetBehavior */
/* eslint-disable-next-line @typescript-eslint/naming-convention */
override _scrollHandler() {
if (this.scrollTarget) {
// When the tabs are visible, show the toolbar shadow for the synced
// devices page or product specifications page.
this.toolbarShadow_ = this.scrollTarget.scrollTop !== 0 &&
(!this.showHistoryClusters_ ||
this.syncedTabsSelected_(this.selectedPage_!) ||
this.selectedPage_ === Page.PRODUCT_SPECIFICATIONS_LISTS);
}
}
private onCrToolbarMenuClick_() {
this.$.drawer.get().toggle();
}
/**
* Listens for history-item being selected or deselected (through checkbox)
* and changes the view of the top toolbar.
*/
checkboxSelected() {
this.$.toolbar.count = this.$.history.getSelectedItemCount();
}
/**
* Listens for product-specs-item being selected or deselected (through
* checkbox) and changes the view of the top toolbar.
*/
private productSpecificationsCheckboxSelected_() {
if (this.selectedPage_ !== Page.PRODUCT_SPECIFICATIONS_LISTS) {
return;
}
const productSpecsListElement =
this.shadowRoot!.querySelector('product-specifications-lists');
assert(productSpecsListElement);
this.$.toolbar.count = productSpecsListElement.getSelectedItemCount();
}
selectOrUnselectAll() {
if (this.selectedPage_ === Page.PRODUCT_SPECIFICATIONS_LISTS) {
const productSpecsListElement =
this.shadowRoot!.querySelector('product-specifications-lists');
assert(productSpecsListElement);
productSpecsListElement.selectOrUnselectAll();
this.$.toolbar.count = productSpecsListElement.getSelectedItemCount();
return;
}
this.$.history.selectOrUnselectAll();
this.$.toolbar.count = this.$.history.getSelectedItemCount();
}
/**
* Listens for call to cancel selection and loops through all items to set
* checkbox to be unselected.
*/
private unselectAll() {
if (this.selectedPage_ === Page.PRODUCT_SPECIFICATIONS_LISTS) {
this.productSpecificationsUnselectAll_();
return;
}
this.$.history.unselectAllItems();
this.$.toolbar.count = 0;
}
private productSpecificationsUnselectAll_() {
const productSpecsListElement =
this.shadowRoot!.querySelector('product-specifications-lists');
// This method is also called on selectedPageChanged, so it is possible
// for the list element to be empty.
if (productSpecsListElement) {
productSpecsListElement.unselectAllItems();
this.$.toolbar.count = 0;
}
}
deleteSelected() {
if (this.selectedPage_ === Page.PRODUCT_SPECIFICATIONS_LISTS) {
const productSpecsListElement =
this.shadowRoot!.querySelector('product-specifications-lists');
assert(productSpecsListElement);
productSpecsListElement.deleteSelectedWithPrompt();
} else {
this.$.history.deleteSelectedWithPrompt();
}
}
private onQueryFinished_() {
this.$.history.historyResult(
this.queryResult_.info!, this.queryResult_.results!);
if (document.body.classList.contains('loading')) {
document.body.classList.remove('loading');
this.onFirstRender_();
}
}
private onKeyDown_(e: KeyboardEvent) {
if ((e.key === 'Delete' || e.key === 'Backspace') && !hasKeyModifiers(e)) {
this.onDeleteCommand_();
return;
}
if (e.key === 'a' && !e.altKey && !e.shiftKey) {
let hasTriggerModifier = e.ctrlKey && !e.metaKey;
// <if expr="is_macosx">
hasTriggerModifier = !e.ctrlKey && e.metaKey;
// </if>
if (hasTriggerModifier && this.onSelectAllCommand_()) {
e.preventDefault();
}
}
if (e.key === 'Escape') {
this.unselectAll();
getAnnouncerInstance().announce(
loadTimeData.getString('itemsUnselected'));
e.preventDefault();
}
}
private onVisibilityChange_() {
if (this.selectedPage_ !== Page.HISTORY_CLUSTERS) {
return;
}
if (document.visibilityState === 'hidden') {
this.recordHistoryClustersDuration_();
} else if (
document.visibilityState === 'visible' &&
this.historyClustersViewStartTime_ === null) {
// Restart the timer if the user switches back to the History tab.
this.historyClustersViewStartTime_ = new Date();
}
}
private onRecordHistoryLinkClick_(
e: CustomEvent<{resultType: HistoryResultType, index: number}>) {
// All of the above code only applies to History search results, not the
// zero-query state. Check queryResult_ instead of queryState_ to key on
// actually displayed results rather than the latest user input, which may
// not have finished loading yet.
if (!this.queryResult_.info || !this.queryResult_.info.term) {
return;
}
this.browserService_!.recordHistogram(
'History.SearchResultClicked.Type', e.detail.resultType,
HistoryResultType.END);
// MetricsHandler uses a 100 bucket limit, so the max index is 99.
const maxIndex = 99;
const clampedIndex = Math.min(e.detail.index, 99);
this.browserService_!.recordHistogram(
'History.SearchResultClicked.Index', clampedIndex, maxIndex);
switch (e.detail.resultType) {
case HistoryResultType.TRADITIONAL: {
this.browserService_!.recordHistogram(
'History.SearchResultClicked.Index.Traditional', clampedIndex,
maxIndex);
break;
}
case HistoryResultType.GROUPED: {
this.browserService_!.recordHistogram(
'History.SearchResultClicked.Index.Grouped', clampedIndex,
maxIndex);
break;
}
case HistoryResultType.EMBEDDINGS: {
this.browserService_!.recordHistogram(
'History.SearchResultClicked.Index.Embeddings', clampedIndex,
maxIndex);
break;
}
}
}
private onDeleteCommand_() {
if (this.$.toolbar.count === 0 || this.pendingDelete_) {
return;
}
this.deleteSelected();
}
/**
* @return Whether the command was actually triggered.
*/
private onSelectAllCommand_(): boolean {
if (this.$.toolbar.searchField.isSearchFocused() ||
this.syncedTabsSelected_(this.selectedPage_!) ||
this.historyClustersSelected_(
this.selectedPage_!, this.showHistoryClusters_)) {
return false;
}
this.selectOrUnselectAll();
return true;
}
/**
* @param sessionList Array of objects describing the sessions from other
* devices.
*/
private setForeignSessions_(sessionList: ForeignSession[]) {
this.set('queryResult_.sessionList', sessionList);
}
/**
* Update sign in state of synced device manager after user logs in or out.
*/
private onSignInStateChanged_(isUserSignedIn: boolean) {
this.isUserSignedIn_ = isUserSignedIn;
}
/**
* Update sign in state of synced device manager after user logs in or out.
*/
private onHasOtherFormsChanged_(hasOtherForms: boolean) {
this.set('footerInfo.otherFormsOfHistory', hasOtherForms);
}
private syncedTabsSelected_(_selectedPage: string): boolean {
return this.selectedPage_ === Page.SYNCED_TABS;
}
/**
* @return Whether a loading spinner should be shown (implies the
* backend is querying a new search term).
*/
private shouldShowSpinner_(
querying: boolean, incremental: boolean, searchTerm: string): boolean {
return querying && !incremental && searchTerm !== '';
}
private updateContentPage_() {
switch (this.selectedPage_) {
case Page.SYNCED_TABS:
this.contentPage_ = Page.SYNCED_TABS;
break;
case Page.PRODUCT_SPECIFICATIONS_LISTS:
this.contentPage_ = Page.PRODUCT_SPECIFICATIONS_LISTS;
break;
default:
this.contentPage_ = Page.HISTORY;
}
}
private updateTabsContentPage_() {
this.tabsContentPage_ = (this.selectedPage_ === Page.HISTORY_CLUSTERS &&
this.historyClustersEnabled_) ?
Page.HISTORY_CLUSTERS :
Page.HISTORY;
}
private selectedPageChanged_(newPage: string, oldPage: string) {
this.updateContentPage_();
this.updateTabsContentPage_();
this.unselectAll();
this.historyViewChanged_();
this.maybeUpdateSelectedHistoryTab_();
if (oldPage === Page.HISTORY_CLUSTERS &&
newPage !== Page.HISTORY_CLUSTERS) {
this.recordHistoryClustersDuration_();
}
if (newPage === Page.HISTORY_CLUSTERS) {
this.historyClustersViewStartTime_ = new Date();
}
}
private updateScrollTarget_() {
const topLevelIronPages = this.$['content'];
const topLevelHistoryPage = this.$['tabs-container'];
if (topLevelIronPages.selectedItem &&
topLevelIronPages.selectedItem === topLevelHistoryPage) {
if (this.enableHistoryEmbeddings_) {
// The top-level History page has another inner IronPages element that
// can toggle between different pages.
this.scrollTarget = this.$.tabsScrollContainer;
} else {
this.scrollTarget = this.$['tabs-content'].selectedItem as HTMLElement;
}
} else if (topLevelIronPages.selectedItem) {
this.scrollTarget = topLevelIronPages.selectedItem as HTMLElement;
} else {
this.scrollTarget = null;
}
// Notify iron-list parents of potential resize, since the selected
// page or tab has changed.
setTimeout(() => {
this.$.history.notifyResize();
}, 0);
}
private selectedTabChanged_() {
this.lastSelectedTab_ = this.selectedTab_;
// Change in the currently selected tab requires change in the currently
// selected page.
if (!this.selectedPage_ || TABBED_PAGES.includes(this.selectedPage_)) {
this.selectedPage_ = TABBED_PAGES[this.selectedTab_];
}
this.browserService_!.setLastSelectedTab(this.selectedTab_);
}
private maybeUpdateSelectedHistoryTab_() {
// Change in the currently selected page may require change in the currently
// selected tab.
if (TABBED_PAGES.includes(this.selectedPage_)) {
this.selectedTab_ = TABBED_PAGES.indexOf(this.selectedPage_);
}
}
private historyViewChanged_() {
// This allows the synced-device-manager to render so that it can be set
// as the scroll target.
requestAnimationFrame(() => {
this._scrollHandler();
});
this.recordHistoryPageView_();
}
// Records the history clusters page duration.
private recordHistoryClustersDuration_() {
assert(this.historyClustersViewStartTime_ !== null);
const duration =
new Date().getTime() - this.historyClustersViewStartTime_.getTime();
this.browserService_!.recordLongTime(
'History.Clusters.WebUISessionDuration', duration);
this.historyClustersViewStartTime_ = null;
}
private hasDrawerChanged_() {
const drawer = this.$.drawer.getIfExists();
if (!this.hasDrawer_ && drawer && drawer.open) {
drawer.cancel();
}
}
private closeDrawer_() {
const drawer = this.$.drawer.get() as CrDrawerElement;
if (drawer && drawer.open) {
drawer.close();
}
}
private recordHistoryPageView_() {
let histogramValue = HistoryPageViewHistogram.END;
switch (this.selectedPage_) {
case Page.HISTORY_CLUSTERS:
histogramValue = HistoryPageViewHistogram.JOURNEYS;
break;
case Page.SYNCED_TABS:
histogramValue = this.isUserSignedIn_ ?
HistoryPageViewHistogram.SYNCED_TABS :
HistoryPageViewHistogram.SIGNIN_PROMO;
break;
case Page.PRODUCT_SPECIFICATIONS_LISTS:
histogramValue = HistoryPageViewHistogram.PRODUCT_SPECIFICATIONS_LISTS;
break;
default:
histogramValue = HistoryPageViewHistogram.HISTORY;
break;
}
// Avoid double-recording the same page consecutively.
if (histogramValue === this.lastRecordedSelectedPageHistogramValue_) {
return;
}
this.lastRecordedSelectedPageHistogramValue_ = histogramValue;
this.browserService_!.recordHistogram(
'History.HistoryPageView', histogramValue,
HistoryPageViewHistogram.END);
}
// Override FindShortcutMixin methods.
override handleFindShortcut(modalContextOpen: boolean): boolean {
if (modalContextOpen) {
return false;
}
this.$.toolbar.searchField.showAndFocus();
return true;
}
// Override FindShortcutMixin methods.
override searchInputHasFocus(): boolean {
return this.$.toolbar.searchField.isSearchFocused();
}
setHasDrawerForTesting(enabled: boolean) {
this.hasDrawer_ = enabled;
}
private shouldShowHistoryEmbeddings_(): boolean {
if (!loadTimeData.getBoolean('enableHistoryEmbeddings')) {
return false;
}
if (!this.queryState_.searchTerm) {
return false;
}
return this.queryState_.searchTerm.split(' ')
.filter(part => part.length > 0)
.length >=
loadTimeData.getInteger('historyEmbeddingsSearchMinimumWordCount');
}
private onSelectedSuggestionChanged_(e: CustomEvent<{value: Suggestion}>) {
let afterString: string|undefined = undefined;
if (e.detail.value?.timeRangeStart) {
afterString = convertDateToQueryValue(e.detail.value.timeRangeStart);
}
this.fire_('change-query', {
search: this.queryState_.searchTerm,
after: afterString,
});
}
private computeQueryStateAfterDate_(): Date|undefined {
const afterString = this.queryState_.after;
if (!afterString) {
return undefined;
}
const afterDate = new Date(afterString + 'T00:00:00');
// This compute function listens for any subproperty changes on the
// queryState_ so the `after` param may not have changed.
if (this.queryStateAfterDate_?.getTime() === afterDate.getTime()) {
return this.queryStateAfterDate_;
}
return afterDate;
}
private onHistoryEmbeddingsItemMoreFromSiteClick_(
e: HistoryEmbeddingsMoreActionsClickEvent) {
const historyEmbeddingsItem = e.detail;
this.fire_(
'change-query',
{search: 'host:' + new URL(historyEmbeddingsItem.url.url).hostname});
}
private onHistoryEmbeddingsItemRemoveClick_(
e: HistoryEmbeddingsMoreActionsClickEvent) {
const historyEmbeddingsItem = e.detail;
this.browserService_.removeVisits([{
url: historyEmbeddingsItem.url.url,
timestamps: [historyEmbeddingsItem.lastUrlVisitTimestamp],
}]);
}
private onHistoryEmbeddingsIsEmptyChanged_(e: CustomEvent<{value: boolean}>) {
this.hasHistoryEmbeddingsResults_ = !e.detail.value;
}
private onHistoryEmbeddingsContainerShown_() {
assert(this.enableHistoryEmbeddings_);
const historyEmbeddingsContainer =
this.shadowRoot!.querySelector('#historyEmbeddingsContainer');
assert(historyEmbeddingsContainer);
this.historyEmbeddingsResizeObserver_ = new ResizeObserver((entries) => {
assert(entries.length === 1);
this.tabContentScrollOffset_ = entries[0].contentRect.height;
});
this.historyEmbeddingsResizeObserver_.observe(historyEmbeddingsContainer);
}
private onToolbarSearchInputNativeBeforeInput_(
e: CustomEvent<{e: InputEvent}>) {
// TODO(crbug.com/40673976): This needs to be cached on the `beforeinput`
// event since there is a bug where this data is not available in the
// native `input` event below.
this.dataFromNativeBeforeInput_ = e.detail.e.data;
}
private onToolbarSearchInputNativeInput_(
e: CustomEvent<{e: InputEvent, inputValue: string}>) {
const insertedText = this.dataFromNativeBeforeInput_;
this.dataFromNativeBeforeInput_ = null;
if (e.detail.inputValue.length === 0) {
// Input was entirely cleared (eg. backspace/delete was hit).
this.numCharsTypedInSearch_ = 0;
} else if (insertedText === e.detail.inputValue) {
// If the inserted text matches exactly with the current value of the
// input, that implies that the previous input value was cleared or
// was empty to begin with. So, reset the num chars typed and count this
// input event as 1 char typed.
this.numCharsTypedInSearch_ = 1;
} else {
this.numCharsTypedInSearch_++;
}
}
private onToolbarSearchCleared_() {
this.numCharsTypedInSearch_ = 0;
}
}
declare global {
interface HTMLElementTagNameMap {
'history-app': HistoryAppElement;
}
}
customElements.define(HistoryAppElement.is, HistoryAppElement);