595 lines
18 KiB
JavaScript
595 lines
18 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
import {
|
|
classMap,
|
|
html,
|
|
ifDefined,
|
|
styleMap,
|
|
when,
|
|
} from "chrome://global/content/vendor/lit.all.mjs";
|
|
import {
|
|
FxviewTabListBase,
|
|
FxviewTabRowBase,
|
|
} from "chrome://browser/content/firefoxview/fxview-tab-list.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://global/content/elements/moz-button.mjs";
|
|
|
|
const lazy = {};
|
|
let XPCOMUtils;
|
|
|
|
XPCOMUtils = ChromeUtils.importESModule(
|
|
"resource://gre/modules/XPCOMUtils.sys.mjs"
|
|
).XPCOMUtils;
|
|
XPCOMUtils.defineLazyPreferenceGetter(
|
|
lazy,
|
|
"virtualListEnabledPref",
|
|
"browser.firefox-view.virtual-list.enabled"
|
|
);
|
|
|
|
/**
|
|
* A list of clickable tab items
|
|
*
|
|
* @property {boolean} pinnedTabsGridView - Whether to show pinned tabs in a grid view
|
|
*/
|
|
|
|
export class OpenTabsTabList extends FxviewTabListBase {
|
|
constructor() {
|
|
super();
|
|
this.pinnedTabsGridView = false;
|
|
this.pinnedTabs = [];
|
|
this.unpinnedTabs = [];
|
|
}
|
|
|
|
static properties = {
|
|
pinnedTabsGridView: { type: Boolean },
|
|
};
|
|
|
|
static queries = {
|
|
...FxviewTabListBase.queries,
|
|
rowEls: {
|
|
all: "opentabs-tab-row",
|
|
},
|
|
};
|
|
|
|
willUpdate(changes) {
|
|
this.activeIndex = Math.min(
|
|
Math.max(this.activeIndex, 0),
|
|
this.tabItems.length - 1
|
|
);
|
|
|
|
if (changes.has("dateTimeFormat") || changes.has("updatesPaused")) {
|
|
this.clearIntervalTimer();
|
|
if (!this.updatesPaused && this.dateTimeFormat == "relative") {
|
|
this.startIntervalTimer();
|
|
this.onIntervalUpdate();
|
|
}
|
|
}
|
|
|
|
// Move pinned tabs to the beginning of the list
|
|
if (this.pinnedTabsGridView) {
|
|
// Can set maxTabsLength to -1 to have no max
|
|
this.unpinnedTabs = this.tabItems.filter(
|
|
tab => !tab.indicators.includes("pinned")
|
|
);
|
|
this.pinnedTabs = this.tabItems.filter(tab =>
|
|
tab.indicators.includes("pinned")
|
|
);
|
|
if (this.maxTabsLength > 0) {
|
|
this.unpinnedTabs = this.unpinnedTabs.slice(0, this.maxTabsLength);
|
|
}
|
|
this.tabItems = [...this.pinnedTabs, ...this.unpinnedTabs];
|
|
} else if (this.maxTabsLength > 0) {
|
|
this.tabItems = this.tabItems.slice(0, this.maxTabsLength);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Focuses the expected element (either the link or button) within fxview-tab-row
|
|
* The currently focused/active element ID within a row is stored in this.currentActiveElementId
|
|
*/
|
|
handleFocusElementInRow(e) {
|
|
let fxviewTabRow = e.target;
|
|
if (e.code == "ArrowUp") {
|
|
// Focus either the link or button of the previous row based on this.currentActiveElementId
|
|
e.preventDefault();
|
|
if (
|
|
(this.pinnedTabsGridView &&
|
|
this.activeIndex >= this.pinnedTabs.length) ||
|
|
!this.pinnedTabsGridView
|
|
) {
|
|
this.focusPrevRow();
|
|
}
|
|
} else if (e.code == "ArrowDown") {
|
|
// Focus either the link or button of the next row based on this.currentActiveElementId
|
|
e.preventDefault();
|
|
if (
|
|
this.pinnedTabsGridView &&
|
|
this.activeIndex < this.pinnedTabs.length
|
|
) {
|
|
this.focusIndex(this.pinnedTabs.length);
|
|
} else {
|
|
this.focusNextRow();
|
|
}
|
|
} else if (e.code == "ArrowRight") {
|
|
// Focus either the link or the button in the current row and
|
|
// set this.currentActiveElementId to that element's ID
|
|
e.preventDefault();
|
|
if (document.dir == "rtl") {
|
|
fxviewTabRow.moveFocusLeft();
|
|
} else {
|
|
fxviewTabRow.moveFocusRight();
|
|
}
|
|
} else if (e.code == "ArrowLeft") {
|
|
// Focus either the link or the button in the current row and
|
|
// set this.currentActiveElementId to that element's ID
|
|
e.preventDefault();
|
|
if (document.dir == "rtl") {
|
|
fxviewTabRow.moveFocusRight();
|
|
} else {
|
|
fxviewTabRow.moveFocusLeft();
|
|
}
|
|
}
|
|
}
|
|
|
|
async focusIndex(index) {
|
|
// Focus link or button of item
|
|
if (
|
|
((this.pinnedTabsGridView && index > this.pinnedTabs.length) ||
|
|
!this.pinnedTabsGridView) &&
|
|
lazy.virtualListEnabledPref
|
|
) {
|
|
let row = this.rootVirtualListEl.getItem(index - this.pinnedTabs.length);
|
|
if (!row) {
|
|
return;
|
|
}
|
|
let subList = this.rootVirtualListEl.getSubListForItem(
|
|
index - this.pinnedTabs.length
|
|
);
|
|
if (!subList) {
|
|
return;
|
|
}
|
|
this.activeIndex = index;
|
|
|
|
// In Bug 1866845, these manual updates to the sublists should be removed
|
|
// and scrollIntoView() should also be iterated on so that we aren't constantly
|
|
// moving the focused item to the center of the viewport
|
|
for (const sublist of Array.from(this.rootVirtualListEl.children)) {
|
|
await sublist.requestUpdate();
|
|
await sublist.updateComplete;
|
|
}
|
|
row.scrollIntoView({ block: "center" });
|
|
row.focus();
|
|
} else if (index >= 0 && index < this.rowEls?.length) {
|
|
this.rowEls[index].focus();
|
|
this.activeIndex = index;
|
|
}
|
|
}
|
|
|
|
#getTabListWrapperClasses() {
|
|
let wrapperClasses = ["fxview-tab-list"];
|
|
let tabsToCheck = this.pinnedTabsGridView
|
|
? this.unpinnedTabs
|
|
: this.tabItems;
|
|
if (tabsToCheck.some(tab => tab.containerObj)) {
|
|
wrapperClasses.push(`hasContainerTab`);
|
|
}
|
|
return wrapperClasses;
|
|
}
|
|
|
|
itemTemplate = (tabItem, i) => {
|
|
let time;
|
|
if (tabItem.time || tabItem.closedAt) {
|
|
let stringTime = (tabItem.time || tabItem.closedAt).toString();
|
|
// Different APIs return time in different units, so we use
|
|
// the length to decide if it's milliseconds or nanoseconds.
|
|
if (stringTime.length === 16) {
|
|
time = (tabItem.time || tabItem.closedAt) / 1000;
|
|
} else {
|
|
time = tabItem.time || tabItem.closedAt;
|
|
}
|
|
}
|
|
|
|
return html`<opentabs-tab-row
|
|
?active=${i == this.activeIndex}
|
|
class=${classMap({
|
|
pinned:
|
|
this.pinnedTabsGridView && tabItem.indicators?.includes("pinned"),
|
|
})}
|
|
.currentActiveElementId=${this.currentActiveElementId}
|
|
.favicon=${tabItem.icon}
|
|
.compact=${this.compactRows}
|
|
.containerObj=${ifDefined(tabItem.containerObj)}
|
|
.indicators=${tabItem.indicators}
|
|
.pinnedTabsGridView=${ifDefined(this.pinnedTabsGridView)}
|
|
.primaryL10nId=${tabItem.primaryL10nId}
|
|
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
|
|
.secondaryL10nId=${tabItem.secondaryL10nId}
|
|
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
|
|
.tertiaryL10nId=${ifDefined(tabItem.tertiaryL10nId)}
|
|
.tertiaryL10nArgs=${ifDefined(tabItem.tertiaryL10nArgs)}
|
|
.secondaryActionClass=${this.secondaryActionClass}
|
|
.tertiaryActionClass=${ifDefined(this.tertiaryActionClass)}
|
|
.sourceClosedId=${ifDefined(tabItem.sourceClosedId)}
|
|
.sourceWindowId=${ifDefined(tabItem.sourceWindowId)}
|
|
.closedId=${ifDefined(tabItem.closedId || tabItem.closedId)}
|
|
role=${tabItem.pinned && this.pinnedTabsGridView ? "tab" : "listitem"}
|
|
.tabElement=${ifDefined(tabItem.tabElement)}
|
|
.time=${ifDefined(time)}
|
|
.title=${tabItem.title}
|
|
.url=${tabItem.url}
|
|
.searchQuery=${ifDefined(this.searchQuery)}
|
|
.timeMsPref=${ifDefined(this.timeMsPref)}
|
|
.hasPopup=${this.hasPopup}
|
|
.dateTimeFormat=${this.dateTimeFormat}
|
|
></opentabs-tab-row>`;
|
|
};
|
|
|
|
render() {
|
|
if (this.searchQuery && this.tabItems.length === 0) {
|
|
return this.emptySearchResultsTemplate();
|
|
}
|
|
return html`
|
|
${this.stylesheets()}
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/firefoxview/opentabs-tab-list.css"
|
|
/>
|
|
${when(
|
|
this.pinnedTabsGridView && this.pinnedTabs.length,
|
|
() => html`
|
|
<div
|
|
id="fxview-tab-list"
|
|
class="fxview-tab-list pinned"
|
|
data-l10n-id="firefoxview-pinned-tabs"
|
|
role="tablist"
|
|
@keydown=${this.handleFocusElementInRow}
|
|
>
|
|
${this.pinnedTabs.map((tabItem, i) =>
|
|
this.customItemTemplate
|
|
? this.customItemTemplate(tabItem, i)
|
|
: this.itemTemplate(tabItem, i)
|
|
)}
|
|
</div>
|
|
`
|
|
)}
|
|
<div
|
|
id="fxview-tab-list"
|
|
class=${this.#getTabListWrapperClasses().join(" ")}
|
|
data-l10n-id="firefoxview-tabs"
|
|
role="list"
|
|
@keydown=${this.handleFocusElementInRow}
|
|
>
|
|
${when(
|
|
lazy.virtualListEnabledPref,
|
|
() => html`
|
|
<virtual-list
|
|
.activeIndex=${this.activeIndex}
|
|
.pinnedTabsIndexOffset=${this.pinnedTabsGridView
|
|
? this.pinnedTabs.length
|
|
: 0}
|
|
.items=${this.pinnedTabsGridView
|
|
? this.unpinnedTabs
|
|
: this.tabItems}
|
|
.template=${this.itemTemplate}
|
|
></virtual-list>
|
|
`,
|
|
() =>
|
|
html`${this.tabItems.map((tabItem, i) =>
|
|
this.itemTemplate(tabItem, i)
|
|
)}`
|
|
)}
|
|
</div>
|
|
<slot name="menu"></slot>
|
|
`;
|
|
}
|
|
}
|
|
customElements.define("opentabs-tab-list", OpenTabsTabList);
|
|
|
|
/**
|
|
* A tab item that displays favicon, title, url, and time of last access
|
|
*
|
|
* @property {object} containerObj - Info about an open tab's container if within one
|
|
* @property {string} indicators - An array of tab indicators if any are present
|
|
* @property {boolean} pinnedTabsGridView - Whether the show pinned tabs in a grid view
|
|
*/
|
|
|
|
export class OpenTabsTabRow extends FxviewTabRowBase {
|
|
constructor() {
|
|
super();
|
|
this.indicators = [];
|
|
this.pinnedTabsGridView = false;
|
|
}
|
|
|
|
static properties = {
|
|
...FxviewTabRowBase.properties,
|
|
containerObj: { type: Object },
|
|
indicators: { type: Array },
|
|
pinnedTabsGridView: { type: Boolean },
|
|
};
|
|
|
|
static queries = {
|
|
...FxviewTabRowBase.queries,
|
|
mediaButtonEl: "#fxview-tab-row-media-button",
|
|
pinnedTabButtonEl: "moz-button#fxview-tab-row-main",
|
|
};
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
this.addEventListener("keydown", this.handleKeydown);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
this.removeEventListener("keydown", this.handleKeydown);
|
|
}
|
|
|
|
handleKeydown(e) {
|
|
if (
|
|
this.active &&
|
|
this.pinnedTabsGridView &&
|
|
this.indicators?.includes("pinned") &&
|
|
e.key === "m" &&
|
|
e.ctrlKey
|
|
) {
|
|
this.muteOrUnmuteTab();
|
|
}
|
|
}
|
|
|
|
moveFocusRight() {
|
|
let tabList = this.getRootNode().host;
|
|
if (this.pinnedTabsGridView && this.indicators?.includes("pinned")) {
|
|
tabList.focusNextRow();
|
|
} else if (
|
|
(this.indicators?.includes("soundplaying") ||
|
|
this.indicators?.includes("muted")) &&
|
|
this.currentActiveElementId === "fxview-tab-row-main"
|
|
) {
|
|
this.focusMediaButton();
|
|
} else if (
|
|
this.currentActiveElementId === "fxview-tab-row-media-button" ||
|
|
this.currentActiveElementId === "fxview-tab-row-main"
|
|
) {
|
|
this.focusSecondaryButton();
|
|
} else if (
|
|
this.tertiaryButtonEl &&
|
|
this.currentActiveElementId === "fxview-tab-row-secondary-button"
|
|
) {
|
|
this.focusTertiaryButton();
|
|
}
|
|
}
|
|
|
|
moveFocusLeft() {
|
|
let tabList = this.getRootNode().host;
|
|
if (
|
|
this.pinnedTabsGridView &&
|
|
(this.indicators?.includes("pinned") ||
|
|
(tabList.currentActiveElementId === "fxview-tab-row-main" &&
|
|
tabList.activeIndex === tabList.pinnedTabs.length))
|
|
) {
|
|
tabList.focusPrevRow();
|
|
} else if (
|
|
tabList.currentActiveElementId === "fxview-tab-row-tertiary-button"
|
|
) {
|
|
this.focusSecondaryButton();
|
|
} else if (
|
|
(this.indicators?.includes("soundplaying") ||
|
|
this.indicators?.includes("muted")) &&
|
|
tabList.currentActiveElementId === "fxview-tab-row-secondary-button"
|
|
) {
|
|
this.focusMediaButton();
|
|
} else {
|
|
this.focusLink();
|
|
}
|
|
}
|
|
|
|
focusMediaButton() {
|
|
let tabList = this.getRootNode().host;
|
|
this.mediaButtonEl.focus();
|
|
tabList.currentActiveElementId = this.mediaButtonEl.id;
|
|
}
|
|
|
|
#secondaryActionHandler(event) {
|
|
if (
|
|
(this.pinnedTabsGridView &&
|
|
this.indicators?.includes("pinned") &&
|
|
event.type == "contextmenu") ||
|
|
(event.type == "click" && event.detail && !event.altKey) ||
|
|
// detail=0 is from keyboard
|
|
(event.type == "click" && !event.detail)
|
|
) {
|
|
event.preventDefault();
|
|
this.dispatchEvent(
|
|
new CustomEvent("fxview-tab-list-secondary-action", {
|
|
bubbles: true,
|
|
composed: true,
|
|
detail: { originalEvent: event, item: this },
|
|
})
|
|
);
|
|
}
|
|
}
|
|
|
|
#faviconTemplate() {
|
|
return html`<span
|
|
class=${classMap({
|
|
"fxview-tab-row-favicon-wrapper": true,
|
|
pinned: this.indicators?.includes("pinned"),
|
|
pinnedOnNewTab: this.indicators?.includes("pinnedOnNewTab"),
|
|
attention: this.indicators?.includes("attention"),
|
|
bookmark: this.indicators?.includes("bookmark"),
|
|
})}
|
|
>
|
|
<span
|
|
class="fxview-tab-row-favicon icon"
|
|
id="fxview-tab-row-favicon"
|
|
style=${styleMap({
|
|
backgroundImage: `url(${this.getImageUrl(this.favicon, this.url)})`,
|
|
})}
|
|
></span>
|
|
${when(
|
|
this.pinnedTabsGridView &&
|
|
this.indicators?.includes("pinned") &&
|
|
(this.indicators?.includes("muted") ||
|
|
this.indicators?.includes("soundplaying")),
|
|
() => html`
|
|
<button
|
|
class="fxview-tab-row-pinned-media-button"
|
|
id="fxview-tab-row-media-button"
|
|
tabindex="-1"
|
|
data-l10n-id=${this.indicators?.includes("muted")
|
|
? "fxviewtabrow-unmute-tab-button-no-context"
|
|
: "fxviewtabrow-mute-tab-button-no-context"}
|
|
muted=${this.indicators?.includes("muted")}
|
|
soundplaying=${this.indicators?.includes("soundplaying") &&
|
|
!this.indicators?.includes("muted")}
|
|
@click=${this.muteOrUnmuteTab}
|
|
></button>
|
|
`
|
|
)}
|
|
</span>`;
|
|
}
|
|
|
|
#getContainerClasses() {
|
|
let containerClasses = ["fxview-tab-row-container-indicator", "icon"];
|
|
if (this.containerObj) {
|
|
let { icon, color } = this.containerObj;
|
|
containerClasses.push(`identity-icon-${icon}`);
|
|
containerClasses.push(`identity-color-${color}`);
|
|
}
|
|
return containerClasses;
|
|
}
|
|
|
|
muteOrUnmuteTab(e) {
|
|
e?.preventDefault();
|
|
// If the tab has no sound playing, the mute/unmute button will be removed when toggled.
|
|
// We should move the focus to the right in that case. This does not apply to pinned tabs
|
|
// on the Open Tabs page.
|
|
let shouldMoveFocus =
|
|
(!this.pinnedTabsGridView ||
|
|
(!this.indicators.includes("pinned") && this.pinnedTabsGridView)) &&
|
|
this.mediaButtonEl &&
|
|
!this.indicators.includes("soundplaying") &&
|
|
this.currentActiveElementId === "fxview-tab-row-media-button";
|
|
|
|
// detail=0 is from keyboard
|
|
if (e?.type == "click" && !e?.detail && shouldMoveFocus) {
|
|
if (document.dir == "rtl") {
|
|
this.moveFocusLeft();
|
|
} else {
|
|
this.moveFocusRight();
|
|
}
|
|
}
|
|
this.tabElement.toggleMuteAudio();
|
|
}
|
|
|
|
#mediaButtonTemplate() {
|
|
return html`${when(
|
|
this.indicators?.includes("soundplaying") ||
|
|
this.indicators?.includes("muted"),
|
|
() =>
|
|
html`<moz-button
|
|
type="icon ghost"
|
|
class="fxview-tab-row-button"
|
|
id="fxview-tab-row-media-button"
|
|
data-l10n-id=${this.indicators?.includes("muted")
|
|
? "fxviewtabrow-unmute-tab-button-no-context"
|
|
: "fxviewtabrow-mute-tab-button-no-context"}
|
|
muted=${this.indicators?.includes("muted")}
|
|
soundplaying=${this.indicators?.includes("soundplaying") &&
|
|
!this.indicators?.includes("muted")}
|
|
@click=${this.muteOrUnmuteTab}
|
|
tabindex=${this.active &&
|
|
this.currentActiveElementId === "fxview-tab-row-media-button"
|
|
? "0"
|
|
: "-1"}
|
|
></moz-button>`,
|
|
() => html`<span></span>`
|
|
)}`;
|
|
}
|
|
|
|
#containerIndicatorTemplate() {
|
|
let tabList = this.getRootNode().host;
|
|
let tabsToCheck = tabList.pinnedTabsGridView
|
|
? tabList.unpinnedTabs
|
|
: tabList.tabItems;
|
|
return html`${when(
|
|
tabsToCheck.some(tab => tab.containerObj),
|
|
() => html`<span class=${this.#getContainerClasses().join(" ")}></span>`
|
|
)}`;
|
|
}
|
|
|
|
#pinnedTabItemTemplate() {
|
|
return html`
|
|
<moz-button
|
|
type="icon ghost"
|
|
id="fxview-tab-row-main"
|
|
aria-haspopup=${ifDefined(this.hasPopup)}
|
|
data-l10n-id=${ifDefined(this.primaryL10nId)}
|
|
data-l10n-args=${ifDefined(this.primaryL10nArgs)}
|
|
tabindex=${this.active &&
|
|
this.currentActiveElementId === "fxview-tab-row-main"
|
|
? "0"
|
|
: "-1"}
|
|
role="tab"
|
|
@click=${this.primaryActionHandler}
|
|
@keydown=${this.primaryActionHandler}
|
|
@contextmenu=${this.#secondaryActionHandler}
|
|
>
|
|
${this.#faviconTemplate()}
|
|
</moz-button>
|
|
`;
|
|
}
|
|
|
|
#unpinnedTabItemTemplate() {
|
|
return html`<a
|
|
href=${ifDefined(this.url)}
|
|
class="fxview-tab-row-main"
|
|
id="fxview-tab-row-main"
|
|
tabindex=${this.active &&
|
|
this.currentActiveElementId === "fxview-tab-row-main"
|
|
? "0"
|
|
: "-1"}
|
|
data-l10n-id=${ifDefined(this.primaryL10nId)}
|
|
data-l10n-args=${ifDefined(this.primaryL10nArgs)}
|
|
@click=${this.primaryActionHandler}
|
|
@keydown=${this.primaryActionHandler}
|
|
title=${!this.primaryL10nId ? this.url : null}
|
|
>
|
|
${this.#faviconTemplate()} ${this.titleTemplate()}
|
|
${when(
|
|
!this.compact,
|
|
() =>
|
|
html`${this.#containerIndicatorTemplate()} ${this.urlTemplate()}
|
|
${this.dateTemplate()} ${this.timeTemplate()}`
|
|
)}
|
|
</a>
|
|
${this.#mediaButtonTemplate()} ${this.secondaryButtonTemplate()}
|
|
${this.tertiaryButtonTemplate()}`;
|
|
}
|
|
|
|
render() {
|
|
return html`
|
|
${this.stylesheets()}
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/firefoxview/opentabs-tab-row.css"
|
|
/>
|
|
${when(
|
|
this.containerObj,
|
|
() => html`
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/usercontext/usercontext.css"
|
|
/>
|
|
`
|
|
)}
|
|
${when(
|
|
this.pinnedTabsGridView && this.indicators?.includes("pinned"),
|
|
this.#pinnedTabItemTemplate.bind(this),
|
|
this.#unpinnedTabItemTemplate.bind(this)
|
|
)}
|
|
`;
|
|
}
|
|
}
|
|
customElements.define("opentabs-tab-row", OpenTabsTabRow);
|