feat(new tool): iban validation and parser (#591)
This commit is contained in:
		
							parent
							
								
									81bfe57cb8
								
							
						
					
					
						commit
						3a63837d3d
					
				
							
								
								
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -32,6 +32,7 @@ declare module '@vue/runtime-core' { | |||||||
|     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] |     Chronometer: typeof import('./src/tools/chronometer/chronometer.vue')['default'] | ||||||
|     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] |     CInputText: typeof import('./src/ui/c-input-text/c-input-text.vue')['default'] | ||||||
|     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] |     'CInputText.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||||
|  |     CKeyValueList: typeof import('./src/ui/c-key-value-list/c-key-value-list.vue')['default'] | ||||||
|     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] |     CLabel: typeof import('./src/ui/c-label/c-label.vue')['default'] | ||||||
|     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] |     CLink: typeof import('./src/ui/c-link/c-link.vue')['default'] | ||||||
|     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] |     'CLink.demo': typeof import('./src/ui/c-link/c-link.demo.vue')['default'] | ||||||
| @ -45,6 +46,10 @@ declare module '@vue/runtime-core' { | |||||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] |     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] | ||||||
|     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] |     CSelect: typeof import('./src/ui/c-select/c-select.vue')['default'] | ||||||
|     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] |     'CSelect.demo': typeof import('./src/ui/c-select/c-select.demo.vue')['default'] | ||||||
|  |     CTextCopyable: typeof import('./src/ui/c-text-copyable/c-text-copyable.vue')['default'] | ||||||
|  |     'CTextCopyable.demo': typeof import('./src/ui/c-text-copyable/c-text-copyable.demo.vue')['default'] | ||||||
|  |     CTooltip: typeof import('./src/ui/c-tooltip/c-tooltip.vue')['default'] | ||||||
|  |     'CTooltip.demo': typeof import('./src/ui/c-tooltip/c-tooltip.demo.vue')['default'] | ||||||
|     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] |     DateTimeConverter: typeof import('./src/tools/date-time-converter/date-time-converter.vue')['default'] | ||||||
|     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] |     'DemoHome.page': typeof import('./src/ui/demo/demo-home.page.vue')['default'] | ||||||
|     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] |     DemoWrapper: typeof import('./src/ui/demo/demo-wrapper.vue')['default'] | ||||||
| @ -68,6 +73,7 @@ declare module '@vue/runtime-core' { | |||||||
|     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] |     HtmlEntities: typeof import('./src/tools/html-entities/html-entities.vue')['default'] | ||||||
|     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] |     HtmlWysiwygEditor: typeof import('./src/tools/html-wysiwyg-editor/html-wysiwyg-editor.vue')['default'] | ||||||
|     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] |     HttpStatusCodes: typeof import('./src/tools/http-status-codes/http-status-codes.vue')['default'] | ||||||
|  |     IbanValidatorAndParser: typeof import('./src/tools/iban-validator-and-parser/iban-validator-and-parser.vue')['default'] | ||||||
|     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] |     'IconMdi:brushVariant': typeof import('~icons/mdi/brush-variant')['default'] | ||||||
|     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] |     'IconMdi:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] |     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||||
|  | |||||||
| @ -58,6 +58,7 @@ | |||||||
|     "fuse.js": "^6.6.2", |     "fuse.js": "^6.6.2", | ||||||
|     "highlight.js": "^11.7.0", |     "highlight.js": "^11.7.0", | ||||||
|     "iarna-toml-esm": "^3.0.5", |     "iarna-toml-esm": "^3.0.5", | ||||||
|  |     "ibantools": "^4.3.3", | ||||||
|     "json5": "^2.2.3", |     "json5": "^2.2.3", | ||||||
|     "jwt-decode": "^3.1.2", |     "jwt-decode": "^3.1.2", | ||||||
|     "libphonenumber-js": "^1.10.28", |     "libphonenumber-js": "^1.10.28", | ||||||
|  | |||||||
							
								
								
									
										7
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										7
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -80,6 +80,9 @@ dependencies: | |||||||
|   iarna-toml-esm: |   iarna-toml-esm: | ||||||
|     specifier: ^3.0.5 |     specifier: ^3.0.5 | ||||||
|     version: 3.0.5 |     version: 3.0.5 | ||||||
|  |   ibantools: | ||||||
|  |     specifier: ^4.3.3 | ||||||
|  |     version: 4.3.3 | ||||||
|   json5: |   json5: | ||||||
|     specifier: ^2.2.3 |     specifier: ^2.2.3 | ||||||
|     version: 2.2.3 |     version: 2.2.3 | ||||||
| @ -5845,6 +5848,10 @@ packages: | |||||||
|       stream: 0.0.2 |       stream: 0.0.2 | ||||||
|     dev: false |     dev: false | ||||||
| 
 | 
 | ||||||
