Spracherkennung für: .mjs vermutete Sprache: Unknown {[0] [0] [0]} [Methode: Schwerpunktbildung, einfache Gewichte, sechs Dimensionen]
/* 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 { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
import {
SidebarTabList,
SidebarTabRow,
} from "chrome://browser/content/sidebar/sidebar-tab-list.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
PlacesUIUtils: "moz-src:///browser/components/places/PlacesUIUtils.sys.mjs",
PlacesUtils: "resource://gre/modules/PlacesUtils.sys.mjs",
});
const TAB_DROP_TYPE = "application/x-moz-tabbrowser-tab";
const DROP_BEFORE = -
1;
const DROP_ON =
0;
const DROP_AFTER =
1;
// Matches the legacy bookmarks sidebar tree's drag-hover-to-expand delay,
// which uses LookAndFeel::IntID::TreeOpenDelay (
1000ms on all platforms).
const DRAG_HOVER_EXPAND_DELAY_MS =
1000;
let activeDropList = null;
export class SidebarBookmarkList extends SidebarTabList {
static properties = {
...SidebarTabList.properties,
expandedFolderGuids: { type: Object },
readOnly: { type: Boolean },
};
#draggedGuid = null;
#dropTarget = null;
#hoverFolderGuid = null;
#hoverFolderTimer = null;
constructor() {
super();
this.bookmarksContext = true;
this.expandedFolderGuids = new Set();
// True when this list shows the contents of a fixed-sort query folder
// (e.g. Recently Bookmarked or a tag under Recent Tags). Items in such a
// folder can't be reordered, so dragging from or dropping into the list is
// disabled, matching the legacy bookmarks sidebar.
this.readOnly = false;
this.getItemHeight = (item, h) => this.#itemHeightGetter(item, h);
}
#containingDetails = null;
#onContainingDetailsToggle = () => {
if (this.#containingDetails?.open) {
// The inner <virtual-list> was first observed by its IntersectionObserver
// while the containing <details> had no layout box, so it never flipped
// its `isVisible` flag and skipped rendering rows. Re-observe now that
// the details has opened and a layout box exists.
this.shadowRoot
?.querySelector("virtual-list")
?.triggerIntersectionObserver();
} else {
this.treeView.clearSelectionForList(this);
}
};
connectedCallback() {
super.connectedCallback();
this.#containingDetails = this.closest("details");
this.#containingDetails?.addEventListener(
"toggle",
this.#onContainingDetailsToggle
);
}
disconnectedCallback() {
super.disconnectedCallback();
this.#containingDetails?.removeEventListener(
"toggle",
this.#onContainingDetailsToggle
);
this.#containingDetails = null;
this.#clearHoverFolderExpand();
}
/**
* Find the rendered nested `<sidebar-bookmark-list>` for a folder guid.
* Returns null if the folder isn't currently rendered (e.g. virtual-list
* has it scrolled out of view).
*
* @param {string} guid
* @returns {SidebarBookmarkList}
*/
findSublistForGuid(guid) {
const folder = this.shadowRoot.querySelector(
`summary[data-guid="${CSS.escape(guid)}"]`
);
return folder?.parentElement?.querySelector("sidebar-bookmark-list");
}
willUpdate(changes) {
super.willUpdate(changes);
if (changes.has("expandedFolderGuids")) {
// Reassign to a new function reference so Lit detects the change and
// propagates it to sublists, causing them to reset their cached heights.
this.getItemHeight = (item, h) => this.#itemHeightGetter(item, h);
}
}
#itemHeightGetter = (item, defaultHeight) => {
if (!item.children || !this.expandedFolderGuids.has(item.guid)) {
return defaultHeight;
}
return item.children.reduce(
(sum, child) => sum + this.#itemHeightGetter(child, defaultHeight),
defaultHeight
);
};
static queries = {
...SidebarTabList.queries,
rowEls: { all: "sidebar-bookmark-row" },
folderEls: { all: "details" },
folderLabelEl: ".bookmark-folder-label",
};
itemTemplate = (tabItem, i) => {
let tabIndex = -
1;
if ((this.searchQuery || this.sortOption == "lastvisited") && i ==
0) {
tabIndex =
0;
} else if (!this.searchQuery) {
tabIndex =
0;
}
if (!tabItem.url && !tabItem.children) {
return html`<div
class="bookmark-separator"
draggable="true"
role="separator"
tabindex="
0"
data-guid=${tabItem.guid}
.guid=${tabItem.guid}
></div>`;
}
if (tabItem.children !== undefined) {
let folderKind = null;
if (tabItem.isTagsRoot) {
folderKind = "tags-root";
} else if (tabItem.isTagContainer) {
folderKind = "tag-container";
} else if (tabItem.isPlaceContainer) {
folderKind = "place-container";
}
if (!tabItem.children.length) {
return html`<div
class="bookmark-folder-label"
data-folder-kind=${ifDefined(folderKind)}
tabindex="
0"
draggable="true"
data-guid=${tabItem.guid}
@auxclick=${e => this.#onFolderAuxClick(e, tabItem.guid)}
.guid=${tabItem.guid}
>
${tabItem.title}
</div>`;
}
return html`
<details
?open=${this.expandedFolderGuids.has(tabItem.guid)}
@toggle=${e => this.#onFolderToggle(e, tabItem.guid)}
data-folder-kind=${ifDefined(folderKind)}
.guid=${tabItem.guid}
>
<summary
draggable="true"
part="summary"
data-guid=${tabItem.guid}
@auxclick=${e => this.#onFolderAuxClick(e, tabItem.guid)}
>
${tabItem.title}
</summary>
<div id="content">
<sidebar-bookmark-list
maxTabsLength="-
1"
secondaryActionClass="delete-button"
.tabItems=${tabItem.children}
.expandedFolderGuids=${this.expandedFolderGuids}
.readOnly=${this.readOnly || !!tabItem.isPlaceContainer}
@fxview-tab-list-primary-action=${this.onPrimaryAction}
@fxview-tab-list-secondary-action=${this.onSecondaryAction}
>
</sidebar-bookmark-list>
</div>
</details>
`;
}
return html`
<sidebar-bookmark-row
?active=${i == this.activeIndex}
draggable="true"
.canClose=${ifDefined(tabItem.canClose)}
.closedId=${ifDefined(tabItem.closedId)}
compact
.currentActiveElementId=${this.currentActiveElementId}
.closeRequested=${tabItem.closeRequested}
.favicon=${tabItem.icon}
.guid=${ifDefined(tabItem.guid)}
.hasPopup=${this.hasPopup}
.indicators=${tabItem.indicators}
.primaryL10nArgs=${ifDefined(tabItem.primaryL10nArgs)}
.primaryL10nId=${tabItem.primaryL10nId}
role="listitem"
.searchQuery=${ifDefined(this.searchQuery)}
.secondaryActionClass=${ifDefined(
this.secondaryActionClass ?? tabItem.secondaryActionClass
)}
.secondaryL10nArgs=${ifDefined(tabItem.secondaryL10nArgs)}
.secondaryL10nId=${tabItem.secondaryL10nId}
.selected=${this.isTabItemSelected(tabItem)}
.tabElement=${ifDefined(tabItem.tabElement)}
tabindex=${tabIndex}
.title=${tabItem.title}
.url=${tabItem.url}
@keydown=${e => e.currentTarget.primaryActionHandler(e)}
></sidebar-bookmark-row>
`;
};
stylesheets() {
return [
super.stylesheets(),
html`<link
rel="stylesheet"
href="chrome://browser/content/sidebar/sidebar-bookmark-list.css"
/>`,
];
}
render() {
if (this.searchQuery && !this.tabItems.length) {
return this.emptySearchResultsTemplate();
}
return html`
${this.stylesheets()}
<div
id="fxview-tab-list"
class="fxview-tab-list"
role="list"
@keydown=${this.handleFocusElementInRow}
@dragstart=${this.#onDragStart}
@dragover=${this.#onDragOver}
@dragleave=${this.#onDragLeave}
@drop=${this.#onDrop}
@dragend=${this.#onDragEnd}
>
<div class="drag-indicator"></div>
<virtual-list
.activeIndex=${this.activeIndex}
.items=${this.tabItems}
.template=${this.itemTemplate}
.getItemHeight=${this.getItemHeight}
></virtual-list>
</div>
<slot name="menu"></slot>
`;
}
#onFolderToggle(e, guid) {
this.dispatchEvent(
new CustomEvent("bookmark-folder-toggle", {
bubbles: true,
composed: true,
detail: { guid, open: e.target.open },
})
);
}
#onFolderAuxClick(e, guid) {
if (e.button !==
1) {
return;
}
e.preventDefault();
this.dispatchEvent(
new CustomEvent("bookmark-folder-middleclick", {
bubbles: true,
composed: true,
detail: { guid, isFolder: true },
})
);
}
#findBookmarkElement(composedPath) {
for (const el of composedPath) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (el.localName === "sidebar-bookmark-row" && el.guid) {
return { guid: el.guid, url: el.url, title: el.title };
}
if (el.localName === "summary") {
const details = el.parentElement;
if (details?.guid) {
return {
guid: details.guid,
title: el.textContent.trim(),
isFolder: true,
};
}
}
if (el.classList?.contains("bookmark-folder-label") && el.guid) {
return { guid: el.guid, title: el.textContent.trim(), isFolder: true };
}
if (el.classList?.contains("bookmark-separator") && el.guid) {
return { guid: el.guid, isSeparator: true };
}
}
return null;
}
#findDropTarget(composedPath, clientY) {
for (const el of composedPath) {
if (!el || el.nodeType !== Node.ELEMENT_NODE) {
continue;
}
if (el.localName === "sidebar-bookmark-row" && el.guid) {
const rect = el.getBoundingClientRect();
const orientation =
clientY < rect.top + rect.height /
2 ? DROP_BEFORE : DROP_AFTER;
return { element: el, guid: el.guid, orientation };
}
if (el.localName === "summary") {
const details = el.parentElement;
if (details?.guid) {
const rect = el.getBoundingClientRect();
const relY = (clientY - rect.top) / rect.height;
let orientation;
if (relY <
0.
25) {
orientation = DROP_BEFORE;
} else if (relY >
0.
75) {
orientation = DROP_AFTER;
} else {
orientation = DROP_ON;
}
return {
element: details,
guid: details.guid,
orientation,
isFolder: true,
};
}
}
if (el.classList?.contains("bookmark-folder-label") && el.guid) {
const rect = el.getBoundingClientRect();
const relY = (clientY - rect.top) / rect.height;
let orientation;
if (relY <
0.
25) {
orientation = DROP_BEFORE;
} else if (relY >
0.
75) {
orientation = DROP_AFTER;
} else {
orientation = DROP_ON;
}
return { element: el, guid: el.guid, orientation, isFolder: true };
}
if (el.classList?.contains("bookmark-separator") && el.guid) {
const rect = el.getBoundingClientRect();
const orientation =
clientY < rect.top + rect.height /
2 ? DROP_BEFORE : DROP_AFTER;
return { element: el, guid: el.guid, orientation };
}
if (el.id === "fxview-tab-list") {
break;
}
}
return null;
}
#getSupportedFlavor(dataTransfer) {
const types = [...dataTransfer.types];
for (const flavor of lazy.PlacesUIUtils.SUPPORTED_FLAVORS) {
if (types.includes(flavor)) {
return flavor;
}
}
return null;
}
#showDropIndicator(target) {
if (activeDropList && activeDropList !== this) {
const previousList = activeDropList;
previousList.#cleanupIndicator();
previousList.#dropTarget = null;
}
activeDropList = this;
const listEl = this.shadowRoot?.querySelector("#fxview-tab-list");
if (!listEl) {
return;
}
const indicator = listEl.querySelector(".drag-indicator");
if (!indicator) {
return;
}
if (target.orientation === DROP_ON) {
target.element.setAttribute("drag-over", "");
indicator.classList.remove("visible");
return;
}
const listRect = listEl.getBoundingClientRect();
const itemRect = target.element.getBoundingClientRect();
const indicatorTop =
itemRect.top -
listRect.top +
(target.orientation === DROP_AFTER ? itemRect.height :
0);
indicator.style.top = `${indicatorTop}px`;
indicator.classList.add("visible");
}
#cleanupIndicator() {
if (this.#dropTarget?.orientation === DROP_ON) {
this.#dropTarget.element.removeAttribute("drag-over");
}
const listEl = this.shadowRoot?.querySelector("#fxview-tab-list");
listEl?.querySelector(".drag-indicator")?.classList.remove("visible");
if (activeDropList === this) {
activeDropList = null;
}
this.#clearHoverFolderExpand();
}
// Arms a timer that expands a collapsed folder when the user dwells over it
// during a drag, so they can drop into nested folders without first opening
// them by hand. Only non-empty folders (rendered as <details>) participate.
#scheduleHoverFolderExpand(target) {
const shouldArm =
target?.isFolder &&
target.orientation === DROP_ON &&
target.element.localName === "details" &&
!target.element.open;
if (!shouldArm) {
this.#clearHoverFolderExpand();
return;
}
if (this.#hoverFolderGuid === target.guid) {
return;
}
this.#clearHoverFolderExpand();
this.#hoverFolderGuid = target.guid;
const detailsEl = target.element;
this.#hoverFolderTimer = setTimeout(() => {
this.#hoverFolderTimer = null;
this.#hoverFolderGuid = null;
if (detailsEl.isConnected && !detailsEl.open) {
detailsEl.open = true;
}
}, DRAG_HOVER_EXPAND_DELAY_MS);
}
#clearHoverFolderExpand() {
if (this.#hoverFolderTimer) {
clearTimeout(this.#hoverFolderTimer);
this.#hoverFolderTimer = null;
}
this.#hoverFolderGuid = null;
}
#onDragStart(e) {
if (this.readOnly) {
e.preventDefault();
e.stopPropagation();
return;
}
const item = this.#findBookmarkElement(e.composedPath());
if (!item) {
e.preventDefault();
return;
}
this.#draggedGuid = item.guid;
let data;
if (item.isSeparator) {
data = JSON.stringify({
type: lazy.PlacesUtils.TYPE_X_MOZ_PLACE_SEPARATOR,
itemGuid: item.guid,
instanceId: lazy.PlacesUtils.instanceId,
});
} else if (item.isFolder) {
data = JSON.stringify({
type: lazy.PlacesUtils.TYPE_X_MOZ_PLACE_CONTAINER,
itemGuid: item.guid,
guid: item.guid,
instanceId: lazy.PlacesUtils.instanceId,
title: item.title,
});
} else {
data = JSON.stringify({
type: lazy.PlacesUtils.TYPE_X_MOZ_PLACE,
itemGuid: item.guid,
guid: item.guid,
instanceId: lazy.PlacesUtils.instanceId,
title: item.title,
uri: item.url,
});
}
e.dataTransfer.clearData();
e.dataTransfer.setData(lazy.PlacesUtils.TYPE_X_MOZ_PLACE, data);
if (item.url) {
e.dataTransfer.setData(
lazy.PlacesUtils.TYPE_X_MOZ_URL,
item.url + "\n" + item.title
);
e.dataTransfer.setData(lazy.PlacesUtils.TYPE_PLAINTEXT, item.url);
}
e.dataTransfer.effectAllowed = "copyMove";
e.stopPropagation();
}
#onDragOver(e) {
e.stopPropagation();
if (this.readOnly) {
this.#cleanupIndicator();
this.#dropTarget = null;
return;
}
const flavor = this.#getSupportedFlavor(e.dataTransfer);
if (!flavor) {
return;
}
let target = this.#findDropTarget(e.composedPath(), e.clientY);
if (!target) {
target = this.#getFolderDropTarget();
}
if (
!target ||
target.guid === this.#draggedGuid ||
this.#isFixedSortFolderTarget(target)
) {
this.#cleanupIndicator();
this.#dropTarget = null;
return;
}
e.preventDefault();
if (
this.#dropTarget?.orientation === DROP_ON &&
(this.#dropTarget.element !== target.element ||
target.orientation !== DROP_ON)
) {
this.#dropTarget.element.removeAttribute("drag-over");
}
this.#showDropIndicator(target);
this.#dropTarget = target;
this.#scheduleHoverFolderExpand(target);
e.dataTransfer.dropEffect = lazy.PlacesUIUtils.PLACES_FLAVORS.includes(
flavor
)
? "move"
: "copy";
}
#getFolderDropTarget() {
const parentDetails = this.closest("details");
if (parentDetails?.guid) {
return {
element: parentDetails,
guid: parentDetails.guid,
orientation: DROP_ON,
isFolder: true,
};
}
return null;
}
// A query folder (Recently Bookmarked, Recent Tags, or a tag container) has
// a forced sort order, so we can't drop into it. Such folders are the only
// ones tagged with `data-folder-kind`.
#isFixedSortFolderTarget(target) {
return (
target.isFolder &&
target.orientation === DROP_ON &&
!!target.element?.dataset?.folderKind
);
}
#onDragLeave(e) {
let node = e.relatedTarget;
while (node) {
const root = node.getRootNode?.();
if (ShadowRoot.isInstance(root)) {
node = root.host;
} else {
node = node.parentNode;
}
if (node === this) {
return;
}
}
this.#cleanupIndicator();
this.#dropTarget = null;
}
#onDrop(e) {
e.preventDefault();
e.stopPropagation();
if (this.readOnly) {
this.#cleanupIndicator();
this.#dropTarget = null;
return;
}
const target = this.#dropTarget;
this.#cleanupIndicator();
this.#dropTarget = null;
if (!target) {
return;
}
const flavor = this.#getSupportedFlavor(e.dataTransfer);
if (!flavor) {
return;
}
let validNodes;
if (flavor === TAB_DROP_TYPE) {
validNodes = this.#getNodesFromTabDrop(e.dataTransfer);
} else {
const data = e.dataTransfer.getData(flavor);
if (!data) {
return;
}
try {
({ validNodes } = lazy.PlacesUtils.unwrapNodes(data, flavor));
} catch (ex) {
return;
}
}
if (!validNodes?.length) {
return;
}
const doCopy =
!lazy.PlacesUIUtils.PLACES_FLAVORS.includes(flavor) ||
e.dataTransfer.dropEffect === "copy";
this.#doInsert(validNodes, target, doCopy);
}
#getNodesFromTabDrop(dataTransfer) {
const nodes = [];
const dropCount = dataTransfer.mozItemCount ||
1;
for (let i =
0; i < dropCount; i++) {
const data = dataTransfer.mozGetDataAt(TAB_DROP_TYPE, i);
if (!data) {
continue;
}
if (
XULElement.isInstance(data) &&
data.localName === "tab" &&
data.documentGlobal.isChromeWindow
) {
const uri = data.linkedBrowser.currentURI;
nodes.push({
uri: uri?.spec ?? "about:blank",
title: data.label,
type: lazy.PlacesUtils.TYPE_X_MOZ_URL,
});
} else if (
XULElement.isInstance(data) &&
data.localName === "tab-split-view-wrapper" &&
data.documentGlobal.isChromeWindow
) {
for (const tab of data.tabs) {
nodes.push({
uri: tab.linkedBrowser.currentURI?.spec ?? "about:blank",
title: tab.label,
type: lazy.PlacesUtils.TYPE_X_MOZ_URL,
});
}
}
}
return nodes;
}
async #doInsert(validNodes, target, doCopy) {
let insertionPoint;
if (target.orientation === DROP_ON) {
insertionPoint = {
guid: target.guid,
isTag: false,
getIndex: async () => lazy.PlacesUtils.bookmarks.DEFAULT_INDEX,
};
} else {
let fetchInfo;
try {
fetchInfo = await lazy.PlacesUtils.bookmarks.fetch({
guid: target.guid,
});
} catch (ex) {
return;
}
if (!fetchInfo) {
return;
}
insertionPoint = {
guid: fetchInfo.parentGuid,
isTag: false,
getIndex: async () =>
target.orientation === DROP_BEFORE
? fetchInfo.index
: fetchInfo.index +
1,
};
}
await lazy.PlacesUIUtils.handleTransferItems(
validNodes,
insertionPoint,
doCopy,
null
);
}
#onDragEnd() {
this.#cleanupIndicator();
this.#dropTarget = null;
this.#draggedGuid = null;
}
}
customElements.define("sidebar-bookmark-list", SidebarBookmarkList);
export class SidebarBookmarkRow extends SidebarTabRow {
get tooltipText() {
return this.url ? `${this.title}\n${this.url}` : null;
}
}
customElements.define("sidebar-bookmark-row", SidebarBookmarkRow);