admin-vben5/packages/effects/common-ui/src/components/resize/resize.vue

1123 lines
24 KiB
Vue
Raw Normal View History

<script lang="ts" setup>
/**
* This components is refactored from vue-drag-resize: https://github.com/kirillmurashov/vue-drag-resize
*/
import {
computed,
getCurrentInstance,
nextTick,
onBeforeUnmount,
onMounted,
ref,
toRefs,
watch,
} from 'vue';
const props = defineProps({
stickSize: {
type: Number,
default: 8,
},
parentScaleX: {
type: Number,
default: 1,
},
parentScaleY: {
type: Number,
default: 1,
},
isActive: {
type: Boolean,
default: false,
},
preventActiveBehavior: {
type: Boolean,
default: false,
},
isDraggable: {
type: Boolean,
default: true,
},
isResizable: {
type: Boolean,
default: true,
},
aspectRatio: {
type: Boolean,
default: false,
},
parentLimitation: {
type: Boolean,
default: false,
},
snapToGrid: {
type: Boolean,
default: false,
},
gridX: {
type: Number,
default: 50,
validator(val: number) {
return val >= 0;
},
},
gridY: {
type: Number,
default: 50,
validator(val: number) {
return val >= 0;
},
},
parentW: {
type: Number,
default: 0,
validator(val: number) {
return val >= 0;
},
},
parentH: {
type: Number,
default: 0,
validator(val: number) {
return val >= 0;
},
},
w: {
type: [String, Number],
default: 200,
validator(val: number) {
return typeof val === 'string' ? val === 'auto' : val >= 0;
},
},
h: {
type: [String, Number],
default: 200,
validator(val: number) {
return typeof val === 'string' ? val === 'auto' : val >= 0;
},
},
minw: {
type: Number,
default: 50,
validator(val: number) {
return val >= 0;
},
},
minh: {
type: Number,
default: 50,
validator(val: number) {
return val >= 0;
},
},
x: {
type: Number,
default: 0,
validator(val: number) {
return typeof val === 'number';
},
},
y: {
type: Number,
default: 0,
validator(val: number) {
return typeof val === 'number';
},
},
z: {
type: [String, Number],
default: 'auto',
validator(val: number) {
return typeof val === 'string' ? val === 'auto' : val >= 0;
},
},
dragHandle: {
type: String,
default: null,
},
dragCancel: {
type: String,
default: null,
},
sticks: {
type: Array<'bl' | 'bm' | 'br' | 'ml' | 'mr' | 'tl' | 'tm' | 'tr'>,
default() {
return ['tl', 'tm', 'tr', 'mr', 'br', 'bm', 'bl', 'ml'];
},
},
axis: {
type: String,
default: 'both',
validator(val: string) {
return ['both', 'none', 'x', 'y'].includes(val);
},
},
contentClass: {
type: String,
required: false,
default: '',
},
});
const emit = defineEmits([
'clicked',
'dragging',
'dragstop',
'resizing',
'resizestop',
'activated',
'deactivated',
]);
const styleMapping = {
y: {
t: 'top',
m: 'marginTop',
b: 'bottom',
},
x: {
l: 'left',
m: 'marginLeft',
r: 'right',
},
};
function addEvents(events: Map<string, (...args: any[]) => void>) {
events.forEach((cb, eventName) => {
document.documentElement.addEventListener(eventName, cb);
});
}
function removeEvents(events: Map<string, (...args: any[]) => void>) {
events.forEach((cb, eventName) => {
document.documentElement.removeEventListener(eventName, cb);
});
}
const {
stickSize,
parentScaleX,
parentScaleY,
isActive,
preventActiveBehavior,
isDraggable,
isResizable,
aspectRatio,
parentLimitation,
snapToGrid,
gridX,
gridY,
parentW,
parentH,
w,
h,
minw,
minh,
x,
y,
z,
dragHandle,
dragCancel,
sticks,
axis,
contentClass,
} = toRefs(props);
// states
const active = ref(false);
const zIndex = ref<null | number>(null);
const parentWidth = ref<null | number>(null);
const parentHeight = ref<null | number>(null);
const left = ref<null | number>(null);
const top = ref<null | number>(null);
const right = ref<null | number>(null);
const bottom = ref<null | number>(null);
const aspectFactor = ref<null | number>(null);
// state end
const stickDrag = ref(false);
const bodyDrag = ref(false);
const dimensionsBeforeMove = ref({
pointerX: 0,
pointerY: 0,
x: 0,
y: 0,
w: 0,
h: 0,
top: 0,
right: 0,
bottom: 0,
left: 0,
width: 0,
height: 0,
});
const limits = ref({
left: { min: null as null | number, max: null as null | number },
right: { min: null as null | number, max: null as null | number },
top: { min: null as null | number, max: null as null | number },
bottom: { min: null as null | number, max: null as null | number },
});
const currentStick = ref<null | string>(null);
const parentElement = ref<HTMLElement | null>(null);
const width = computed(() => parentWidth.value! - left.value! - right.value!);
const height = computed(() => parentHeight.value! - top.value! - bottom.value!);
const rect = computed(() => ({
left: Math.round(left.value!),
top: Math.round(top.value!),
width: Math.round(width.value),
height: Math.round(height.value),
}));
const saveDimensionsBeforeMove = ({
pointerX,
pointerY,
}: {
pointerX: number;
pointerY: number;
}) => {
dimensionsBeforeMove.value.pointerX = pointerX;
dimensionsBeforeMove.value.pointerY = pointerY;
dimensionsBeforeMove.value.left = left.value as number;
dimensionsBeforeMove.value.right = right.value as number;
dimensionsBeforeMove.value.top = top.value as number;
dimensionsBeforeMove.value.bottom = bottom.value as number;
dimensionsBeforeMove.value.width = width.value as number;
dimensionsBeforeMove.value.height = height.value as number;
aspectFactor.value = width.value / height.value;
};
const sideCorrectionByLimit = (
limit: { max: number; min: number },
current: number,
) => {
let value = current;
if (limit.min !== null && current < limit.min) {
value = limit.min;
} else if (limit.max !== null && limit.max < current) {
value = limit.max;
}
return value;
};
const rectCorrectionByLimit = (rect: {
newBottom: number;
newLeft: number;
newRight: number;
newTop: number;
}) => {
// const { limits } = this;
let { newRight, newLeft, newBottom, newTop } = rect;
type RectRange = {
max: number;
min: number;
};
newLeft = sideCorrectionByLimit(limits.value.left as RectRange, newLeft);
newRight = sideCorrectionByLimit(limits.value.right as RectRange, newRight);
newTop = sideCorrectionByLimit(limits.value.top as RectRange, newTop);
newBottom = sideCorrectionByLimit(
limits.value.bottom as RectRange,
newBottom,
);
return {
newLeft,
newRight,
newTop,
newBottom,
};
};
const rectCorrectionByAspectRatio = (rect: {
newBottom: number;
newLeft: number;
newRight: number;
newTop: number;
}) => {
let { newLeft, newRight, newTop, newBottom } = rect;
// const { parentWidth, parentHeight, currentStick, aspectFactor, dimensionsBeforeMove } = this;
let newWidth = parentWidth.value! - newLeft - newRight;
let newHeight = parentHeight.value! - newTop - newBottom;
if (currentStick.value![1] === 'm') {
const deltaHeight = newHeight - dimensionsBeforeMove.value.height;
newLeft -= (deltaHeight * aspectFactor.value!) / 2;
newRight -= (deltaHeight * aspectFactor.value!) / 2;
} else if (currentStick.value![0] === 'm') {
const deltaWidth = newWidth - dimensionsBeforeMove.value.width;
newTop -= deltaWidth / aspectFactor.value! / 2;
newBottom -= deltaWidth / aspectFactor.value! / 2;
} else if (newWidth / newHeight > aspectFactor.value!) {
newWidth = aspectFactor.value! * newHeight;
if (currentStick.value![1] === 'l') {
newLeft = parentWidth.value! - newRight - newWidth;
} else {
newRight = parentWidth.value! - newLeft - newWidth;
}
} else {
newHeight = newWidth / aspectFactor.value!;
if (currentStick.value![0] === 't') {
newTop = parentHeight.value! - newBottom - newHeight;
} else {
newBottom = parentHeight.value! - newTop - newHeight;
}
}
return { newLeft, newRight, newTop, newBottom };
};
const stickMove = (delta: { x: number; y: number }) => {
let newTop = dimensionsBeforeMove.value.top;
let newBottom = dimensionsBeforeMove.value.bottom;
let newLeft = dimensionsBeforeMove.value.left;
let newRight = dimensionsBeforeMove.value.right;
switch (currentStick.value![0]) {
case 'b': {
newBottom = dimensionsBeforeMove.value.bottom + delta.y;
if (snapToGrid.value) {
newBottom =
(parentHeight.value as number) -
Math.round(
((parentHeight.value as number) - newBottom) / gridY.value,
) *
gridY.value;
}
break;
}
case 't': {
newTop = dimensionsBeforeMove.value.top - delta.y;
if (snapToGrid.value) {
newTop = Math.round(newTop / gridY.value) * gridY.value;
}
break;
}
default: {
break;
}
}
switch (currentStick.value![1]) {
case 'l': {
newLeft = dimensionsBeforeMove.value.left - delta.x;
if (snapToGrid.value) {
newLeft = Math.round(newLeft / gridX.value) * gridX.value;
}
break;
}
case 'r': {
newRight = dimensionsBeforeMove.value.right + delta.x;
if (snapToGrid.value) {
newRight =
(parentWidth.value as number) -
Math.round(((parentWidth.value as number) - newRight) / gridX.value) *
gridX.value;
}
break;
}
default: {
break;
}
}
({ newLeft, newRight, newTop, newBottom } = rectCorrectionByLimit({
newLeft,
newRight,
newTop,
newBottom,
}));
if (aspectRatio.value) {
({ newLeft, newRight, newTop, newBottom } = rectCorrectionByAspectRatio({
newLeft,
newRight,
newTop,
newBottom,
}));
}
left.value = newLeft;
right.value = newRight;
top.value = newTop;
bottom.value = newBottom;
emit('resizing', rect.value);
};
const stickUp = () => {
stickDrag.value = false;
// dimensionsBeforeMove.value = {
// pointerX: 0,
// pointerY: 0,
// x: 0,
// y: 0,
// w: 0,
// h: 0,
// };
Object.assign(dimensionsBeforeMove.value, {
pointerX: 0,
pointerY: 0,
x: 0,
y: 0,
w: 0,
h: 0,
});
limits.value = {
left: { min: null, max: null },
right: { min: null, max: null },
top: { min: null, max: null },
bottom: { min: null, max: null },
};
emit('resizing', rect.value);
emit('resizestop', rect.value);
};
const calcDragLimitation = () => {
return {
left: { min: 0, max: (parentWidth.value as number) - width.value },
right: { min: 0, max: (parentWidth.value as number) - width.value },
top: { min: 0, max: (parentHeight.value as number) - height.value },
bottom: { min: 0, max: (parentHeight.value as number) - height.value },
};
};
const calcResizeLimits = () => {
// const { aspectFactor, width, height, bottom, top, left, right } = this;
const parentLim = parentLimitation.value ? 0 : null;
if (aspectRatio.value) {
if (minw.value / minh.value > (aspectFactor.value as number)) {
minh.value = minw.value / (aspectFactor.value as number);
} else {
minw.value = ((aspectFactor.value as number) * minh.value) as number;
}
}
const limits = {
left: {
min: parentLim,
max: (left.value as number) + (width.value - minw.value),
},
right: {
min: parentLim,
max: (right.value as number) + (width.value - minw.value),
},
top: {
min: parentLim,
max: (top.value as number) + (height.value - minh.value),
},
bottom: {
min: parentLim,
max: (bottom.value as number) + (height.value - minh.value),
},
};
if (aspectRatio.value) {
const aspectLimits = {
left: {
min:
left.value! -
Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
max:
left.value! +
((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
},
right: {
min:
right.value! -
Math.min(top.value!, bottom.value!) * aspectFactor.value! * 2,
max:
right.value! +
((height.value - minh.value!) / 2) * aspectFactor.value! * 2,
},
top: {
min:
top.value! -
(Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
max:
top.value! +
((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
},
bottom: {
min:
bottom.value! -
(Math.min(left.value!, right.value!) / aspectFactor.value!) * 2,
max:
bottom.value! +
((width.value - minw.value) / 2 / aspectFactor.value!) * 2,
},
};
if (currentStick.value![0] === 'm') {
limits.left = {
min: Math.max(limits.left.min!, aspectLimits.left.min),
max: Math.min(limits.left.max, aspectLimits.left.max),
};
limits.right = {
min: Math.max(limits.right.min!, aspectLimits.right.min),
max: Math.min(limits.right.max, aspectLimits.right.max),
};
} else if (currentStick.value![1] === 'm') {
limits.top = {
min: Math.max(limits.top.min!, aspectLimits.top.min),
max: Math.min(limits.top.max, aspectLimits.top.max),
};
limits.bottom = {
min: Math.max(limits.bottom.min!, aspectLimits.bottom.min),
max: Math.min(limits.bottom.max, aspectLimits.bottom.max),
};
}
}
return limits;
};
const positionStyle = computed(() => ({
top: `${top.value}px`,
left: `${left.value}px`,
zIndex: zIndex.value!,
}));
const sizeStyle = computed(() => ({
width: w.value === 'auto' ? 'auto' : `${width.value}px`,
height: h.value === 'auto' ? 'auto' : `${height.value}px`,
}));
const stickStyles = computed(() => (stick: string) => {
const stickStyle = {
width: `${stickSize.value / parentScaleX.value}px`,
height: `${stickSize.value / parentScaleY.value}px`,
};
stickStyle[
styleMapping.y[stick[0] as 'b' | 'm' | 't'] as 'height' | 'width'
] = `${stickSize.value / parentScaleX.value / -2}px`;
stickStyle[
styleMapping.x[stick[1] as 'l' | 'm' | 'r'] as 'height' | 'width'
] = `${stickSize.value / parentScaleX.value / -2}px`;
return stickStyle;
});
const bodyMove = (delta: { x: number; y: number }) => {
let newTop = dimensionsBeforeMove.value.top - delta.y;
let newBottom = dimensionsBeforeMove.value.bottom + delta.y;
let newLeft = dimensionsBeforeMove.value.left - delta.x;
let newRight = dimensionsBeforeMove.value.right + delta.x;
if (snapToGrid.value) {
let alignTop = true;
let alignLeft = true;
let diffT = newTop - Math.floor(newTop / gridY.value) * gridY.value;
let diffB =
(parentHeight.value as number) -
newBottom -
Math.floor(((parentHeight.value as number) - newBottom) / gridY.value) *
gridY.value;
let diffL = newLeft - Math.floor(newLeft / gridX.value) * gridX.value;
let diffR =
(parentWidth.value as number) -
newRight -
Math.floor(((parentWidth.value as number) - newRight) / gridX.value) *
gridX.value;
if (diffT > gridY.value / 2) {
diffT -= gridY.value;
}
if (diffB > gridY.value / 2) {
diffB -= gridY.value;
}
if (diffL > gridX.value / 2) {
diffL -= gridX.value;
}
if (diffR > gridX.value / 2) {
diffR -= gridX.value;
}
if (Math.abs(diffB) < Math.abs(diffT)) {
alignTop = false;
}
if (Math.abs(diffR) < Math.abs(diffL)) {
alignLeft = false;
}
newTop -= alignTop ? diffT : diffB;
newBottom = (parentHeight.value as number) - height.value - newTop;
newLeft -= alignLeft ? diffL : diffR;
newRight = (parentWidth.value as number) - width.value - newLeft;
}
({
newLeft: left.value,
newRight: right.value,
newTop: top.value,
newBottom: bottom.value,
} = rectCorrectionByLimit({ newLeft, newRight, newTop, newBottom }));
emit('dragging', rect.value);
};
const bodyUp = () => {
bodyDrag.value = false;
emit('dragging', rect.value);
emit('dragstop', rect.value);
// dimensionsBeforeMove.value = { pointerX: 0, pointerY: 0, x: 0, y: 0, w: 0, h: 0 };
Object.assign(dimensionsBeforeMove.value, {
pointerX: 0,
pointerY: 0,
x: 0,
y: 0,
w: 0,
h: 0,
});
limits.value = {
left: { min: null, max: null },
right: { min: null, max: null },
top: { min: null, max: null },
bottom: { min: null, max: null },
};
};
const stickDown = (
stick: string,
ev: { pageX: any; pageY: any; touches?: any },
force = false,
) => {
if ((!isResizable.value || !active.value) && !force) {
return;
}
stickDrag.value = true;
const pointerX = ev.pageX === undefined ? ev.touches[0].pageX : ev.pageX;
const pointerY = ev.pageY === undefined ? ev.touches[0].pageY : ev.pageY;
saveDimensionsBeforeMove({ pointerX, pointerY });
currentStick.value = stick;
limits.value = calcResizeLimits();
};
const move = (ev: MouseEvent & TouchEvent) => {
if (!stickDrag.value && !bodyDrag.value) {
return;
}
ev.stopPropagation();
// touches 兼容性代码
const pageX = ev.pageX === undefined ? ev.touches![0]!.pageX : ev.pageX;
const pageY = ev.pageY === undefined ? ev.touches![0]!.pageY : ev.pageY;
const delta = {
x: (dimensionsBeforeMove.value.pointerX - pageX) / parentScaleX.value,
y: (dimensionsBeforeMove.value.pointerY - pageY) / parentScaleY.value,
};
if (stickDrag.value) {
stickMove(delta);
}
if (bodyDrag.value) {
switch (axis.value) {
case 'none': {
return;
}
case 'x': {
delta.y = 0;
break;
}
case 'y': {
delta.x = 0;
break;
}
// No default
}
bodyMove(delta);
}
};
const up = () => {
if (stickDrag.value) {
stickUp();
} else if (bodyDrag.value) {
bodyUp();
}
};
const deselect = () => {
if (preventActiveBehavior.value) {
return;
}
active.value = false;
};
const domEvents = ref(
new Map([
['mousedown', deselect],
['mouseleave', up],
['mousemove', move],
['mouseup', up],
['touchcancel', up],
['touchend', up],
['touchmove', move],
['touchstart', up],
]),
);
const container = ref<HTMLDivElement>();
onMounted(() => {
const currentInstance = getCurrentInstance();
const $el = currentInstance?.vnode.el as HTMLElement;
parentElement.value = $el?.parentNode as HTMLElement;
parentWidth.value = parentW.value ?? parentElement.value?.clientWidth;
parentHeight.value = parentH.value ?? parentElement.value?.clientHeight;
left.value = x.value;
top.value = y.value;
right.value = (parentWidth.value -
(w.value === 'auto' ? container.value!.scrollWidth : (w.value as number)) -
left.value) as number;
bottom.value = (parentHeight.value -
(h.value === 'auto' ? container.value!.scrollHeight : (h.value as number)) -
top.value) as number;
addEvents(domEvents.value);
if (dragHandle.value) {
[...($el?.querySelectorAll(dragHandle.value) || [])].forEach(
(dragHandle) => {
(dragHandle as HTMLElement).dataset.dragHandle = String(
currentInstance?.uid,
);
},
);
}
if (dragCancel.value) {
[...($el?.querySelectorAll(dragCancel.value) || [])].forEach(
(cancelHandle) => {
(cancelHandle as HTMLElement).dataset.dragCancel = String(
currentInstance?.uid,
);
},
);
}
});
onBeforeUnmount(() => {
removeEvents(domEvents.value);
});
const bodyDown = (ev: MouseEvent & TouchEvent) => {
const { target, button } = ev;
if (!preventActiveBehavior.value) {
active.value = true;
}
if (button && button !== 0) {
return;
}
emit('clicked', ev);
if (!active.value) {
return;
}
if (
dragHandle.value &&
(target! as HTMLElement).dataset.dragHandle !==
getCurrentInstance()?.uid.toString()
) {
return;
}
if (
dragCancel.value &&
(target! as HTMLElement).dataset.dragCancel ===
getCurrentInstance()?.uid.toString()
) {
return;
}
if (ev.stopPropagation !== undefined) {
ev.stopPropagation();
}
if (ev.preventDefault !== undefined) {
ev.preventDefault();
}
if (isDraggable.value) {
bodyDrag.value = true;
}
const pointerX = ev.pageX === undefined ? ev.touches[0]!.pageX : ev.pageX;
const pointerY = ev.pageY === undefined ? ev.touches[0]!.pageY : ev.pageY;
saveDimensionsBeforeMove({ pointerX, pointerY });
if (parentLimitation.value) {
limits.value = calcDragLimitation();
}
};
watch(
() => active.value,
(isActive) => {
if (isActive) {
emit('activated');
} else {
emit('deactivated');
}
},
);
watch(
() => isActive.value,
(val) => {
active.value = val;
},
{ immediate: true },
);
watch(
() => z.value,
(val) => {
if ((val as number) >= 0 || val === 'auto') {
zIndex.value = val as number;
}
},
{ immediate: true },
);
watch(
() => x.value,
(newVal, oldVal) => {
if (stickDrag.value || bodyDrag.value || newVal === left.value) {
return;
}
const delta = oldVal - newVal;
bodyDown({ pageX: left.value!, pageY: top.value! } as MouseEvent &
TouchEvent);
bodyMove({ x: delta, y: 0 });
nextTick(() => {
bodyUp();
});
},
);
watch(
() => y.value,
(newVal, oldVal) => {
if (stickDrag.value || bodyDrag.value || newVal === top.value) {
return;
}
const delta = oldVal - newVal;
bodyDown({ pageX: left.value, pageY: top.value } as MouseEvent &
TouchEvent);
bodyMove({ x: 0, y: delta });
nextTick(() => {
bodyUp();
});
},
);
watch(
() => w.value,
(newVal, oldVal) => {
if (stickDrag.value || bodyDrag.value || newVal === width.value) {
return;
}
const stick = 'mr';
const delta = (oldVal as number) - (newVal as number);
stickDown(
stick,
{ pageX: right.value, pageY: top.value! + height.value / 2 },
true,
);
stickMove({ x: delta, y: 0 });
nextTick(() => {
stickUp();
});
},
);
watch(
() => h.value,
(newVal, oldVal) => {
if (stickDrag.value || bodyDrag.value || newVal === height.value) {
return;
}
const stick = 'bm';
const delta = (oldVal as number) - (newVal as number);
stickDown(
stick,
{ pageX: left.value! + width.value / 2, pageY: bottom.value },
true,
);
stickMove({ x: 0, y: delta });
nextTick(() => {
stickUp();
});
},
);
watch(
() => parentW.value,
(val) => {
right.value = val - width.value - left.value!;
parentWidth.value = val;
},
);
watch(
() => parentH.value,
(val) => {
bottom.value = val - height.value - top.value!;
parentHeight.value = val;
},
);
</script>
<template>
<div
:class="`${active || isActive ? 'active' : 'inactive'} ${contentClass ? contentClass : ''}`"
:style="positionStyle"
class="resize"
@mousedown="bodyDown($event as TouchEvent & MouseEvent)"
@touchend="up"
@touchstart="bodyDown($event as TouchEvent & MouseEvent)"
>
<div ref="container" :style="sizeStyle" class="content-container">
<slot></slot>
</div>
<div
v-for="(stick, index) of sticks"
:key="index"
:class="[`resize-stick-${stick}`, isResizable ? '' : 'not-resizable']"
:style="stickStyles(stick)"
class="resize-stick"
@mousedown.stop.prevent="
stickDown(stick, $event as TouchEvent & MouseEvent)
"
@touchstart.stop.prevent="
stickDown(stick, $event as TouchEvent & MouseEvent)
"
></div>
</div>
</template>
<style lang="css" scoped>
.resize {
position: absolute;
box-sizing: border-box;
}
.resize.active::before {
position: absolute;
top: 0;
left: 0;
box-sizing: border-box;
width: 100%;
height: 100%;
content: '';
outline: 1px dashed #d6d6d6;
}
.resize-stick {
position: absolute;
box-sizing: border-box;
font-size: 1px;
background: #fff;
border: 1px solid #6c6c6c;
box-shadow: 0 0 2px #bbb;
}
.inactive .resize-stick {
display: none;
}
.resize-stick-tl,
.resize-stick-br {
cursor: nwse-resize;
}
.resize-stick-tm,
.resize-stick-bm {
left: 50%;
cursor: ns-resize;
}
.resize-stick-tr,
.resize-stick-bl {
cursor: nesw-resize;
}
.resize-stick-ml,
.resize-stick-mr {
top: 50%;
cursor: ew-resize;
}
.resize-stick.not-resizable {
display: none;
}
.content-container {
position: relative;
display: block;
}
</style>