|  |   /ibantools@4.3.3: | ||||||
|  |     resolution: {integrity: sha512-RUTlGuFj3cU/Qfu5YIrsIZjW34/VDgKOz5fDr64Mc4NWP9b2i48vQ39r5xCl1yyFQeyEG/lASstIQHAUX18rRA==} | ||||||
|  |     dev: false | ||||||
|  | 
 | ||||||
|   /iconv-lite@0.6.3: |   /iconv-lite@0.6.3: | ||||||
|     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} |     resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} | ||||||
|     engines: {node: '>=0.10.0'} |     engines: {node: '>=0.10.0'} | ||||||
|  | |||||||
| @ -0,0 +1,51 @@ | |||||||
|  | import { type Page, expect, test } from '@playwright/test'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | 
 | ||||||
|  | async function extractIbanInfo({ page }: { page: Page }) { | ||||||
|  |   const tdHandles = await page.locator('table tr td').elementHandles(); | ||||||
|  |   const tdTextContents = await Promise.all(tdHandles.map(el => el.textContent())); | ||||||
|  | 
 | ||||||
|  |   return _.chain(tdTextContents) | ||||||
|  |     .map(tdTextContent => tdTextContent?.trim().replace(' Copy to clipboard', '')) | ||||||
|  |     .chunk(2) | ||||||
|  |     .value(); | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | test.describe('Tool - Iban validator and parser', () => { | ||||||
|  |   test.beforeEach(async ({ page }) => { | ||||||
|  |     await page.goto('/iban-validator-and-parser'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('Has correct title', async ({ page }) => { | ||||||
|  |     await expect(page).toHaveTitle('IBAN validator and parser - IT Tools'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('iban info are extracted from a valid iban', async ({ page }) => { | ||||||
|  |     await page.getByTestId('iban-input').fill('DE89370400440532013000'); | ||||||
|  | 
 | ||||||
|  |     const ibanInfo = await extractIbanInfo({ page }); | ||||||
|  | 
 | ||||||
|  |     expect(ibanInfo).toEqual([ | ||||||
|  |       ['Is IBAN valid ?', 'Yes'], | ||||||
|  |       ['Is IBAN a QR-IBAN ?', 'No'], | ||||||
|  |       ['Country code', 'DE'], | ||||||
|  |       ['BBAN', '370400440532013000'], | ||||||
|  |       ['IBAN friendly format', 'DE89 3704 0044 0532 0130 00'], | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   test('invalid iban errors are displayed', async ({ page }) => { | ||||||
|  |     await page.getByTestId('iban-input').fill('FR7630006060011234567890189'); | ||||||
|  | 
 | ||||||
|  |     const ibanInfo = await extractIbanInfo({ page }); | ||||||
|  | 
 | ||||||
|  |     expect(ibanInfo).toEqual([ | ||||||
|  |       ['Is IBAN valid ?', 'No'], | ||||||
|  |       ['IBAN errors', 'Wrong account bank branch checksumWrong IBAN checksum Copy to clipboard'], | ||||||
|  |       ['Is IBAN a QR-IBAN ?', 'No'], | ||||||
|  |       ['Country code', 'N/A'], | ||||||
|  |       ['BBAN', 'N/A'], | ||||||
|  |       ['IBAN friendly format', 'FR76 3000 6060 0112 3456 7890 189'], | ||||||
|  |     ]); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | import { ValidationErrorsIBAN } from 'ibantools'; | ||||||
|  | 
 | ||||||
|  | export { getFriendlyErrors }; | ||||||
|  | 
 | ||||||
|  | const ibanErrorToMessage = { | ||||||
|  |   [ValidationErrorsIBAN.NoIBANProvided]: 'No IBAN provided', | ||||||
|  |   [ValidationErrorsIBAN.NoIBANCountry]: 'No IBAN country', | ||||||
|  |   [ValidationErrorsIBAN.WrongBBANLength]: 'Wrong BBAN length', | ||||||
|  |   [ValidationErrorsIBAN.WrongBBANFormat]: 'Wrong BBAN format', | ||||||
|  |   [ValidationErrorsIBAN.ChecksumNotNumber]: 'Checksum is not a number', | ||||||
|  |   [ValidationErrorsIBAN.WrongIBANChecksum]: 'Wrong IBAN checksum', | ||||||
|  |   [ValidationErrorsIBAN.WrongAccountBankBranchChecksum]: 'Wrong account bank branch checksum', | ||||||
|  |   [ValidationErrorsIBAN.QRIBANNotAllowed]: 'QR-IBAN not allowed', | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | function getFriendlyErrors(errorCodes: ValidationErrorsIBAN[]) { | ||||||
|  |   return errorCodes.map(errorCode => ibanErrorToMessage[errorCode]).filter(Boolean); | ||||||
|  | } | ||||||
| @ -0,0 +1,71 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { extractIBAN, friendlyFormatIBAN, isQRIBAN, validateIBAN } from 'ibantools'; | ||||||
|  | import { getFriendlyErrors } from './iban-validator-and-parser.service'; | ||||||
|  | import type { CKeyValueListItems } from '@/ui/c-key-value-list/c-key-value-list.types'; | ||||||
|  | 
 | ||||||
|  | const rawIban = ref(''); | ||||||
|  | 
 | ||||||
|  | const ibanInfo = computed<CKeyValueListItems>(() => { | ||||||
|  |   const iban = rawIban.value.toUpperCase().replace(/\s/g, '').replace(/-/g, ''); | ||||||
|  | 
 | ||||||
|  |   if (iban === '') { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   const { valid: isIbanValid, errorCodes } = validateIBAN(iban); | ||||||
|  |   const { countryCode, bban } = extractIBAN(iban); | ||||||
|  |   const errors = getFriendlyErrors(errorCodes); | ||||||
|  | 
 | ||||||
|  |   return [ | ||||||
|  | 
 | ||||||
|  |     { | ||||||
|  |       label: 'Is IBAN valid ?', | ||||||
|  |       value: isIbanValid, | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'IBAN errors', | ||||||
|  |       value: errors.length === 0 ? undefined : errors, | ||||||
|  |       hideOnNil: true, | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'Is IBAN a QR-IBAN ?', | ||||||
|  |       value: isQRIBAN(iban), | ||||||
|  |       showCopyButton: false, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'Country code', | ||||||
|  |       value: countryCode, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'BBAN', | ||||||
|  |       value: bban, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       label: 'IBAN friendly format', | ||||||
|  |       value: friendlyFormatIBAN(iban), | ||||||
|  |     }, | ||||||
|  |   ]; | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const ibanExamples = [ | ||||||
|  |   'FR7630006000011234567890189', | ||||||
|  |   'DE89370400440532013000', | ||||||
|  |   'GB29NWBK60161331926819', | ||||||
|  | ]; | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-input-text v-model:value="rawIban" placeholder="Enter an IBAN to check for validity..." test-id="iban-input" /> | ||||||
|  | 
 | ||||||
|  |     <c-key-value-list :items="ibanInfo" my-5 /> | ||||||
|  | 
 | ||||||
|  |     <c-card title="Valid IBAN examples"> | ||||||
|  |       <div v-for="iban in ibanExamples" :key="iban"> | ||||||
|  |         <c-text-copyable :value="iban" font-mono :displayed-value="friendlyFormatIBAN(iban)" /> | ||||||
|  |       </div> | ||||||
|  |     </c-card> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										12
									
								
								src/tools/iban-validator-and-parser/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								src/tools/iban-validator-and-parser/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,12 @@ | |||||||
|  | import { defineTool } from '../tool'; | ||||||
|  | import Bank from '~icons/mdi/bank'; | ||||||
|  | 
 | ||||||
|  | export const tool = defineTool({ | ||||||
|  |   name: 'IBAN validator and parser', | ||||||
|  |   path: '/iban-validator-and-parser', | ||||||
|  |   description: 'Validate and parse IBAN numbers. Check if IBAN is valid and get the country, BBAN, if it is a QR-IBAN and the IBAN friendly format.', | ||||||
|  |   keywords: ['iban', 'validator', 'and', 'parser', 'bic', 'bank'], | ||||||
|  |   component: () => import('./iban-validator-and-parser.vue'), | ||||||
|  |   icon: Bank, | ||||||
|  |   createdAt: new Date('2023-08-26'), | ||||||
|  | }); | ||||||
| @ -1,6 +1,7 @@ | |||||||
| import { tool as base64FileConverter } from './base64-file-converter'; | import { tool as base64FileConverter } from './base64-file-converter'; | ||||||
| import { tool as base64StringConverter } from './base64-string-converter'; | import { tool as base64StringConverter } from './base64-string-converter'; | ||||||
| import { tool as basicAuthGenerator } from './basic-auth-generator'; | import { tool as basicAuthGenerator } from './basic-auth-generator'; | ||||||
|  | import { tool as ibanValidatorAndParser } from './iban-validator-and-parser'; | ||||||
| import { tool as stringObfuscator } from './string-obfuscator'; | import { tool as stringObfuscator } from './string-obfuscator'; | ||||||
| import { tool as textDiff } from './text-diff'; | import { tool as textDiff } from './text-diff'; | ||||||
| import { tool as emojiPicker } from './emoji-picker'; | import { tool as emojiPicker } from './emoji-picker'; | ||||||
| @ -151,7 +152,7 @@ export const toolsByCategory: ToolCategory[] = [ | |||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     name: 'Data', |     name: 'Data', | ||||||
|     components: [phoneParserAndFormatter], |     components: [phoneParserAndFormatter, ibanValidatorAndParser], | ||||||
|   }, |   }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
							
								
								
									
										9
									
								
								src/ui/c-key-value-list/c-key-value-list.types.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/ui/c-key-value-list/c-key-value-list.types.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | |||||||
|  | export interface CKeyValueListItem { | ||||||
|  |   label: string | ||||||
|  |   value: string | string[] | number | boolean | undefined | null | ||||||
|  |   hideOnNil?: boolean | ||||||
|  |   placeholder?: string | ||||||
|  |   showCopyButton?: boolean | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export type CKeyValueListItems = CKeyValueListItem[]; | ||||||
							
								
								
									
										37
									
								
								src/ui/c-key-value-list/c-key-value-list.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/ui/c-key-value-list/c-key-value-list.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | <script lang="ts" setup> | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import type { CKeyValueListItems } from './c-key-value-list.types'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ items?: CKeyValueListItems }>(), { items: () => [] }); | ||||||
|  | const { items } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const formattedItems = computed(() => items.value.filter(item => !_.isNil(item.value) || !item.hideOnNil)); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <table border-collapse table-fixed> | ||||||
|  |     <tr v-for="item in formattedItems" :key="item.label"> | ||||||
|  |       <td py-1 pr-2 text-right font-bold> | ||||||
|  |         {{ item.label }} | ||||||
|  |       </td> | ||||||
|  | 
 | ||||||
|  |       <td v-if="_.isArray(item.value)"> | ||||||
|  |         <div v-for="value in item.value" :key="value"> | ||||||
|  |           <c-text-copyable :value="value" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |         </div> | ||||||
|  |       </td> | ||||||
|  |       <td v-else-if="_.isBoolean(item.value)"> | ||||||
|  |         <c-text-copyable :value="item.value ? 'true' : 'false'" :displayed-value="item.value ? 'Yes' : 'No'" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |       </td> | ||||||
|  |       <td v-else-if="_.isNumber(item.value)" font-mono> | ||||||
|  |         <c-text-copyable :value="String(item.value)" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |       </td> | ||||||
|  |       <td v-else-if="_.isNil(item.value) || item.value === ''" op-70> | ||||||
|  |         {{ item.placeholder ?? 'N/A' }} | ||||||
|  |       </td> | ||||||
|  |       <td v-else> | ||||||
|  |         <c-text-copyable :value="item.value" :show-icon="item.showCopyButton ?? true" /> | ||||||
|  |       </td> | ||||||
|  |     </tr> | ||||||
|  |   </table> | ||||||
|  | </template> | ||||||
							
								
								
									
										3
									
								
								src/ui/c-text-copyable/c-text-copyable.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/ui/c-text-copyable/c-text-copyable.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | <template> | ||||||
|  |   <c-text-copyable value="value" displayed-value="displayedValue" /> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/ui/c-text-copyable/c-text-copyable.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/ui/c-text-copyable/c-text-copyable.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | import { useCopy } from '@/composable/copy'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults(defineProps<{ value?: string; displayedValue?: string; showIcon?: boolean }>(), { value: '', displayedValue: undefined, showIcon: true }); | ||||||
|  | const { value, displayedValue, showIcon } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const { copy, isJustCopied } = useCopy({ source: value, createToast: false }); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <c-tooltip :tooltip="isJustCopied ? 'Copied!' : 'Copy to clipboard'" cursor-pointer @click="copy"> | ||||||
|  |     <span flex items-center gap-2> | ||||||
|  |       {{ displayedValue ?? value }} | ||||||
|  |       <icon-mdi-content-copy v-if="showIcon" op-40 /> | ||||||
|  |     </span> | ||||||
|  |   </c-tooltip> | ||||||
|  | </template> | ||||||
							
								
								
									
										17
									
								
								src/ui/c-tooltip/c-tooltip.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/ui/c-tooltip/c-tooltip.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,17 @@ | |||||||
|  | <template> | ||||||
|  |   <div> | ||||||
|  |     <c-tooltip> | ||||||
|  |       Hover me | ||||||
|  | 
 | ||||||
|  |       <template #tooltip> | ||||||
|  |         Tooltip content | ||||||
|  |       </template> | ||||||
|  |     </c-tooltip> | ||||||
|  |   </div> | ||||||
|  | 
 | ||||||
|  |   <div mt-5> | ||||||
|  |     <c-tooltip tooltip="Tooltip content"> | ||||||
|  |       Hover me | ||||||
|  |     </c-tooltip> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
							
								
								
									
										27
									
								
								src/ui/c-tooltip/c-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								src/ui/c-tooltip/c-tooltip.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,27 @@ | |||||||
|  | <script setup lang="ts"> | ||||||
|  | const props = withDefaults(defineProps<{ tooltip?: string }>(), { tooltip: '' }); | ||||||
|  | const { tooltip } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const targetRef = ref(); | ||||||
|  | const isTargetHovered = useElementHover(targetRef); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <template> | ||||||
|  |   <div class="relative" inline-block> | ||||||
|  |     <div ref="targetRef"> | ||||||
|  |       <slot /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <div | ||||||
|  |       class="absolute bottom-100% left-50% z-10 mb-5px whitespace-nowrap rounded bg-black px-12px py-6px text-sm text-white shadow-lg transition transition transition-duration-0.2s -translate-x-1/2" | ||||||
|  |       :class="{ | ||||||
|  |         'op-0 scale-0': isTargetHovered === false, | ||||||
|  |         'op-100 scale-100': isTargetHovered, | ||||||
|  |       }" | ||||||
|  |     > | ||||||
|  |       <slot name="tooltip"> | ||||||
|  |         {{ tooltip }} | ||||||
|  |       </slot> | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user