Skip to content

Commit 4fa0815

Browse files
committed
feat(v1-components): UiCopyButton component
1 parent 53e60cb commit 4fa0815

File tree

10 files changed

+337
-0
lines changed

10 files changed

+337
-0
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import type { UiPopperProperties } from '@/common/components/popper'
2+
import type { SIZE } from '@/common/components/button'
3+
4+
export type TooltipOptions = Omit<
5+
UiPopperProperties,
6+
| 'target'
7+
| 'targetTriggers'
8+
| 'popperTriggers'
9+
| 'globalTriggers'
10+
>
11+
12+
export type UiCopyButtonProperties = {
13+
text: string;
14+
size: SIZE;
15+
tooltipOptions?: TooltipOptions;
16+
}

packages/v1-components/src/host.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export { default as UiAvatar } from '@/host/components/avatar/UiAvatar.vue'
22
export { default as UiAvatarList } from '@/host/components/avatar/UiAvatarList.vue'
33
export { default as UiButton } from '@/host/components/button/UiButton.vue'
44
export { default as UiCheckbox } from '@/host/components/checkbox/UiCheckbox.vue'
5+
export { default as UiCopyButton } from '@/host/components/copy-button/UiCopyButton.vue'
56
export { default as UiDate } from '@/host/components/date/UiDate.vue'
67
export { default as UiError } from '@/host/components/error/UiError.vue'
78
export { default as UiImage } from '@/host/components/image/UiImage.vue'
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
<template>
2+
<div
3+
ref="root"
4+
class="ui-v1-copy-button"
5+
@mouseenter="onMouseEnter"
6+
@mouseleave="onMouseLeave"
7+
@click="copy"
8+
>
9+
<slot name="trigger">
10+
<UiButton :size="size" appearance="tertiary">
11+
<IconCopy />
12+
</UiButton>
13+
</slot>
14+
15+
<UiTooltip
16+
v-model:visible="visible"
17+
:target="rootTarget"
18+
:target-triggers="[]"
19+
v-bind="{
20+
delay: { hide: 500 },
21+
...tooltipOptions,
22+
}"
23+
>
24+
<div class="ui-v1-copy-button__tooltip">
25+
<template v-if="copied">
26+
<div class="ui-v1-copy-button__icon">
27+
<IconCheckmarkCircleOutlined />
28+
</div>
29+
30+
<div class="ui-v1-copy-button__text">
31+
<slot name="hint-copied" />
32+
</div>
33+
</template>
34+
35+
<slot v-else name="hint" />
36+
</div>
37+
</UiTooltip>
38+
39+
<input
40+
v-if="!clipboardAvailable"
41+
ref="input"
42+
:value="text"
43+
class="ui-v1-copy-button__area"
44+
type="text"
45+
>
46+
</div>
47+
</template>
48+
49+
<script lang="ts" setup>
50+
import type { PropType } from 'vue'
51+
import type { TooltipOptions } from '@/common/components/copy-button'
52+
53+
import IconCheckmarkCircleOutlined from '~assets/sprites/actions/checkmark-circle-outlined.svg'
54+
import IconCopy from '~assets/sprites/media-and-editing/copy.svg'
55+
import UiButton from '@/host/components/button/UiButton.vue'
56+
import UiTooltip from '@/host/components/tooltip/UiTooltip.vue'
57+
58+
import {
59+
computed,
60+
onMounted,
61+
ref,
62+
} from 'vue'
63+
64+
import { SIZE } from '@/common/components/button'
65+
66+
const props = defineProps({
67+
/** Текст для копирования в буфер обмена */
68+
text: {
69+
type: String,
70+
required: true,
71+
},
72+
73+
/** Размер кнопки */
74+
size: {
75+
type: String as unknown as PropType<SIZE | `${SIZE}`>,
76+
default: SIZE.XS,
77+
},
78+
79+
/** Объект, содержащий параметры настройки Tooltip */
80+
tooltipOptions: {
81+
type: Object as PropType<TooltipOptions>,
82+
default: () => ({}),
83+
},
84+
})
85+
86+
const root = ref<HTMLElement | null>(null)
87+
const rootTarget = computed(() => root)
88+
89+
const input = ref<HTMLInputElement | null>(null)
90+
91+
const visible = ref(false)
92+
const copied = ref(false)
93+
const clipboardAvailable = ref(false)
94+
95+
let copyTimer = null as ReturnType<typeof setTimeout> | null
96+
97+
const emit = defineEmits(['error'])
98+
99+
onMounted(() => {
100+
clipboardAvailable.value = navigator.clipboard && 'writeText' in navigator.clipboard
101+
})
102+
103+
const copy = async () => {
104+
try {
105+
if (clipboardAvailable.value) {
106+
await navigator.clipboard.writeText(props.text)
107+
108+
copied.value = true
109+
} else {
110+
input.value?.focus()
111+
input.value?.select()
112+
113+
copied.value = document.execCommand('copy')
114+
}
115+
} catch (e) {
116+
emit('error', e)
117+
}
118+
}
119+
120+
const onMouseEnter = () => {
121+
visible.value = true
122+
123+
if (copyTimer) {
124+
clearTimeout(copyTimer)
125+
copyTimer = null
126+
}
127+
}
128+
129+
const onMouseLeave = () => {
130+
visible.value = false
131+
132+
if (copyTimer) {
133+
clearTimeout(copyTimer)
134+
copyTimer = null
135+
}
136+
137+
copyTimer = setTimeout(() => copied.value = false, 200)
138+
}
139+
</script>
140+
141+
<style lang="less" src="./copy-button.less" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import type { DefineComponent } from '@/common/vue'
2+
import type { UiCopyButtonProperties } from '@/common/components/copy-button'
3+
4+
declare const UiCopyButton: DefineComponent<UiCopyButtonProperties>
5+
6+
export default UiCopyButton
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
@import (reference) '../../../../assets/stylesheets/palette.less';
2+
3+
.ui-v1-copy-button {
4+
display: inline-block;
5+
vertical-align: top;
6+
position: relative;
7+
8+
&__tooltip {
9+
display: flex;
10+
align-items: center;
11+
}
12+
13+
&__icon {
14+
flex-shrink: 0;
15+
width: 16px;
16+
height: 16px;
17+
color: @green-500;
18+
margin-left: -2px;
19+
margin-right: 4px;
20+
}
21+
22+
&__text {
23+
white-space: nowrap;
24+
text-overflow: ellipsis;
25+
overflow: hidden;
26+
min-width: 0;
27+
}
28+
29+
&__area {
30+
position: absolute;
31+
left: -9999px;
32+
opacity: 0;
33+
width: 10px;
34+
height: 10px;
35+
}
36+
}

