blob: ca53ac99e4802d30c220a1e05b5836961814fa61 [file] [log] [blame]
// Copyright 2025 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import './icons.html.js';
import './composebox_tab_favicon.js';
import '//resources/cr_elements/icons.html.js';
import '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import '//resources/cr_elements/cr_button/cr_button.js';
import '//resources/cr_elements/cr_icon_button/cr_icon_button.js';
import '//resources/cr_elements/cr_icon/cr_icon.js';
import {AnchorAlignment} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import type {CrActionMenuElement} from '//resources/cr_elements/cr_action_menu/cr_action_menu.js';
import {I18nMixinLit} from '//resources/cr_elements/i18n_mixin_lit.js';
import {assert} from '//resources/js/assert.js';
import {loadTimeData} from '//resources/js/load_time_data.js';
import {CrLitElement} from '//resources/lit/v3_0/lit.rollup.js';
import type {TabInfo} from '//resources/mojo/components/omnibox/browser/searchbox.mojom-webui.js';
import type {UnguessableToken} from '//resources/mojo/mojo/public/mojom/base/unguessable_token.mojom-webui.js';
import {getCss} from './context_menu_entrypoint.css.js';
import {getHtml} from './context_menu_entrypoint.html.js';
/** The width of the dropdown menu in pixels. */
const MENU_WIDTH_PX = 190;
/** The string value of the tall bottom context layout mode. */
const TALL_BOTTOM_CONTEXT_LAYOUT_MODE = 'TallBottomContext';
let glyphAnimationDone = false;
export interface ContextMenuEntrypointElement {
$: {
menu: CrActionMenuElement,
};
}
const ContextMenuEntrypointElementBase = I18nMixinLit(CrLitElement);
export class ContextMenuEntrypointElement extends
ContextMenuEntrypointElementBase {
static get is() {
return 'cr-composebox-context-menu-entrypoint';
}
static override get styles() {
return getCss();
}
override render() {
return getHtml.bind(this)();
}
static override get properties() {
return {
// =========================================================================
// Public properties
// =========================================================================
inputsDisabled: {type: Boolean},
fileNum: {type: Number},
showContextMenuDescription: {type: Boolean},
inCreateImageMode: {
reflect: true,
type: Boolean,
},
hasImageFiles: {
reflect: true,
type: Boolean,
},
disabledTabIds: {type: Object},
tabSuggestions: {type: Array},
entrypointName: {type: String},
searchboxLayoutMode: {type: String},
ntpNextFeaturesEnabled: {type: Boolean, reflect: true},
// =========================================================================
// Protected properties
// =========================================================================
enableMultiTabSelection_: {
reflect: true,
type: Boolean,
},
tabPreviewUrl_: {type: String},
tabPreviewsEnabled_: {type: Boolean},
showDeepSearch_: {
reflect: true,
type: Boolean,
},
showCreateImage_: {
reflect: true,
type: Boolean,
},
};
}
accessor inputsDisabled: boolean = false;
accessor fileNum: number = 0;
accessor showContextMenuDescription: boolean = false;
accessor inCreateImageMode: boolean = false;
accessor hasImageFiles: boolean = false;
accessor disabledTabIds: Map<number, UnguessableToken> = new Map();
accessor tabSuggestions: TabInfo[] = [];
accessor entrypointName: string = '';
accessor searchboxLayoutMode: string = '';
accessor ntpNextFeaturesEnabled: boolean = false;
protected accessor enableMultiTabSelection_: boolean =
loadTimeData.getBoolean('composeboxContextMenuEnableMultiTabSelection');
protected accessor tabPreviewUrl_: string = '';
protected accessor tabPreviewsEnabled_: boolean =
loadTimeData.getBoolean('composeboxShowContextMenuTabPreviews');
protected accessor showDeepSearch_: boolean =
loadTimeData.getBoolean('composeboxShowDeepSearchButton');
protected accessor showCreateImage_: boolean =
loadTimeData.getBoolean('composeboxShowCreateImageButton');
protected maxFileCount_: number =
loadTimeData.getInteger('composeboxFileMaxCount');
constructor() {
super();
}
openMenuForMultiSelection() {
if (this.enableMultiTabSelection_ &&
this.searchboxLayoutMode !== TALL_BOTTOM_CONTEXT_LAYOUT_MODE) {
this.showMenuAtEntrypoint_();
}
}
override firstUpdated(changedProperties: Map<string|number|symbol, unknown>):
void {
super.firstUpdated(changedProperties);
if (!glyphAnimationDone) {
this.shadowRoot.querySelector<HTMLElement>('#glowWrapper')
?.classList.add('play');
glyphAnimationDone = true;
}
}
// Checks if the image upload item in the context menu should be disabled.
protected get imageUploadDisabled_(): boolean {
return this.fileNum >= this.maxFileCount_ ||
(this.inCreateImageMode && this.hasImageFiles);
}
// Checks if the file upload item in the context menu should be disabled.
protected get fileUploadDisabled_(): boolean {
return this.inCreateImageMode || this.fileNum >= this.maxFileCount_;
}
// Checks if the deep search item in the context menu should be disabled.
protected get deepSearchDisabled_(): boolean {
return this.inCreateImageMode || this.fileNum === 1 || this.fileNum > 1;
}
// Checks if the create image item in the context menu should be disabled.
protected get createImageDisabled_(): boolean {
return this.inCreateImageMode || this.fileNum > 1 ||
((this.fileNum === 1) && !this.hasImageFiles);
}
// Checks if a tab item in the context menu should be disabled.
protected isTabDisabled_(tab: TabInfo): boolean {
const noNewContextAllowed =
this.inCreateImageMode || this.fileNum >= this.maxFileCount_;
const isTabInContext = this.disabledTabIds.has(tab.tabId);
// If multi-tab selection is enabled, we only want to disable a tab if
// no more context can be added and the tab has not yet been added as
// context already. Otherwise, don't disable the tab, since we want to allow
// users to unselect the tab, and remove it from the context.
if (this.enableMultiTabSelection_) {
return noNewContextAllowed && !isTabInContext;
}
return noNewContextAllowed || isTabInContext;
}
protected onEntrypointClick_() {
if (this.entrypointName === 'Omnibox') {
const entrypoint =
this.shadowRoot.querySelector<HTMLElement>('#entrypoint');
assert(entrypoint);
this.fire('context-menu-entrypoint-click', {
x: entrypoint.getBoundingClientRect().left,
y: entrypoint.getBoundingClientRect().bottom,
});
return;
}
const metricName =
'NewTabPage.' + this.entrypointName + '.ContextMenuEntry.Clicked';
chrome.metricsPrivate.recordBoolean(metricName, true);
this.showMenuAtEntrypoint_();
}
protected onTabClick_(e: Event) {
e.stopPropagation();
const tabElement = e.currentTarget! as HTMLButtonElement;
const tabInfo = this.tabSuggestions[Number(tabElement.dataset['index'])];
assert(tabInfo);
if (this.enableMultiTabSelection_ &&
this.disabledTabIds.has(tabInfo.tabId)) {
this.deleteTabContext_(this.disabledTabIds.get(tabInfo.tabId)!);
return;
}
this.addTabContext_(tabInfo);
}
protected deleteTabContext_(uuid: UnguessableToken) {
this.fire('delete-tab-context', {uuid: uuid});
if (this.searchboxLayoutMode === TALL_BOTTOM_CONTEXT_LAYOUT_MODE) {
this.$.menu.close();
}
}
protected addTabContext_(tabInfo: TabInfo) {
this.fire('add-tab-context', {
id: tabInfo.tabId,
title: tabInfo.title,
url: tabInfo.url,
delayUpload: false,
});
if (!this.enableMultiTabSelection_ || this.entrypointName === 'Realbox' ||
this.searchboxLayoutMode === TALL_BOTTOM_CONTEXT_LAYOUT_MODE) {
this.$.menu.close();
}
}
protected onTabPointerenter_(e: Event) {
if (!this.tabPreviewsEnabled_) {
return;
}
const tabElement = e.currentTarget! as HTMLElement;
const tabInfo = this.tabSuggestions[Number(tabElement.dataset['index'])];
assert(tabInfo);
// Clear the preview URL before fetching the new one to make sure an old
// or incorrect preview doesn't show while the new one is loading.
this.tabPreviewUrl_ = '';
this.fire('get-tab-preview', {
tabId: tabInfo.tabId,
onPreviewFetched: (previewDataUrl: string) => {
this.tabPreviewUrl_ = previewDataUrl;
},
});
}
protected shouldShowTabPreview_(): boolean {
return this.tabPreviewsEnabled_ && this.tabPreviewUrl_ !== '';
}
protected openImageUpload_() {
this.fire('open-image-upload');
this.$.menu.close();
}
protected openFileUpload_() {
this.fire('open-file-upload');
this.$.menu.close();
}
protected onDeepSearchClick_() {
this.fire('deep-search-click');
this.$.menu.close();
}
protected onCreateImageClick_() {
this.fire('create-image-click');
this.$.menu.close();
}
private showMenuAtEntrypoint_() {
const entrypoint =
this.shadowRoot.querySelector<HTMLElement>('#entrypoint');
assert(entrypoint);
this.$.menu.showAt(entrypoint, {
top: entrypoint.getBoundingClientRect().bottom,
width: MENU_WIDTH_PX,
anchorAlignmentX: AnchorAlignment['AFTER_START'],
});
}
}
declare global {
interface HTMLElementTagNameMap {
'cr-composebox-context-menu-entrypoint': ContextMenuEntrypointElement;
}
}
customElements.define(
ContextMenuEntrypointElement.is, ContextMenuEntrypointElement);