mirror of
https://github.com/zen-browser/desktop.git
synced 2026-02-08 12:57:15 +00:00
feat: Implement vertical dnd tab splitting, p=#12289
This commit is contained in:
@@ -208,6 +208,10 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
if (typeof groupIndex === "undefined") {
|
||||
groupIndex = this._data.findIndex((group) => group.tabs.includes(tab));
|
||||
}
|
||||
// If groupIndex === -1, so `this._data.findIndex` couldn't find the split group
|
||||
if (groupIndex < 0) {
|
||||
return;
|
||||
}
|
||||
const group = this._data[groupIndex];
|
||||
const tabIndex = group.tabs.indexOf(tab);
|
||||
group.tabs.splice(tabIndex, 1);
|
||||
@@ -259,6 +263,33 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
return element;
|
||||
}
|
||||
|
||||
_calculateDropSide(event, panelsRect) {
|
||||
const { width, height } = panelsRect;
|
||||
const { clientX, clientY } = event;
|
||||
// TODO(octaviusz): Maybe we should add this as preference
|
||||
// `zen.splitView.tab-drop-treshold`
|
||||
const quarterWidth = width / 4;
|
||||
const quarterHeight = height / 4;
|
||||
|
||||
const edges = [
|
||||
{ side: "left", dist: clientX - panelsRect.left, threshold: quarterWidth },
|
||||
{ side: "right", dist: panelsRect.right - clientX, threshold: quarterWidth },
|
||||
{ side: "top", dist: clientY - panelsRect.top, threshold: quarterHeight },
|
||||
{ side: "bottom", dist: panelsRect.bottom - clientY, threshold: quarterHeight },
|
||||
];
|
||||
|
||||
let closestEdge = null;
|
||||
let minDist = Infinity;
|
||||
for (const edge of edges) {
|
||||
if (edge.dist < edge.threshold && edge.dist < minDist) {
|
||||
minDist = edge.dist;
|
||||
closestEdge = edge;
|
||||
}
|
||||
}
|
||||
|
||||
return closestEdge ? closestEdge.side : null;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line complexity
|
||||
onBrowserDragOverToSplit(event) {
|
||||
gBrowser.tabContainer.tabDragAndDrop.clearSpaceSwitchTimer();
|
||||
@@ -303,6 +334,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
const panelsRect = gBrowser.tabbox.getBoundingClientRect();
|
||||
const panelsWidth = panelsRect.width;
|
||||
const panelsHeight = panelsRect.height;
|
||||
if (
|
||||
event.clientX > panelsRect.left + panelsWidth - 10 ||
|
||||
event.clientX < panelsRect.left + 10 ||
|
||||
@@ -311,11 +343,17 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const dropSide = this._calculateDropSide(event, panelsRect);
|
||||
if (!dropSide) {
|
||||
return;
|
||||
}
|
||||
// first quarter or last quarter of the screen, but not the middle
|
||||
if (
|
||||
!(
|
||||
event.clientX < panelsRect.left + panelsWidth / 4 ||
|
||||
event.clientX > panelsRect.left + (panelsWidth / 4) * 3
|
||||
event.clientX > panelsRect.left + (panelsWidth / 4) * 3 ||
|
||||
event.clientY < panelsRect.top + panelsHeight / 4 ||
|
||||
event.clientY > panelsRect.top + (panelsHeight / 4) * 3
|
||||
)
|
||||
) {
|
||||
return;
|
||||
@@ -336,93 +374,113 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
this._canDrop = true;
|
||||
// eslint-disable-next-line mozilla/valid-services
|
||||
Services.zen.playHapticFeedback();
|
||||
{
|
||||
this._draggingTab = draggedTab;
|
||||
gBrowser.selectedTab = oldTab;
|
||||
this._hasAnimated = true;
|
||||
this.tabBrowserPanel.setAttribute("dragging-split", "true");
|
||||
// Add a min width to all the browser elements to prevent them from resizing
|
||||
// eslint-disable-next-line no-shadow
|
||||
const panelsWidth = gBrowser.tabbox.getBoundingClientRect().width;
|
||||
let numOfTabsToDivide = 2;
|
||||
if (currentView) {
|
||||
numOfTabsToDivide = currentView.tabs.length + 1;
|
||||
this._draggingTab = draggedTab;
|
||||
gBrowser.selectedTab = oldTab;
|
||||
this._hasAnimated = true;
|
||||
this.tabBrowserPanel.setAttribute("dragging-split", "true");
|
||||
this._animateDropEdge(dropSide, currentView, draggedTab, oldTab);
|
||||
}
|
||||
|
||||
_animateDropEdge(dropSide, currentView, draggedTab, oldTab) {
|
||||
// Add a min width to all the browser elements to prevent them from resizing
|
||||
// eslint-disable-next-line no-shadow
|
||||
const { height, width } = gBrowser.tabbox.getBoundingClientRect();
|
||||
let numOfTabsToDivide = 2;
|
||||
if (currentView) {
|
||||
numOfTabsToDivide = currentView.tabs.length + 1;
|
||||
}
|
||||
const halfWidth = width / numOfTabsToDivide;
|
||||
const halfHeight = height / numOfTabsToDivide;
|
||||
const side = dropSide;
|
||||
for (const browser of gBrowser.browsers) {
|
||||
if (!browser) {
|
||||
continue;
|
||||
}
|
||||
const halfWidth = panelsWidth / numOfTabsToDivide;
|
||||
let threshold =
|
||||
gNavToolbox.getBoundingClientRect().width *
|
||||
(gZenVerticalTabsManager._prefsRightSide ? 0 : 1);
|
||||
if (gZenCompactModeManager.preference) {
|
||||
threshold = 0;
|
||||
const { width: browserWidth, height: browserHeight } = browser.getBoundingClientRect();
|
||||
// Only apply it to the left side because if we add it to the right side,
|
||||
// we wont be able to move the element to the left.
|
||||
// FIXME: This is a workaround, we should find a better way to do this
|
||||
switch (side) {
|
||||
case "left":
|
||||
browser.style.minWidth = `${browserWidth}px`;
|
||||
break;
|
||||
case "top":
|
||||
browser.style.minHeight = `${browserHeight}px`;
|
||||
break;
|
||||
}
|
||||
const side = event.clientX - threshold > halfWidth ? "right" : "left";
|
||||
for (const browser of gBrowser.browsers) {
|
||||
if (!browser) {
|
||||
continue;
|
||||
}
|
||||
this.fakeBrowser = document.createXULElement("vbox");
|
||||
window.addEventListener("dragend", this.onBrowserDragEndToSplit, { once: true });
|
||||
const padding = ZenThemeModifier.elementSeparation;
|
||||
this.fakeBrowser.setAttribute("flex", "1");
|
||||
this.fakeBrowser.id = "zen-split-view-fake-browser";
|
||||
if (oldTab.splitView) {
|
||||
this.fakeBrowser.setAttribute("has-split-view", "true");
|
||||
}
|
||||
gBrowser.tabbox.appendChild(this.fakeBrowser);
|
||||
this.fakeBrowser.setAttribute("side", side);
|
||||
let animateTabBox = null;
|
||||
let animateFakeBrowser = null;
|
||||
switch (side) {
|
||||
case "left":
|
||||
animateTabBox = {
|
||||
padding: [0, `0 0 0 ${halfWidth}px`],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
width: [0, `${halfWidth - padding}px`],
|
||||
margin: [0, `0 0 0 ${-halfWidth}px`],
|
||||
};
|
||||
break;
|
||||
case "right":
|
||||
animateTabBox = {
|
||||
padding: [0, `0 ${halfWidth}px 0 0`],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
width: [0, `${halfWidth - padding}px`],
|
||||
};
|
||||
break;
|
||||
|
||||
case "top":
|
||||
animateTabBox = {
|
||||
padding: [0, `${halfHeight}px 0 0 0`],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
height: [0, `${halfHeight - padding}px`],
|
||||
margin: [0, `${-halfHeight}px 0 0 0`],
|
||||
};
|
||||
break;
|
||||
|
||||
case "bottom":
|
||||
animateTabBox = {
|
||||
padding: [0, `0 0 ${halfHeight}px 0`],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
height: [0, `${halfHeight - padding}px`],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
this._finishAllAnimatingPromise = Promise.all([
|
||||
gZenUIManager.motion.animate(gBrowser.tabbox, animateTabBox, {
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}),
|
||||
gZenUIManager.motion.animate(this.fakeBrowser, animateFakeBrowser, {
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}),
|
||||
]);
|
||||
if (this._finishAllAnimatingPromise) {
|
||||
this._finishAllAnimatingPromise.then(() => {
|
||||
if (draggedTab !== oldTab) {
|
||||
draggedTab.linkedBrowser.docShellIsActive = false;
|
||||
draggedTab.linkedBrowser
|
||||
.closest(".browserSidebarContainer")
|
||||
.classList.remove("deck-selected");
|
||||
}
|
||||
const width = browser.getBoundingClientRect().width;
|
||||
// Only apply it to the left side because if we add it to the right side,
|
||||
// we wont be able to move the element to the left.
|
||||
// FIXME: This is a workaround, we should find a better way to do this
|
||||
if (side === "left") {
|
||||
browser.style.minWidth = `${width}px`;
|
||||
}
|
||||
}
|
||||
this.fakeBrowser = document.createXULElement("vbox");
|
||||
window.addEventListener("dragend", this.onBrowserDragEndToSplit, { once: true });
|
||||
const padding = ZenThemeModifier.elementSeparation;
|
||||
this.fakeBrowser.setAttribute("flex", "1");
|
||||
this.fakeBrowser.id = "zen-split-view-fake-browser";
|
||||
if (oldTab.splitView) {
|
||||
this.fakeBrowser.setAttribute("has-split-view", "true");
|
||||
}
|
||||
gBrowser.tabbox.appendChild(this.fakeBrowser);
|
||||
this.fakeBrowser.setAttribute("side", side);
|
||||
this._finishAllAnimatingPromise = Promise.all([
|
||||
gZenUIManager.motion.animate(
|
||||
gBrowser.tabbox,
|
||||
side === "left"
|
||||
? {
|
||||
paddingLeft: [0, `${halfWidth}px`],
|
||||
paddingRight: 0,
|
||||
}
|
||||
: {
|
||||
paddingRight: [0, `${halfWidth}px`],
|
||||
paddingLeft: 0,
|
||||
},
|
||||
{
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}
|
||||
),
|
||||
gZenUIManager.motion.animate(
|
||||
this.fakeBrowser,
|
||||
{
|
||||
width: [0, `${halfWidth - padding}px`],
|
||||
...(side === "left"
|
||||
? {
|
||||
marginLeft: [0, `${-halfWidth}px`],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}
|
||||
),
|
||||
]);
|
||||
if (this._finishAllAnimatingPromise) {
|
||||
this._finishAllAnimatingPromise.then(() => {
|
||||
if (draggedTab !== oldTab) {
|
||||
draggedTab.linkedBrowser.docShellIsActive = false;
|
||||
draggedTab.linkedBrowser
|
||||
.closest(".browserSidebarContainer")
|
||||
.classList.remove("deck-selected");
|
||||
}
|
||||
this.fakeBrowser.addEventListener("dragleave", this.onBrowserDragEndToSplit);
|
||||
this._canDrop = true;
|
||||
});
|
||||
}
|
||||
this.fakeBrowser.addEventListener("dragleave", this.onBrowserDragEndToSplit);
|
||||
this._canDrop = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -447,12 +505,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
return;
|
||||
}
|
||||
const panelsWidth = panelsRect.width;
|
||||
const panelsHeight = panelsRect.height;
|
||||
let numOfTabsToDivide = 2;
|
||||
const currentView = this._data[this._lastOpenedTab.splitViewValue];
|
||||
if (currentView) {
|
||||
numOfTabsToDivide = currentView.tabs.length + 1;
|
||||
}
|
||||
const halfWidth = panelsWidth / numOfTabsToDivide;
|
||||
const halfHeight = panelsHeight / numOfTabsToDivide;
|
||||
const padding = ZenThemeModifier.elementSeparation;
|
||||
if (!this.fakeBrowser) {
|
||||
return;
|
||||
@@ -464,39 +524,60 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
...gBrowser.tabContainer.tabDragAndDrop.originalDragImageArgs
|
||||
);
|
||||
this._canDrop = false;
|
||||
Promise.all([
|
||||
gZenUIManager.motion.animate(
|
||||
gBrowser.tabbox,
|
||||
side === "left"
|
||||
? {
|
||||
paddingLeft: [`${halfWidth}px`, 0],
|
||||
}
|
||||
: {
|
||||
paddingRight: [`${halfWidth}px`, 0],
|
||||
},
|
||||
{
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}
|
||||
),
|
||||
gZenUIManager.motion.animate(
|
||||
this.fakeBrowser,
|
||||
{
|
||||
width: [`${halfWidth - padding * 2}px`, 0],
|
||||
...(side === "left"
|
||||
? {
|
||||
marginLeft: [`${-halfWidth}px`, 0],
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
{
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}
|
||||
),
|
||||
]).finally(() => {
|
||||
this._maybeRemoveFakeBrowser();
|
||||
});
|
||||
let animateTabBox = null;
|
||||
let animateFakeBrowser = null;
|
||||
switch (side) {
|
||||
case "left":
|
||||
animateTabBox = {
|
||||
padding: [`0 0 0 ${halfWidth}px`, 0],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
width: [`${halfWidth - padding}px`, 0],
|
||||
margin: [`0 0 0 ${-halfWidth}px`, 0],
|
||||
};
|
||||
break;
|
||||
case "right":
|
||||
animateTabBox = {
|
||||
padding: [`0 ${halfWidth}px 0 0`, 0],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
width: [`${halfWidth - padding}px`, 0],
|
||||
};
|
||||
break;
|
||||
case "top":
|
||||
animateTabBox = {
|
||||
padding: [`${halfHeight}px 0 0 0`, 0],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
height: [`${halfHeight - padding}px`, 0],
|
||||
margin: [`${-halfHeight}px 0 0 0`, 0],
|
||||
};
|
||||
break;
|
||||
case "bottom":
|
||||
animateTabBox = {
|
||||
padding: [`0 0 ${halfHeight}px 0`, 0],
|
||||
};
|
||||
animateFakeBrowser = {
|
||||
height: [`${halfHeight - padding}px`, 0],
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
this._finishAllAnimatingPromise = Promise.all([
|
||||
gZenUIManager.motion.animate(gBrowser.tabbox, animateTabBox, {
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}),
|
||||
gZenUIManager.motion.animate(this.fakeBrowser, animateFakeBrowser, {
|
||||
duration: 0.1,
|
||||
easing: "ease-out",
|
||||
}),
|
||||
]);
|
||||
if (this._finishAllAnimatingPromise) {
|
||||
this._finishAllAnimatingPromise.then(() => {
|
||||
this._maybeRemoveFakeBrowser();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1845,12 +1926,24 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
const dropSide = this.fakeBrowser?.getAttribute("side");
|
||||
const containerRect = this.fakeBrowser.getBoundingClientRect();
|
||||
const padding = ZenThemeModifier.elementSeparation;
|
||||
const dropTarget = document.elementFromPoint(
|
||||
dropSide === "left"
|
||||
? containerRect.left + containerRect.width + padding + 5
|
||||
: containerRect.left - padding - 5,
|
||||
event.clientY
|
||||
);
|
||||
let targetX = event.clientX;
|
||||
let targetY = event.clientY;
|
||||
switch (dropSide) {
|
||||
case "left":
|
||||
targetX = containerRect.left + containerRect.width + padding + 5;
|
||||
break;
|
||||
case "right":
|
||||
targetX = containerRect.left - padding - 5;
|
||||
break;
|
||||
case "top":
|
||||
targetY = containerRect.top + containerRect.height + padding + 5;
|
||||
break;
|
||||
case "bottom":
|
||||
targetY = containerRect.top - padding - 5;
|
||||
break;
|
||||
}
|
||||
|
||||
const dropTarget = document.elementFromPoint(targetX, targetY);
|
||||
const browser =
|
||||
dropTarget?.closest("browser") ??
|
||||
dropTarget?.closest(".browserSidebarContainer")?.querySelector("browser");
|
||||
@@ -1862,7 +1955,7 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
|
||||
let droppedOnTab = gZenGlanceManager.getTabOrGlanceParent(gBrowser.getTabForBrowser(browser));
|
||||
if (droppedOnTab === this._draggingTab) {
|
||||
this.createEmptySplit(dropSide == "right");
|
||||
this.createEmptySplit(dropSide);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1919,29 +2012,21 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const droppedOnSplitNode = this.getSplitNodeFromTab(droppedOnTab);
|
||||
const parentNode = droppedOnSplitNode.parent;
|
||||
|
||||
// Then add the tab to the split view
|
||||
group.tabs.push(draggedTab);
|
||||
|
||||
// If dropping on a side, create a new split in that direction
|
||||
// If dropping on a side, wrap entire layout in a new split at the root level
|
||||
if (hoverSide !== "center") {
|
||||
const splitDirection = hoverSide === "left" || hoverSide === "right" ? "row" : "column";
|
||||
if (parentNode.direction !== splitDirection) {
|
||||
this.splitIntoNode(
|
||||
droppedOnSplitNode,
|
||||
new nsSplitLeafNode(draggedTab, 50),
|
||||
hoverSide,
|
||||
0.5
|
||||
);
|
||||
const rootNode = group.layoutTree;
|
||||
const prepend = hoverSide === "left" || hoverSide === "top";
|
||||
|
||||
if (rootNode.direction === splitDirection) {
|
||||
// Root has the same direction, add as a new child of the root
|
||||
this.addTabToSplit(draggedTab, rootNode, prepend);
|
||||
} else {
|
||||
this.addTabToSplit(
|
||||
draggedTab,
|
||||
parentNode,
|
||||
/* prepend = */ hoverSide === "left" || hoverSide === "top"
|
||||
);
|
||||
// Different direction, wrap root in a new split node
|
||||
this.splitIntoNode(rootNode, new nsSplitLeafNode(draggedTab, 50), hoverSide, 0.5);
|
||||
}
|
||||
} else {
|
||||
this.addTabToSplit(draggedTab, group.layoutTree);
|
||||
@@ -1951,13 +2036,14 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
} else {
|
||||
// Create new split view with layout based on drop position
|
||||
let gridType = "vsep";
|
||||
const gridType = dropSide === "top" || dropSide === "bottom" ? "hsep" : "vsep";
|
||||
const topOrLeft = dropSide === "top" || dropSide === "left";
|
||||
|
||||
// Put tabs always as if it was dropped from the left
|
||||
this.splitTabs(
|
||||
dropSide == "left" ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
|
||||
topOrLeft ? [draggedTab, droppedOnTab] : [droppedOnTab, draggedTab],
|
||||
gridType,
|
||||
dropSide == "left" ? 0 : 1
|
||||
topOrLeft ? 0 : 1
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2192,14 +2278,16 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
}
|
||||
}
|
||||
|
||||
createEmptySplit(rightSide = true) {
|
||||
createEmptySplit(side = "right") {
|
||||
const selectedTab = gBrowser.selectedTab;
|
||||
const emptyTab = gZenWorkspaces._emptyTab;
|
||||
let tabs = rightSide ? [selectedTab, emptyTab] : [emptyTab, selectedTab];
|
||||
const gridType = side === "top" || side === "bottom" ? "hsep" : "vsep";
|
||||
const topOrLeft = side === "top" || side === "left";
|
||||
let tabs = topOrLeft ? [emptyTab, selectedTab] : [selectedTab, emptyTab];
|
||||
const data = {
|
||||
tabs,
|
||||
gridType: "grid",
|
||||
layoutTree: this.calculateLayoutTree(tabs, "grid"),
|
||||
gridType,
|
||||
layoutTree: this.calculateLayoutTree(tabs, gridType),
|
||||
};
|
||||
this.#withoutSplitViewTransition(() => {
|
||||
this._data.push(data);
|
||||
@@ -2234,9 +2322,9 @@ class nsZenViewSplitter extends nsZenDOMOperatedFeature {
|
||||
gBrowser.selectedTab = selectedTab;
|
||||
this.resetTabState(emptyTab, false);
|
||||
this.splitTabs(
|
||||
rightSide ? [selectedTab, newSelectedTab] : [newSelectedTab, selectedTab],
|
||||
"grid",
|
||||
rightSide ? 1 : 0
|
||||
topOrLeft ? [newSelectedTab, selectedTab] : [selectedTab, newSelectedTab],
|
||||
gridType,
|
||||
topOrLeft ? 0 : 1
|
||||
);
|
||||
} else {
|
||||
cleanup();
|
||||
|
||||
@@ -44,12 +44,12 @@
|
||||
#zen-splitview-dropzone {
|
||||
position: absolute !important;
|
||||
|
||||
margin: var(--zen-split-column-gap) var(--zen-split-row-gap) !important;
|
||||
margin-bottom: 0 !important;
|
||||
margin-left: 0 !important;
|
||||
margin: var(--zen-split-column-gap) var(--zen-split-row-gap);
|
||||
margin-bottom: 0;
|
||||
margin-left: 0;
|
||||
|
||||
&.browserSidebarContainer:not([zen-split='true']) {
|
||||
margin-top: 0 !important;
|
||||
margin-top: 0;
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
@@ -204,6 +204,15 @@
|
||||
overflow: hidden;
|
||||
will-change: width, margin-left;
|
||||
|
||||
&[side='top'],
|
||||
&[side='bottom'] {
|
||||
width: 100%;
|
||||
|
||||
&[has-split-view='true'] {
|
||||
width: calc(100% - var(--zen-element-separation));
|
||||
}
|
||||
}
|
||||
|
||||
&[side='right'] {
|
||||
right: 0;
|
||||
|
||||
@@ -211,6 +220,10 @@
|
||||
right: var(--zen-element-separation);
|
||||
}
|
||||
}
|
||||
|
||||
&[side='bottom'] {
|
||||
bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
#zen-split-view-drag-image {
|
||||
|
||||
Reference in New Issue
Block a user