packages/v1-components/src/remote.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
export * from '@/remote/components/avatar'
22
export * from '@/remote/components/button'
33
export * from '@/remote/components/checkbox'
4+
export * from '@/remote/components/copy-button'
45
export * from '@/remote/components/error'
56
export * from '@/remote/components/link'
67
export * from '@/remote/components/loader'
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import type { RemoteProperties } from '@/remote/scaffolding'
2+
import type { SchemaType } from '@omnicajs/vue-remote/remote'
3+
4+
import type {
5+
SerializedEvent,
6+
SerializedFocusEvent,
7+
} from '@omnicajs/vue-remote/types/events'
8+
9+
import type { UiCopyButtonProperties } from '@/common/components/copy-button'
10+
11+
import { defineRemoteComponent } from '@omnicajs/vue-remote/remote'
12+
13+
export const UiCopyButtonType = 'UiCopyButton' as SchemaType<
14+
'UiCopyButton',
15+
RemoteProperties<UiCopyButtonProperties>
16+
>
17+
18+
export const UiCopyButton = defineRemoteComponent(
19+
UiCopyButtonType,
20+
['click', 'focus', 'blur'] as unknown as {
21+
'click': (event: SerializedEvent) => boolean,
22+
'focus': (event: SerializedFocusEvent) => boolean,
23+
'blur': (event: SerializedEvent) => boolean,
24+
}
25+
)
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
import ToReact from '../ToReact.ts'
2+
3+
import ExampleBasic from './UiCopyButtonExamples/ExampleBasic.vue'
4+
5+
# UiCopyButton
6+
7+
Кнопки для копирования текста
8+
9+
## Механика
10+
11+
При клике на кнопку (по-умолчанию третичная) копируется заранее заготовленный текст. Кнопка при этом имеет всплывающую
12+
подсказку, контент которой на некоторое время меняется после копирования текста в буфер обмена (индикация, что текст
13+
скопирован).
14+
15+
## Применение
16+
17+
Код:
18+
19+
```html
20+
<template>
21+
<UiCopyButton text="test" size="sm">
22+
<template #hint>
23+
Скопировать
24+
</template>
25+
26+
<template #hint-copied>
27+
Скопировано
28+
</template>
29+
</UiCopyButton>
30+
</template>
31+
32+
<script lang="ts" setup>
33+
import { UiCopyButton } from '@retailcrm/embed-ui-v1-components/remote'
34+
</script>
35+
```
36+
37+
Результат:
38+
39+
<ToReact is={ExampleBasic} />
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import type { Meta, StoryObj } from '@storybook/vue3'
2+
3+
import UiCopyButton from '@/host/components/copy-button/UiCopyButton.vue'
4+
5+
import { SIZE } from '@/common/components/button'
6+
7+
import page from './UiCopyButton.mdx'
8+
9+
const meta = {
10+
id: 'UiCopyButton',
11+
12+
title: 'Components/UiCopyButton',
13+
14+
component: UiCopyButton,
15+
16+
args: {
17+
text: 'Long enough text worth to be copied',
18+
},
19+
20+
argTypes: {
21+
size: {
22+
options: Object.values(SIZE),
23+
},
24+
},
25+
26+
render: (args) => ({
27+
components: {
28+
UiCopyButton,
29+
},
30+
31+
setup: () => ({ args }),
32+
33+
template: `
34+
<UiCopyButton v-bind="args">
35+
<template #hint>
36+
Скопировать
37+
</template>
38+
39+
<template #hint-copied>
40+
Скопировано
41+
</template>
42+
</UiCopyButton>
43+
`,
44+
}),
45+
46+
parameters: {
47+
docs: { page },
48+
layout: 'centered',
49+
},
50+
} satisfies Meta<typeof UiCopyButton>
51+
52+
// noinspection JSUnusedGlobalSymbols
53+
export default meta
54+
55+
type Story = StoryObj<typeof meta>
56+
57+
export const Sandbox: Story = {}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<template>
2+
<UiCopyButton text="test" size="sm">
3+
<template #hint>
4+
Скопировать
5+
</template>
6+
7+
<template #hint-copied>
8+
Скопировано
9+
</template>
10+
</UiCopyButton>
11+
</template>
12+
13+
<script lang="ts" setup>
14+
import UiCopyButton from '@/host/components/copy-button/UiCopyButton.vue'
15+
</script>

0 commit comments

Comments
 (0)