Loading src/common/interfaces/componentsProps.ts +7 −1 Original line number Diff line number Diff line Loading @@ -183,7 +183,13 @@ export interface IPopupProps { } export interface ICropperProps { size?: TSize; src?: string; file?: File; width?: number; height?: number; menuPosition?: 'top' | 'right' | 'bottom' | 'left'; theme?: TThemeColor; darknessTheme?: TDarkness; } export interface IColorPickerProps { Loading src/components/Cropper/Cropper.stories.ts +7 −4 Original line number Diff line number Diff line Loading @@ -14,10 +14,11 @@ const meta: Meta = { }, }, argTypes: { buttonProps: { control: 'object' }, sameButtonColor: { control: 'boolean' }, menuPosition: { control: 'select', options: ['top', 'right', 'bottom', 'left'] }, src: { control: 'text' }, width: { control: 'number' }, height: { control: 'number' }, disabled: { control: 'boolean' }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, }, } satisfies Meta<typeof Cropper>; Loading @@ -26,7 +27,9 @@ export default meta; type Story = StoryObj<typeof meta>; export const Simple: Story = { args: {}, args: { src: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQoFRQjM-wM_nXMA03AGDXgJK3VeX7vtD3ctA&s', }, }; export const Full: Story = { Loading src/components/Cropper/Cropper.vue +105 −3 Original line number Diff line number Diff line <script setup lang="ts"> import type { ICropperProps } from '@interfaces/componentsProps'; import { computed, ref, watch } from 'vue'; import Button from '@components/Button/Button.vue'; import { convertThemeToTextColor } from '@helpers/common'; import SaveIcon from '@icons/Mono/SaveIcon.vue'; import CornerLeftTopIcon from '@icons/Mono/CornerLeftTopIcon.vue'; const props = withDefaults(defineProps<ICropperProps>(), { size: 'normal', disabled: false, width: 300, height: 300, menuPosition: 'top', theme: 'black', darknessTheme: '500', }); const canvas = ref(); const ctx = computed(() => canvas.value && canvas.value.getContext('2d')); const imageSource = computed(() => props.src ?? props.file); const width = computed(() => props.width); const height = computed(() => props.height); const color = computed(() => convertThemeToTextColor(props.theme, props.darknessTheme)); watch( [imageSource, ctx, width, height], () => { if (!imageSource.value) return; const img = new Image(); img.src = props.src ?? URL.createObjectURL(props.file); img.onload = () => { canvas.value.width = width.value ?? 0; canvas.value.height = height.value ?? 0; ctx.value?.drawImage(img, 0, 0, width.value ?? 0, height.value ?? 0); }; }, { immediate: true }, ); </script> <template> <section class="container"></section> <section :class="[ 'container', { flexVertical: menuPosition === 'top' || menuPosition === 'bottom', }, ]" > <div class="canvas-container"> <canvas ref="canvas" id="cropper-canvas"> </canvas> <div class="crop-border left top"> <CornerLeftTopIcon size="16" /> </div> <div class="crop-border right top"></div> <div class="crop-border left bottom"></div> <div class="crop-border right bottom"></div> </div> <div v-show="imageSource" :class="[ 'buttons', { order1: menuPosition === 'top' || menuPosition === 'left', flexVertical: menuPosition === 'right' || menuPosition === 'left', }, ]" > <Button :theme="theme" :darknessTheme="darknessTheme" label="Reset" /> <Button :theme="theme" :darknessTheme="darknessTheme" label="Save"> <SaveIcon :color="color" size="16" /> </Button> </div> </section> </template> <style scoped> .container { display: flex; align-items: center; width: max-content; } .canvas-container { position: relative; line-height: 0; } #cropper-canvas { border: 1px solid black; } .buttons { display: flex; align-items: center; gap: 20px; } .crop-border { position: absolute; z-index: 50; width: 20px; height: 20px; background-color: gray; } .top { top: 0; } .right { right: 0; } .bottom { bottom: 0; } .flexVertical { flex-direction: column; } .order1 { order: -1; } </style> src/components/Cropper/helpers.ts 0 → 100644 +7 −0 Original line number Diff line number Diff line export const getImageInfo = (image: string | File, instance: HTMLImageElement) => { instance.src = typeof image === 'string' ? image : URL.createObjectURL(image); instance.onload = function () { return [instance, 0, 0, instance.width, instance.height]; }; }; src/icons/Mono/CornerLeftTopIcon.vue 0 → 100644 +26 −0 Original line number Diff line number Diff line <script setup lang="ts"> interface Props { color?: string; size?: string | number; } defineProps<Props>(); </script> <template> <svg :width="`${size ?? 40}px`" :height="`${size ?? 40}px`" viewBox="3 3 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" > <path fill-rule="evenodd" clip-rule="evenodd" d="M9.8774 3c-1.101 0-1.9579 0-2.6453.0562-.6979.057-1.265.1744-1.7751.4343-.8467.4314-1.5351 1.1198-1.9665 1.9665-.2599.5101-.3773 1.0772-.4343 1.7751C3 7.9195 3 9 3 10V9.9v2.1c0 .2761.2239.5.5.5s.5-.2239.5-.5V9.9c0-1.1284.0004-1.9446.0528-2.5865.052-.6361.1526-1.0569.3287-1.4025.3355-.6585.871-1.194 1.5295-1.5295.3456-.1761.7664-.2767 1.4025-.3287C7.9554 4.0004 8.7716 4 9.9 4h2.1c.2761 0 .5-.2239.5-.5s-.2239-.5-.5-.5H9.9 9.8774Z" :fill="color ?? '#000000'" /> </svg> </template> <style scoped></style> Loading
src/common/interfaces/componentsProps.ts +7 −1 Original line number Diff line number Diff line Loading @@ -183,7 +183,13 @@ export interface IPopupProps { } export interface ICropperProps { size?: TSize; src?: string; file?: File; width?: number; height?: number; menuPosition?: 'top' | 'right' | 'bottom' | 'left'; theme?: TThemeColor; darknessTheme?: TDarkness; } export interface IColorPickerProps { Loading
src/components/Cropper/Cropper.stories.ts +7 −4 Original line number Diff line number Diff line Loading @@ -14,10 +14,11 @@ const meta: Meta = { }, }, argTypes: { buttonProps: { control: 'object' }, sameButtonColor: { control: 'boolean' }, menuPosition: { control: 'select', options: ['top', 'right', 'bottom', 'left'] }, src: { control: 'text' }, width: { control: 'number' }, height: { control: 'number' }, disabled: { control: 'boolean' }, size: { control: 'select', options: ['small', 'normal', 'large', 'huge'] }, }, } satisfies Meta<typeof Cropper>; Loading @@ -26,7 +27,9 @@ export default meta; type Story = StoryObj<typeof meta>; export const Simple: Story = { args: {}, args: { src: 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQoFRQjM-wM_nXMA03AGDXgJK3VeX7vtD3ctA&s', }, }; export const Full: Story = { Loading
src/components/Cropper/Cropper.vue +105 −3 Original line number Diff line number Diff line <script setup lang="ts"> import type { ICropperProps } from '@interfaces/componentsProps'; import { computed, ref, watch } from 'vue'; import Button from '@components/Button/Button.vue'; import { convertThemeToTextColor } from '@helpers/common'; import SaveIcon from '@icons/Mono/SaveIcon.vue'; import CornerLeftTopIcon from '@icons/Mono/CornerLeftTopIcon.vue'; const props = withDefaults(defineProps<ICropperProps>(), { size: 'normal', disabled: false, width: 300, height: 300, menuPosition: 'top', theme: 'black', darknessTheme: '500', }); const canvas = ref(); const ctx = computed(() => canvas.value && canvas.value.getContext('2d')); const imageSource = computed(() => props.src ?? props.file); const width = computed(() => props.width); const height = computed(() => props.height); const color = computed(() => convertThemeToTextColor(props.theme, props.darknessTheme)); watch( [imageSource, ctx, width, height], () => { if (!imageSource.value) return; const img = new Image(); img.src = props.src ?? URL.createObjectURL(props.file); img.onload = () => { canvas.value.width = width.value ?? 0; canvas.value.height = height.value ?? 0; ctx.value?.drawImage(img, 0, 0, width.value ?? 0, height.value ?? 0); }; }, { immediate: true }, ); </script> <template> <section class="container"></section> <section :class="[ 'container', { flexVertical: menuPosition === 'top' || menuPosition === 'bottom', }, ]" > <div class="canvas-container"> <canvas ref="canvas" id="cropper-canvas"> </canvas> <div class="crop-border left top"> <CornerLeftTopIcon size="16" /> </div> <div class="crop-border right top"></div> <div class="crop-border left bottom"></div> <div class="crop-border right bottom"></div> </div> <div v-show="imageSource" :class="[ 'buttons', { order1: menuPosition === 'top' || menuPosition === 'left', flexVertical: menuPosition === 'right' || menuPosition === 'left', }, ]" > <Button :theme="theme" :darknessTheme="darknessTheme" label="Reset" /> <Button :theme="theme" :darknessTheme="darknessTheme" label="Save"> <SaveIcon :color="color" size="16" /> </Button> </div> </section> </template> <style scoped> .container { display: flex; align-items: center; width: max-content; } .canvas-container { position: relative; line-height: 0; } #cropper-canvas { border: 1px solid black; } .buttons { display: flex; align-items: center; gap: 20px; } .crop-border { position: absolute; z-index: 50; width: 20px; height: 20px; background-color: gray; } .top { top: 0; } .right { right: 0; } .bottom { bottom: 0; } .flexVertical { flex-direction: column; } .order1 { order: -1; } </style>
src/components/Cropper/helpers.ts 0 → 100644 +7 −0 Original line number Diff line number Diff line export const getImageInfo = (image: string | File, instance: HTMLImageElement) => { instance.src = typeof image === 'string' ? image : URL.createObjectURL(image); instance.onload = function () { return [instance, 0, 0, instance.width, instance.height]; }; };
src/icons/Mono/CornerLeftTopIcon.vue 0 → 100644 +26 −0 Original line number Diff line number Diff line <script setup lang="ts"> interface Props { color?: string; size?: string | number; } defineProps<Props>(); </script> <template> <svg :width="`${size ?? 40}px`" :height="`${size ?? 40}px`" viewBox="3 3 10 10" fill="none" xmlns="http://www.w3.org/2000/svg" > <path fill-rule="evenodd" clip-rule="evenodd" d="M9.8774 3c-1.101 0-1.9579 0-2.6453.0562-.6979.057-1.265.1744-1.7751.4343-.8467.4314-1.5351 1.1198-1.9665 1.9665-.2599.5101-.3773 1.0772-.4343 1.7751C3 7.9195 3 9 3 10V9.9v2.1c0 .2761.2239.5.5.5s.5-.2239.5-.5V9.9c0-1.1284.0004-1.9446.0528-2.5865.052-.6361.1526-1.0569.3287-1.4025.3355-.6585.871-1.194 1.5295-1.5295.3456-.1761.7664-.2767 1.4025-.3287C7.9554 4.0004 8.7716 4 9.9 4h2.1c.2761 0 .5-.2239.5-.5s-.2239-.5-.5-.5H9.9 9.8774Z" :fill="color ?? '#000000'" /> </svg> </template> <style scoped></style>