feat(editor-ui): Resizable main panel (#3980)
* Introduce node deprecation (#3930) ✨ Introduce node deprecation * 🚧 Scaffold out Code node * 👕 Fix lint * 📘 Create types file * 🚚 Rename theme * 🔥 Remove unneeded prop * ⚡ Override keybindings * ⚡ Expand lintings * ⚡ Create editor content getter * 🚚 Ensure all helpers use `$` * ✨ Add autocompletion * ♻️ Refactore Resize UI lib component, allow to use it in different than n8n-sticky context * 🚧 Use variable width for node settings and allow for resizing * ✨ Use store to keep track of wide and regular main panel widths * ♻️ Extract Resize wrapper from the Sticky and create a story for it * 🐛 Fixed cherry-pick conflicts * ⚡ Filter out welcome note node * ⚡ Convey error line number * ⚡ Highlight error line * ⚡ Restore logging from node * ✨ More autocompletions * ⚡ Streamline completions * 💄 Fix drag-button border * ✏️ Update placeholders * ⚡ Update linter to new methods * ✨ Preserve main panel width in local storage * 🐛 Fallback to max size size if window is too big * 🔥 Remove `$nodeItem` completions * ⚡ Re-update placeholders * 🎨 Fix formatting * 📦 Update `package-lock.json` * ⚡ Refresh with multi-line empty string * ♻️ Refactored DraggablePanels to use relative units and implemented independent resizing, cleaned store * 🐛 Re-implement dragging indicators and move border styles to NDVDraggablePanels component * 🚨 Fix semis * 🚨 Remove unsused UI state props * ♻️ Use only relative left position and calculate right based on it, fix quirks * 🚨Fix linting error * ♻️ Store and retrieve main panel dimensions from store to make them persistable in the same app mount session * 🐛 Prevent resizing of unknown nodes * ♻️ Add typings for `nodeType` prop, remove unused `convertRemToPixels` import * 🏷️ Add typings for `nodeType` prop in NodeSettings.vue * 🐛 Prevent the main panel resize below 280px * 🐛 Fix inputless panel left position * ✨ Resize resource locator on main panel size change * 🐛 Resize resource locator on window resize Co-authored-by: Iván Ovejero <ivov.src@gmail.com>
This commit is contained in:
@@ -7,31 +7,61 @@
|
||||
<slot name="output"></slot>
|
||||
</div>
|
||||
<div :class="$style.mainPanel" :style="mainPanelStyles">
|
||||
<div :class="$style.dragButtonContainer" @click="close">
|
||||
<PanelDragButton
|
||||
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
|
||||
v-if="!hideInputAndOutput && isDraggable"
|
||||
:canMoveLeft="canMoveLeft"
|
||||
:canMoveRight="canMoveRight"
|
||||
@dragstart="onDragStart"
|
||||
@drag="onDrag"
|
||||
@dragend="onDragEnd"
|
||||
/>
|
||||
</div>
|
||||
<slot name="main"></slot>
|
||||
<n8n-resize-wrapper
|
||||
:isResizingEnabled="currentNodePaneType !== 'unknown'"
|
||||
:width="relativeWidthToPx(mainPanelDimensions.relativeWidth)"
|
||||
:minWidth="MIN_PANEL_WIDTH"
|
||||
:gridSize="20"
|
||||
@resize="onResize"
|
||||
@resizestart="onResizeStart"
|
||||
@resizeend="onResizeEnd"
|
||||
:supportedDirections="supportedResizeDirections"
|
||||
>
|
||||
<div :class="$style.dragButtonContainer">
|
||||
<PanelDragButton
|
||||
:class="{ [$style.draggable]: true, [$style.visible]: isDragging }"
|
||||
:canMoveLeft="canMoveLeft"
|
||||
:canMoveRight="canMoveRight"
|
||||
v-if="!hideInputAndOutput && isDraggable"
|
||||
@dragstart="onDragStart"
|
||||
@drag="onDrag"
|
||||
@dragend="onDragEnd"
|
||||
/>
|
||||
</div>
|
||||
<div :class="{ [$style.mainPanelInner]: true, [$style.dragging]: isDragging }">
|
||||
<slot name="main" />
|
||||
</div>
|
||||
</n8n-resize-wrapper>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import Vue from 'vue';
|
||||
import Vue, { PropType } from 'vue';
|
||||
import { get } from 'lodash';
|
||||
|
||||
import { INodeTypeDescription } from 'n8n-workflow';
|
||||
import PanelDragButton from './PanelDragButton.vue';
|
||||
|
||||
const MAIN_PANEL_WIDTH = 360;
|
||||
import {
|
||||
LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH,
|
||||
MAIN_NODE_PANEL_WIDTH,
|
||||
} from '@/constants';
|
||||
|
||||
|
||||
const SIDE_MARGIN = 24;
|
||||
const FIXED_PANEL_WIDTH = 320;
|
||||
const FIXED_PANEL_WIDTH_LARGE = 420;
|
||||
const MINIMUM_INPUT_PANEL_WIDTH = 320;
|
||||
const SIDE_PANELS_MARGIN = 80;
|
||||
const MIN_PANEL_WIDTH = 280;
|
||||
const PANEL_WIDTH = 320;
|
||||
const PANEL_WIDTH_LARGE = 420;
|
||||
|
||||
const initialMainPanelWidth:{ [key: string]: number } = {
|
||||
regular: MAIN_NODE_PANEL_WIDTH,
|
||||
dragless: MAIN_NODE_PANEL_WIDTH,
|
||||
unknown: MAIN_NODE_PANEL_WIDTH,
|
||||
inputless: MAIN_NODE_PANEL_WIDTH,
|
||||
wide: MAIN_NODE_PANEL_WIDTH * 2,
|
||||
};
|
||||
|
||||
export default Vue.extend({
|
||||
name: 'NDVDraggablePanels',
|
||||
@@ -48,124 +78,271 @@ export default Vue.extend({
|
||||
position: {
|
||||
type: Number,
|
||||
},
|
||||
nodeType: {
|
||||
type: Object as PropType<INodeTypeDescription>,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
data(): { windowWidth: number, isDragging: boolean, MIN_PANEL_WIDTH: number} {
|
||||
return {
|
||||
windowWidth: 0,
|
||||
windowWidth: 1,
|
||||
isDragging: false,
|
||||
MIN_PANEL_WIDTH,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.setTotalWidth();
|
||||
|
||||
/*
|
||||
Only set(or restore) initial position if `mainPanelDimensions`
|
||||
is at the default state({relativeLeft:1, relativeRight: 1, relativeWidth: 1}) to make sure we use store values if they are set
|
||||
*/
|
||||
if(this.mainPanelDimensions.relativeLeft === 1 && this.mainPanelDimensions.relativeRight === 1) {
|
||||
this.setMainPanelWidth();
|
||||
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
||||
this.restorePositionData();
|
||||
}
|
||||
|
||||
window.addEventListener('resize', this.setTotalWidth);
|
||||
this.$emit('init', { position: this.getRelativePosition() });
|
||||
this.$emit('init', { position: this.mainPanelDimensions.relativeLeft });
|
||||
},
|
||||
destroyed() {
|
||||
window.removeEventListener('resize', this.setTotalWidth);
|
||||
},
|
||||
computed: {
|
||||
fixedPanelWidth() {
|
||||
if (this.windowWidth > 1700) {
|
||||
return FIXED_PANEL_WIDTH_LARGE;
|
||||
}
|
||||
|
||||
return FIXED_PANEL_WIDTH;
|
||||
mainPanelDimensions(): {
|
||||
relativeWidth: number,
|
||||
relativeLeft: number,
|
||||
relativeRight: number
|
||||
} {
|
||||
return this.$store.getters['ui/mainPanelDimensions'](this.currentNodePaneType);
|
||||
},
|
||||
mainPanelPosition(): number {
|
||||
if (typeof this.position === 'number') {
|
||||
return this.position;
|
||||
}
|
||||
supportedResizeDirections() {
|
||||
const supportedDirections = ['right'];
|
||||
|
||||
if (!this.isDraggable) {
|
||||
return this.fixedPanelWidth + MAIN_PANEL_WIDTH / 2 + SIDE_MARGIN;
|
||||
}
|
||||
|
||||
const relativePosition = this.$store.getters['ui/mainPanelPosition'] as number;
|
||||
|
||||
return relativePosition * this.windowWidth;
|
||||
if(this.isDraggable) supportedDirections.push('left');
|
||||
return supportedDirections;
|
||||
},
|
||||
currentNodePaneType() {
|
||||
if(!this.hasInputSlot) return 'inputless';
|
||||
if(!this.isDraggable) return 'dragless';
|
||||
if(this.nodeType === null) return 'unknown';
|
||||
return get(this, 'nodeType.parameterPane') || 'regular';
|
||||
},
|
||||
hasInputSlot() {
|
||||
return this.$slots.input !== undefined;
|
||||
},
|
||||
inputPanelMargin(): number {
|
||||
return !this.isDraggable? 0 : 80;
|
||||
return this.pxToRelativeWidth(SIDE_PANELS_MARGIN);
|
||||
},
|
||||
minWindowWidth() {
|
||||
return 2 * (SIDE_MARGIN + SIDE_PANELS_MARGIN) + MIN_PANEL_WIDTH;
|
||||
},
|
||||
minimumLeftPosition(): number {
|
||||
return SIDE_MARGIN + this.inputPanelMargin;
|
||||
if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
||||
|
||||
if(!this.hasInputSlot) return this.pxToRelativeWidth(SIDE_MARGIN);
|
||||
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
||||
},
|
||||
maximumRightPosition(): number {
|
||||
return this.windowWidth - MAIN_PANEL_WIDTH - this.minimumLeftPosition;
|
||||
},
|
||||
mainPanelFinalPositionPx(): number {
|
||||
const padding = this.minimumLeftPosition;
|
||||
let pos = this.mainPanelPosition + MAIN_PANEL_WIDTH / 2;
|
||||
pos = Math.max(padding, pos - MAIN_PANEL_WIDTH);
|
||||
pos = Math.min(pos, this.maximumRightPosition);
|
||||
if(this.windowWidth < this.minWindowWidth) return this.pxToRelativeWidth(1);
|
||||
|
||||
return pos;
|
||||
return this.pxToRelativeWidth(SIDE_MARGIN + 20) + this.inputPanelMargin;
|
||||
},
|
||||
canMoveLeft(): boolean {
|
||||
return this.mainPanelFinalPositionPx > this.minimumLeftPosition;
|
||||
return this.mainPanelDimensions.relativeLeft > this.minimumLeftPosition;
|
||||
},
|
||||
canMoveRight(): boolean {
|
||||
return this.mainPanelFinalPositionPx < this.maximumRightPosition;
|
||||
return this.mainPanelDimensions.relativeRight > this.maximumRightPosition;
|
||||
},
|
||||
mainPanelStyles(): { left: string } {
|
||||
mainPanelStyles(): { left: string, right: string } {
|
||||
return {
|
||||
left: `${this.mainPanelFinalPositionPx}px`,
|
||||
'left': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeLeft)}px`,
|
||||
'right': `${this.relativeWidthToPx(this.mainPanelDimensions.relativeRight)}px`,
|
||||
};
|
||||
},
|
||||
inputPanelStyles(): { width: string } {
|
||||
if (!this.isDraggable) {
|
||||
return {
|
||||
width: `${this.fixedPanelWidth}px`,
|
||||
};
|
||||
inputPanelStyles():{ right: string } {
|
||||
return {
|
||||
right: `${this.relativeWidthToPx(this.calculatedPositions.inputPanelRelativeRight)}px`,
|
||||
};
|
||||
},
|
||||
outputPanelStyles(): { left: string, transform: string} {
|
||||
return {
|
||||
left: `${this.relativeWidthToPx(this.calculatedPositions.outputPanelRelativeLeft)}px`,
|
||||
transform: `translateX(-${this.relativeWidthToPx(this.outputPanelRelativeTranslate)}px)`,
|
||||
};
|
||||
},
|
||||
calculatedPositions():{ inputPanelRelativeRight: number, outputPanelRelativeLeft: number } {
|
||||
const hasInput = this.$slots.input !== undefined;
|
||||
const outputPanelRelativeLeft = this.mainPanelDimensions.relativeLeft + this.mainPanelDimensions.relativeWidth;
|
||||
|
||||
const inputPanelRelativeRight = hasInput
|
||||
? 1 - outputPanelRelativeLeft + this.mainPanelDimensions.relativeWidth
|
||||
: (1 - this.pxToRelativeWidth(SIDE_MARGIN));
|
||||
|
||||
return {
|
||||
inputPanelRelativeRight,
|
||||
outputPanelRelativeLeft,
|
||||
};
|
||||
},
|
||||
outputPanelRelativeTranslate():number {
|
||||
const panelMinLeft = 1 - this.pxToRelativeWidth(MIN_PANEL_WIDTH + SIDE_MARGIN);
|
||||
const currentRelativeLeftDelta = this.calculatedPositions.outputPanelRelativeLeft - panelMinLeft;
|
||||
return currentRelativeLeftDelta > 0 ? currentRelativeLeftDelta : 0;
|
||||
},
|
||||
hasDoubleWidth() {
|
||||
return get(this, 'nodeType.parameterPane') === 'wide';
|
||||
},
|
||||
fixedPanelWidth(): number {
|
||||
const multiplier = this.hasDoubleWidth ? 2 : 1;
|
||||
|
||||
if (this.windowWidth > 1700) {
|
||||
return PANEL_WIDTH_LARGE * multiplier;
|
||||
}
|
||||
|
||||
let width = this.mainPanelPosition - MAIN_PANEL_WIDTH / 2 - SIDE_MARGIN;
|
||||
width = Math.min(
|
||||
width,
|
||||
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH,
|
||||
);
|
||||
width = Math.max(320, width);
|
||||
return {
|
||||
width: `${width}px`,
|
||||
};
|
||||
return PANEL_WIDTH * multiplier;
|
||||
},
|
||||
outputPanelStyles(): { width: string } {
|
||||
let width = this.windowWidth - this.mainPanelPosition - MAIN_PANEL_WIDTH / 2 - SIDE_MARGIN;
|
||||
width = Math.min(
|
||||
width,
|
||||
this.windowWidth - SIDE_MARGIN * 2 - this.inputPanelMargin - MAIN_PANEL_WIDTH,
|
||||
);
|
||||
width = Math.max(MINIMUM_INPUT_PANEL_WIDTH, width);
|
||||
return {
|
||||
width: `${width}px`,
|
||||
};
|
||||
isBelowMinWidthMainPanel(): boolean {
|
||||
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||
return this.mainPanelDimensions.relativeWidth < minRelativeWidth;
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
windowWidth(windowWidth) {
|
||||
const minRelativeWidth = this.pxToRelativeWidth(MIN_PANEL_WIDTH);
|
||||
// Prevent the panel resizing below MIN_PANEL_WIDTH whhile maintaing position
|
||||
if(this.isBelowMinWidthMainPanel) {
|
||||
this.setMainPanelWidth(minRelativeWidth);
|
||||
}
|
||||
|
||||
const isBelowMinLeft = this.minimumLeftPosition > this.mainPanelDimensions.relativeLeft;
|
||||
const isMaxRight = this.maximumRightPosition > this.mainPanelDimensions.relativeRight;
|
||||
|
||||
// When user is resizing from non-supported view(sub ~488px) we need to refit the panels
|
||||
if((windowWidth > this.minWindowWidth) && isBelowMinLeft && isMaxRight) {
|
||||
this.setMainPanelWidth(minRelativeWidth);
|
||||
this.setPositions(this.getInitialLeftPosition(this.mainPanelDimensions.relativeWidth));
|
||||
}
|
||||
|
||||
this.setPositions(this.mainPanelDimensions.relativeLeft);
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
getRelativePosition() {
|
||||
const current = this.mainPanelFinalPositionPx + MAIN_PANEL_WIDTH / 2 - this.windowWidth / 2;
|
||||
getInitialLeftPosition(width: number) {
|
||||
if(this.currentNodePaneType === 'dragless') return this.pxToRelativeWidth(SIDE_MARGIN + 1 + this.fixedPanelWidth);
|
||||
|
||||
const pos = Math.floor(
|
||||
(current / ((this.maximumRightPosition - this.minimumLeftPosition) / 2)) * 100,
|
||||
return this.hasInputSlot
|
||||
? 0.5 - (width / 2)
|
||||
: this.minimumLeftPosition;
|
||||
},
|
||||
setMainPanelWidth(relativeWidth?: number) {
|
||||
const mainPanelRelativeWidth = relativeWidth || this.pxToRelativeWidth(initialMainPanelWidth[this.currentNodePaneType]);
|
||||
|
||||
this.$store.commit('ui/setMainPanelDimensions', {
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeWidth: mainPanelRelativeWidth,
|
||||
},
|
||||
});
|
||||
},
|
||||
setPositions(relativeLeft: number) {
|
||||
const mainPanelRelativeLeft = relativeLeft || 1 - this.calculatedPositions.inputPanelRelativeRight;
|
||||
const mainPanelRelativeRight = 1 - mainPanelRelativeLeft - this.mainPanelDimensions.relativeWidth;
|
||||
|
||||
const isMaxRight = this.maximumRightPosition > mainPanelRelativeRight;
|
||||
const isMinLeft = this.minimumLeftPosition > mainPanelRelativeLeft;
|
||||
const isInputless = this.currentNodePaneType === 'inputless';
|
||||
|
||||
if(isMinLeft) {
|
||||
this.$store.commit('ui/setMainPanelDimensions', {
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: this.minimumLeftPosition,
|
||||
relativeRight: 1 - this.mainPanelDimensions.relativeWidth - this.minimumLeftPosition,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if(isMaxRight) {
|
||||
this.$store.commit('ui/setMainPanelDimensions', {
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: 1 - this.mainPanelDimensions.relativeWidth - this.maximumRightPosition,
|
||||
relativeRight: this.maximumRightPosition,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.$store.commit('ui/setMainPanelDimensions', {
|
||||
panelType: this.currentNodePaneType,
|
||||
dimensions: {
|
||||
relativeLeft: isInputless ? this.minimumLeftPosition : mainPanelRelativeLeft,
|
||||
relativeRight: mainPanelRelativeRight,
|
||||
},
|
||||
});
|
||||
},
|
||||
pxToRelativeWidth(px: number) {
|
||||
return px / this.windowWidth;
|
||||
},
|
||||
relativeWidthToPx(relativeWidth: number) {
|
||||
return relativeWidth * this.windowWidth;
|
||||
},
|
||||
onResizeStart() {
|
||||
this.setTotalWidth();
|
||||
},
|
||||
onResizeEnd() {
|
||||
this.storePositionData();
|
||||
},
|
||||
onResize({ direction, x, width }: { direction: string, x: number, width: number}) {
|
||||
const relativeDistance = this.pxToRelativeWidth(x);
|
||||
const relativeWidth = this.pxToRelativeWidth(width);
|
||||
|
||||
if(direction === "left" && relativeDistance <= this.minimumLeftPosition) return;
|
||||
if(direction === "right" && (1 - relativeDistance) <= this.maximumRightPosition) return;
|
||||
if(width <= MIN_PANEL_WIDTH) return;
|
||||
|
||||
this.setMainPanelWidth(relativeWidth);
|
||||
this.setPositions(direction === 'left'
|
||||
? relativeDistance
|
||||
: this.mainPanelDimensions.relativeLeft,
|
||||
);
|
||||
return pos;
|
||||
},
|
||||
restorePositionData() {
|
||||
const storedPanelWidthData = window.localStorage.getItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`);
|
||||
|
||||
if(storedPanelWidthData) {
|
||||
const parsedWidth = parseFloat(storedPanelWidthData);
|
||||
this.setMainPanelWidth(parsedWidth);
|
||||
const initialPosition = this.getInitialLeftPosition(parsedWidth);
|
||||
|
||||
this.setPositions(initialPosition);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
storePositionData() {
|
||||
window.localStorage.setItem(`${LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH}_${this.currentNodePaneType}`, this.mainPanelDimensions.relativeWidth.toString());
|
||||
},
|
||||
onDragStart() {
|
||||
this.isDragging = true;
|
||||
this.$emit('dragstart', { position: this.getRelativePosition() });
|
||||
this.$emit('dragstart', { position: this.mainPanelDimensions.relativeLeft });
|
||||
},
|
||||
onDrag(e: {x: number, y: number}) {
|
||||
const relativePosition = e.x / this.windowWidth;
|
||||
this.$store.commit('ui/setMainPanelRelativePosition', relativePosition);
|
||||
const relativeLeft = this.pxToRelativeWidth(e.x) - (this.mainPanelDimensions.relativeWidth / 2);
|
||||
|
||||
this.setPositions(relativeLeft);
|
||||
},
|
||||
onDragEnd() {
|
||||
setTimeout(() => {
|
||||
this.isDragging = false;
|
||||
this.$emit('dragend', {
|
||||
windowWidth: this.windowWidth,
|
||||
position: this.getRelativePosition(),
|
||||
position: this.mainPanelDimensions.relativeLeft,
|
||||
});
|
||||
}, 0);
|
||||
this.storePositionData();
|
||||
},
|
||||
setTotalWidth() {
|
||||
this.windowWidth = window.innerWidth;
|
||||
@@ -178,14 +355,13 @@ export default Vue.extend({
|
||||
</script>
|
||||
|
||||
<style lang="scss" module>
|
||||
$--main-panel-width: 360px;
|
||||
|
||||
.dataPanel {
|
||||
position: absolute;
|
||||
height: calc(100% - 2 * var(--spacing-l));
|
||||
position: absolute;
|
||||
top: var(--spacing-l);
|
||||
z-index: 0;
|
||||
min-width: 280px;
|
||||
}
|
||||
|
||||
.inputPanel {
|
||||
@@ -200,7 +376,6 @@ $--main-panel-width: 360px;
|
||||
.outputPanel {
|
||||
composes: dataPanel;
|
||||
right: var(--spacing-l);
|
||||
width: $--main-panel-width;
|
||||
|
||||
> * {
|
||||
border-radius: 0 var(--border-radius-large) var(--border-radius-large) 0;
|
||||
@@ -218,18 +393,35 @@ $--main-panel-width: 360px;
|
||||
}
|
||||
}
|
||||
|
||||
.mainPanelInner {
|
||||
height: 100%;
|
||||
border: var(--border-base);
|
||||
border-radius: var(--border-radius-large);
|
||||
box-shadow: 0 4px 16px rgb(50 61 85 / 10%);
|
||||
overflow: hidden;
|
||||
|
||||
&.dragging {
|
||||
border-color: var(--color-primary);
|
||||
box-shadow: 0px 6px 16px rgba(255, 74, 51, 0.15);
|
||||
}
|
||||
}
|
||||
|
||||
.draggable {
|
||||
position: absolute;
|
||||
left: 40%;
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
.dragButtonContainer {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
width: $--main-panel-width;
|
||||
width: 100%;
|
||||
height: 12px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
pointer-events: none;
|
||||
|
||||
.draggable {
|
||||
pointer-events: all;
|
||||
}
|
||||
&:hover .draggable {
|
||||
visibility: visible;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user