手写一个具备拖拉拽多功能的弹窗
一个具备拖拉拽多功能的弹窗具备以下功能:
- 能够拖动弹窗位置;
- 能够放大缩小弹窗;
- 能够实现弹窗的最小化和还原最大化。
最终实现可以直接点击查看实现代码,下面是基本实现的思路。
e.clientX
是相对于整个文档左上角的坐标。e.offsetX
是相对于触发事件的元素左上角的坐标。
获取弹窗的 boxRef
元素对象, 获取拖动 icon
元素的 dragRef
元素对象, 获取缩放弹窗 icon
的 boxSizeRef
元素对象。
拖动窗口: 计算窗体的绝对定位 left 和 top
// 窗体初始 left + 点击按钮当前距离浏览器左侧距离 - 初始点击距离浏览器左侧距离
const left = startBoxOffsetLeft + currentE.clientX - startClientX
const top = startBoxOffsetTop + currentE.clientY - startClientY
缩放窗口: 计算窗体宽度 boxWidth 和高度 boxHeight
const boxWidth = startBoxOffsetWidth + currentE.clientX - startClientX
const boxHeight = startBoxOffsetHeight + currentE.clientY - startClientY
绘制基本弹框
如上图所示,有四个部分:
- 弹框主体
- 第一排拖动和最小化/还原最大化功能区按钮
- 弹窗内容
- 右下角弹窗拖拽功能按钮
实现原理:通过 ref
动态控制弹窗的大小和相对于屏幕的位置。
因此,可以先绘制出最基本的弹窗样式。这里我们引入的是 AntDesignVue 的图标库。
<template>
<h1>拖拉拽弹框</h1>
<div
ref="box"
class="box"
:class="{ 'unset-size': !state.expanded }"
@wheel.capture.stop
>
<div class="container">
<div class="action-bar">
<div ref="dragHandle" class="icon" style="cursor: grab">
<DragOutlined />
</div>
<div
class="icon"
style="cursor: pointer"
@click="state.expanded = !state.expanded"
>
<FullscreenExitOutlined v-if="state.expanded" />
<FullscreenOutlined v-else />
</div>
</div>
<div v-if="state.expanded" class="info">{{ text }}</div>
</div>
<div v-if="state.expanded" ref="resizeHandle" class="mouse-sensor">
<ArrowsAltOutlined />
</div>
</div>
</template>
<script setup lang="ts">
const box = ref<HTMLElement>()
const dragHandle = ref<HTMLElement>()
const resizeHandle = ref<HTMLElement>()
const state = ref({
left: 50,
top: 100,
width: 250,
height: 200,
expanded: true,
})
const text = `Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it uses the native node.js http module, while on the client (browser) it uses XMLHttpRequests.
Features
- Make XMLHttpRequests from the browser
- Make http requests from node.js
- Supports the Promise API
- Intercept request and response
- Transform request and response data
- Cancel requests
- Timeouts
- Query parameters serialization with support for nested entries
- Automatic request body serialization to:
a. JSON (application/json)
b. Multipart / FormData (multipart/form-data)
c. URL encoded form (application/x-www-form-urlencoded)
- Posting HTML forms as JSON
- Automatic JSON data handling in response
- Progress capturing for browsers and node.js with extra info (speed rate, remaining time)
- Setting bandwidth limits for node.js
- Compatible with spec-compliant FormData and Blob (including node.js)
- Client side support for protecting against XSRF`
</script>
<style scoped lang="less">
.box {
position: fixed;
z-index: 2;
background: #42b983;
padding: 8px 16px;
box-shadow: 0px 0px 4px 4px;
border-radius: 4px;
&.unset-size {
width: unset !important;
height: unset !important;
}
.container {
height: 100%;
display: flex;
overflow: hidden;
flex-direction: column;
}
.action-bar {
display: flex;
align-items: center;
user-select: none;
.icon {
font-size: 24px;
padding: 2px 4px;
border-radius: 4px;
&:hover {
background: #374151;
}
}
& > * {
flex-wrap: wrap;
&:not(:last-child) {
margin-right: 8px;
}
}
}
.info {
padding-top: 4px;
word-break: break-all;
white-space: pre-line;
overflow: auto;
}
.mouse-sensor {
position: absolute;
bottom: 1px;
right: 2px;
transform: rotate(90deg);
cursor: se-resize;
z-index: 1;
background: #42b983;
border-radius: 2px;
font-size: 18px;
padding: 2px;
}
}
</style>
在样式中,我们定义了一个响应式变量 state 用来存储弹窗的初始转态,分别是宽高和距离屏幕的初始位置和是否展开窗体的状态。
其它说明:
:class="{ 'unset-size': !state.expanded }"
加入了unset-size
属性是为了在最小化弹窗时重置弹窗大小。 unset 的含义是: 如果属性值被继承自父元素,则会重置为默认值;如果属性值没有被继承,则会重置为 initial 值。
@wheel.capture.stop
表示在弹窗上监听鼠标滚轮事件,并阻止事件继续传播。这样做的目的可能是为了防止鼠标滚轮事件冒泡到父元素或其他元素上,以避免对页面滚动等行为的干扰。
手写拖拽弹窗 Hooks
在上面的初始代码中,我们还定义了三个 ref 用于获取 DOM 元素。它们的作用分别是:
- box:用于响应式的更改窗体大小和位置;
- dragHandle:用于响应式监听拖动窗体位置;
- resizeHandle:用于响应式监听拖拽窗体大小。
因此有 useResizeAndDrag
初始定义:
interface UseResizeAndDragOptions {
onResize?: (width: number, height: number) => void
onDrag?: (left: number, top: number) => void
left?: number
top?: number
width?: number
height?: number
}
function useResizeAndDrag(
elementRef: Ref<HTMLElement | undefined>,
resizeHandleRef: Ref<HTMLElement | undefined>,
dragHandleRef?: Ref<HTMLElement | undefined>,
options?: UseResizeAndDragOptions
): void {}
接下来对窗体进行初始化:
const resizeHandle = { x: 0, y: 0 }
let startX = 0 // 拖动起始位置, 定义变量便于后续使用
let startY = 0
let startWidth = typeof options?.width === 'number' ? options.width : 0
let startHeight = typeof options?.height === 'number' ? options.height : 0
let startLeft = typeof options?.left === 'number' ? options.left : 0
let startTop = typeof options?.top === 'number' ? options.top : 0
let isDragging = false // 检测是否开始拖动,避免拖动和移动事件鼠标移动监听的冲突
const handleWindowResize = () => {
if (!elementRef.value || !resizeHandleRef.value) return
let left = elementRef.value.offsetLeft
let top = elementRef.value.offsetTop
let width = elementRef.value.offsetWidth
let height = elementRef.value.offsetHeight
// 检查元素是否超过视窗宽度
if (left + width > window.innerWidth) {
left = window.innerWidth - width
if (left < 0) {
left = 0
width = window.innerWidth
}
}
// 检查元素是否超过视窗高度
if (top + height > window.innerHeight) {
top = window.innerHeight - height
if (top < 0) {
top = 0
height = window.innerHeight
}
}
// 更新元素位置和大小
elementRef.value.style.left = `${left}px`
elementRef.value.style.top = `${top}px`
elementRef.value.style.width = `${width}px`
elementRef.value.style.height = `${height}px`
}
// 在 hook 中用引入 onMounted 生命周期钩子函数
onMounted(() => {
if (!elementRef.value || !options) return
if (typeof options.width === 'number') {
elementRef.value.style.width = `${options.width}px`
}
if (typeof options.height === 'number') {
elementRef.value.style.height = `${options.height}px`
}
if (typeof options.left === 'number') {
elementRef.value.style.left = `${options.left}px`
}
if (typeof options.top === 'number') {
elementRef.value.style.top = `${options.top}px`
}
handleWindowResize()
window.addEventListener('resize', handleWindowResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleWindowResize)
})
在初始化窗体后,检测窗体大小是否超出可视窗口位置,并添加 resize 事件。(由于添加了 resize 监听事件,所以在最后的 onBeforeUnmount 中进行卸载)
完成初始化后可以开始监听窗体拖拽和滚动事件了,为了兼容手机端因此点击和结束的事件都有俩个:
mousedown
和 touchstart
对应 mouseup
和 touchend
。
watch(
() => [elementRef.value, resizeHandleRef.value, dragHandleRef.value],
([element, resizeHandle, dragHandle]) => {
if (element && resizeHandle) {
resizeHandle.addEventListener('mousedown', handleResizeMouseDown)
resizeHandle.addEventListener('touchstart', handleResizeMouseDown)
}
if (element && dragHandle) {
dragHandle.addEventListener('mousedown', handleDragMouseDown)
dragHandle.addEventListener('touchstart', handleDragMouseDown)
}
}
)
这便完成了对窗体和功能按钮的监听,当按下按钮后接下来便是开始计算拖拽和移动的位置:
// 先看拖拽事件
const handleResizeMouseDown = (e: MouseEvent | TouchEvent) => {
e.stopPropagation() // 阻止事件冒泡
e.preventDefault() // 阻止原生事件
if (!elementRef.value || !resizeHandleRef.value) return
startX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
startY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
startWidth = elementRef.value.offsetWidth
startHeight = elementRef.value.offsetHeight
resizeHandle.x = resizeHandleRef.value.offsetLeft
resizeHandle.y = resizeHandleRef.value.offsetTop
// 监听下按
document.documentElement.addEventListener('mousemove', handleResizeMouseMove)
document.documentElement.addEventListener('touchmove', handleResizeMouseMove)
// 监听抬起
document.documentElement.addEventListener('mouseup', handleResizeMouseUp)
document.documentElement.addEventListener('touchend', handleResizeMouseUp)
}
const handleResizeMouseMove = (e: MouseEvent | TouchEvent) => {
if (!elementRef.value || !resizeHandleRef.value) return
let width =
startWidth +
((e instanceof MouseEvent ? e.clientX : e.touches[0].clientX) - startX)
let height =
startHeight +
((e instanceof MouseEvent ? e.clientY : e.touches[0].clientY) - startY)
// 检查窗体是否超出视宽
if (elementRef.value.offsetLeft + width > window.innerWidth) {
width = window.innerWidth - elementRef.value.offsetLeft
}
// 检查窗体是否超出视高
if (elementRef.value.offsetTop + height > window.innerHeight) {
height = window.innerHeight - elementRef.value.offsetTop
}
elementRef.value.style.width = `${width}px`
elementRef.value.style.height = `${height}px`
// 保存当前窗口大小, 后边下次窗口打开复原
if (options?.onResize) {
options.onResize(width, height)
}
}
const handleResizeMouseUp = () => {
document.documentElement.removeEventListener(
'mousemove',
handleResizeMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleResizeMouseMove
)
document.documentElement.removeEventListener('mouseup', handleResizeMouseUp)
document.documentElement.removeEventListener('touchend', handleResizeMouseUp)
}
// 监听拖动事件
const handleDragMouseDown = (e: MouseEvent | TouchEvent) => {
e.stopPropagation()
e.preventDefault()
if (!elementRef.value || !dragHandleRef.value) return
isDragging = true
startLeft = elementRef.value.offsetLeft
startTop = elementRef.value.offsetTop
startX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
startY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
document.documentElement.addEventListener('mousemove', handleDragMouseMove)
document.documentElement.addEventListener('touchmove', handleDragMouseMove)
document.documentElement.addEventListener('mouseup', handleDragMouseUp)
document.documentElement.addEventListener('touchend', handleDragMouseUp)
}
const handleDragMouseMove = (e: MouseEvent | TouchEvent) => {
if (!elementRef.value || !dragHandleRef.value || !isDragging) return
const left =
startLeft +
((e instanceof MouseEvent ? e.clientX : e.touches[0].clientX) - startX)
const top =
startTop +
((e instanceof MouseEvent ? e.clientY : e.touches[0].clientY) - startY)
// 检查元素是否超出视宽
if (left < 0) {
elementRef.value.style.left = '0px'
} else if (left + elementRef.value.offsetWidth > window.innerWidth) {
elementRef.value.style.left = `${
window.innerWidth - elementRef.value.offsetWidth
}px`
} else {
elementRef.value.style.left = `${left}px`
}
// 检查元素是否超出视高
if (top < 0) {
elementRef.value.style.top = '0px'
} else if (top + elementRef.value.offsetHeight > window.innerHeight) {
elementRef.value.style.top = `${
window.innerHeight - elementRef.value.offsetHeight
}px`
} else {
elementRef.value.style.top = `${top}px`
}
if (options?.onDrag) {
options.onDrag(left, top)
}
}
const handleDragMouseUp = () => {
isDragging = false
document.documentElement.removeEventListener('mousemove', handleDragMouseMove)
document.documentElement.removeEventListener('touchmove', handleDragMouseMove)
document.documentElement.removeEventListener('mouseup', handleDragMouseUp)
document.documentElement.removeEventListener('touchend', handleDragMouseUp)
}
拖动事件和移动事件在鼠标移动上有事件监听重复,因此在初始时增加了一个 isDragging
变量来隔离俩者。
最后为了防止内存泄漏,在组件卸载时统一将这些事件给予卸载:
onBeforeUnmount(() => {
document.documentElement.removeEventListener(
'mousemove',
handleResizeMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleResizeMouseMove
)
document.documentElement.removeEventListener('mouseup', handleResizeMouseUp)
document.documentElement.removeEventListener('touchend', handleResizeMouseUp)
document.documentElement.removeEventListener('mousemove', handleDragMouseMove)
document.documentElement.removeEventListener('touchmove', handleDragMouseMove)
document.documentElement.removeEventListener('mouseup', handleDragMouseUp)
document.documentElement.removeEventListener('touchend', handleDragMouseUp)
window.removeEventListener('resize', handleWindowResize)
})
最终代码
此处附上 Hooks 的完整代码:
// useResize.ts
import { onMounted, onBeforeUnmount, type Ref, watch } from 'vue'
interface ResizeHandle {
x: number
y: number
}
interface UseResizeAndDragOptions {
onResize?: (width: number, height: number) => void
onDrag?: (left: number, top: number) => void
left?: number
top?: number
width?: number
height?: number
}
export function useResizeAndDrag(
elementRef: Ref<HTMLElement | undefined>,
resizeHandleRef: Ref<HTMLElement | undefined>,
dragHandleRef: Ref<HTMLElement | undefined>,
options?: UseResizeAndDragOptions
) {
const resizeHandle: ResizeHandle = { x: 0, y: 0 }
let startX = 0
let startY = 0
let startWidth = typeof options?.width === 'number' ? options.width : 0
let startHeight = typeof options?.height === 'number' ? options.height : 0
let startLeft = typeof options?.left === 'number' ? options.left : 0
let startTop = typeof options?.top === 'number' ? options.top : 0
let isDragging = false
const handleResizeMouseDown = (e: MouseEvent | TouchEvent) => {
e.stopPropagation()
e.preventDefault()
if (!elementRef.value || !resizeHandleRef.value) return
startX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
startY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
startWidth = elementRef.value.offsetWidth
startHeight = elementRef.value.offsetHeight
resizeHandle.x = resizeHandleRef.value.offsetLeft
resizeHandle.y = resizeHandleRef.value.offsetTop
// 监听下按
document.documentElement.addEventListener(
'mousemove',
handleResizeMouseMove
)
document.documentElement.addEventListener(
'touchmove',
handleResizeMouseMove
)
// 监听抬起
document.documentElement.addEventListener('mouseup', handleResizeMouseUp)
document.documentElement.addEventListener('touchend', handleResizeMouseUp)
}
const handleResizeMouseMove = (e: MouseEvent | TouchEvent) => {
if (!elementRef.value || !resizeHandleRef.value) return
let width =
startWidth +
((e instanceof MouseEvent ? e.clientX : e.touches[0].clientX) - startX)
let height =
startHeight +
((e instanceof MouseEvent ? e.clientY : e.touches[0].clientY) - startY)
// 检查窗体是否超出视宽
if (elementRef.value.offsetLeft + width > window.innerWidth) {
width = window.innerWidth - elementRef.value.offsetLeft
}
// 检查窗体是否超出视高
if (elementRef.value.offsetTop + height > window.innerHeight) {
height = window.innerHeight - elementRef.value.offsetTop
}
elementRef.value.style.width = `${width}px`
elementRef.value.style.height = `${height}px`
// 保存当前窗口大小, 后边下次窗口打开复原
if (options?.onResize) {
options.onResize(width, height)
}
}
const handleResizeMouseUp = () => {
document.documentElement.removeEventListener(
'mousemove',
handleResizeMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleResizeMouseMove
)
document.documentElement.removeEventListener('mouseup', handleResizeMouseUp)
document.documentElement.removeEventListener(
'touchend',
handleResizeMouseUp
)
}
const handleDragMouseDown = (e: MouseEvent | TouchEvent) => {
e.stopPropagation()
e.preventDefault()
if (!elementRef.value || !dragHandleRef.value) return
isDragging = true
startLeft = elementRef.value.offsetLeft
startTop = elementRef.value.offsetTop
startX = e instanceof MouseEvent ? e.clientX : e.touches[0].clientX
startY = e instanceof MouseEvent ? e.clientY : e.touches[0].clientY
document.documentElement.addEventListener('mousemove', handleDragMouseMove)
document.documentElement.addEventListener('touchmove', handleDragMouseMove)
document.documentElement.addEventListener('mouseup', handleDragMouseUp)
document.documentElement.addEventListener('touchend', handleDragMouseUp)
}
const handleDragMouseMove = (e: MouseEvent | TouchEvent) => {
if (!elementRef.value || !dragHandleRef.value || !isDragging) return
const left =
startLeft +
((e instanceof MouseEvent ? e.clientX : e.touches[0].clientX) - startX)
const top =
startTop +
((e instanceof MouseEvent ? e.clientY : e.touches[0].clientY) - startY)
// 检查元素是否超出视宽
if (left < 0) {
elementRef.value.style.left = '0px'
} else if (left + elementRef.value.offsetWidth > window.innerWidth) {
elementRef.value.style.left = `${
window.innerWidth - elementRef.value.offsetWidth
}px`
} else {
elementRef.value.style.left = `${left}px`
}
// 检查元素是否超出视高
if (top < 0) {
elementRef.value.style.top = '0px'
} else if (top + elementRef.value.offsetHeight > window.innerHeight) {
elementRef.value.style.top = `${
window.innerHeight - elementRef.value.offsetHeight
}px`
} else {
elementRef.value.style.top = `${top}px`
}
if (options?.onDrag) {
options.onDrag(left, top)
}
}
const handleDragMouseUp = () => {
isDragging = false
document.documentElement.removeEventListener(
'mousemove',
handleDragMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleDragMouseMove
)
document.documentElement.removeEventListener('mouseup', handleDragMouseUp)
document.documentElement.removeEventListener('touchend', handleDragMouseUp)
}
const handleWindowResize = () => {
if (!elementRef.value || !resizeHandleRef.value) return
let left = elementRef.value.offsetLeft
let top = elementRef.value.offsetTop
let width = elementRef.value.offsetWidth
let height = elementRef.value.offsetHeight
// 检查是否超出视宽
if (left + width > window.innerWidth) {
left = window.innerWidth - width
if (left < 0) {
left = 0
width = window.innerWidth
}
}
// 检查是否超出视高
if (top + height > window.innerHeight) {
top = window.innerHeight - height
if (top < 0) {
top = 0
height = window.innerHeight
}
}
// 更新窗体大小
elementRef.value.style.left = `${left}px`
elementRef.value.style.top = `${top}px`
elementRef.value.style.width = `${width}px`
elementRef.value.style.height = `${height}px`
}
onMounted(() => {
if (!elementRef.value || !options) return
if (typeof options.width === 'number') {
elementRef.value.style.width = `${options.width}px`
}
if (typeof options.height === 'number') {
elementRef.value.style.height = `${options.height}px`
}
if (typeof options.left === 'number') {
elementRef.value.style.left = `${options.left}px`
}
if (typeof options.top === 'number') {
elementRef.value.style.top = `${options.top}px`
}
handleWindowResize()
window.addEventListener('resize', handleWindowResize)
})
onBeforeUnmount(() => {
document.documentElement.removeEventListener(
'mousemove',
handleResizeMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleResizeMouseMove
)
document.documentElement.removeEventListener('mouseup', handleResizeMouseUp)
document.documentElement.removeEventListener(
'touchend',
handleResizeMouseUp
)
document.documentElement.removeEventListener(
'mousemove',
handleDragMouseMove
)
document.documentElement.removeEventListener(
'touchmove',
handleDragMouseMove
)
document.documentElement.removeEventListener('mouseup', handleDragMouseUp)
document.documentElement.removeEventListener('touchend', handleDragMouseUp)
window.removeEventListener('resize', handleWindowResize)
})
watch(
() => [elementRef.value, resizeHandleRef.value, dragHandleRef.value],
([element, resizeHandle, dragHandle]) => {
if (element && resizeHandle) {
resizeHandle.addEventListener('mousedown', handleResizeMouseDown)
resizeHandle.addEventListener('touchstart', handleResizeMouseDown)
}
if (element && dragHandle) {
dragHandle.addEventListener('mousedown', handleDragMouseDown)
dragHandle.addEventListener('touchstart', handleDragMouseDown)
}
}
)
return {
handleResizeMouseDown,
handleDragMouseDown,
}
}
在最后,暴露了俩个事件 handleResizeMouseDown
和 handleDragMouseDown
为 Hooks 的返回便于定制化更改窗体大小和位置。
组件封装:
<template>
<div
ref="el"
class="box"
:class="{ 'unset-size': !state.expanded }"
@wheel.capture.stop
>
<div class="container">
<div class="action-bar">
<div ref="dragHandle" class="icon" style="cursor: grab">
<DragOutlined />
</div>
<div
class="icon"
style="cursor: pointer"
@click="state.expanded = !state.expanded"
>
<FullscreenExitOutlined v-if="state.expanded" />
<FullscreenOutlined v-else />
</div>
</div>
<div v-if="state.expanded" class="gen-info">{{ text }}</div>
</div>
<div v-if="state.expanded" ref="resizeHandle" class="mouse-sensor">
<ArrowsAltOutlined />
</div>
</div>
</template>
<script setup lang="ts">
import { useResizeAndDrag } from './useResize'
const el = ref<HTMLElement>()
const dragHandle = ref<HTMLElement>()
const resizeHandle = ref<HTMLElement>()
const state = ref({
left: 50,
top: 100,
width: 250,
height: 200,
expanded: true,
})
useResizeAndDrag(el, resizeHandle, dragHandle, {
...state.value,
onDrag: debounce(function (left: number, top: number) {
state.value = {
...state.value,
left,
top,
}
}, 300),
onResize: debounce(function (width: number, height: number) {
state.value = {
...state.value,
width,
height,
}
}, 300),
})
const text = `Axios is a promise-based HTTP Client for node.js and the browser. It is isomorphic (= it can run in the browser and nodejs with the same codebase). On the server-side it uses the native node.js http module, while on the client (browser) it uses XMLHttpRequests.
Features
- Make XMLHttpRequests from the browser
- Make http requests from node.js
- Supports the Promise API
- Intercept request and response
- Transform request and response data
- Cancel requests
- Timeouts
- Query parameters serialization with support for nested entries
- Automatic request body serialization to:
a. JSON (application/json)
b. Multipart / FormData (multipart/form-data)
c. URL encoded form (application/x-www-form-urlencoded)
- Posting HTML forms as JSON
- Automatic JSON data handling in response
- Progress capturing for browsers and node.js with extra info (speed rate, remaining time)
- Setting bandwidth limits for node.js
- Compatible with spec-compliant FormData and Blob (including node.js)
- Client side support for protecting against XSRF`
</script>
<style scoped lang="less">
.box {
position: fixed;
z-index: 9;
background: #42b983;
padding: 8px 16px;
box-shadow: 0px 0px 4px 4px;
border-radius: 4px;
&.unset-size {
width: unset !important;
height: unset !important;
}
.container {
height: 100%;
display: flex;
overflow: hidden;
flex-direction: column;
}
.action-bar {
display: flex;
align-items: center;
user-select: none;
.icon {
font-size: 24px;
padding: 2px 4px;
border-radius: 4px;
&:hover {
background: #374151;
}
}
& > * {
flex-wrap: wrap;
&:not(:last-child) {
margin-right: 8px;
}
}
}
.gen-info {
padding-top: 4px;
word-break: break-all;
white-space: pre-line;
overflow: auto;
}
.mouse-sensor {
position: absolute;
bottom: 0;
right: 0;
transform: rotate(90deg);
cursor: se-resize;
z-index: 1;
border-radius: 2px;
font-size: 18px;
padding: 2px;
}
}
</style>