ui-lib(new-component): added text input component in the c-lib
This commit is contained in:
		
							parent
							
								
									401f13f7e3
								
							
						
					
					
						commit
						aad8d84e13
					
				
							
								
								
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -26,11 +26,15 @@ declare module '@vue/runtime-core' { | |||||||
|     'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] |     'CCard.demo': typeof import('./src/ui/c-card/c-card.demo.vue')['default'] | ||||||
|     ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] |     ChmodCalculator: typeof import('./src/tools/chmod-calculator/chmod-calculator.vue')['default'] | ||||||
|     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.demo': typeof import('./src/ui/c-input-text/c-input-text.demo.vue')['default'] | ||||||
|  |     'CInputText.theme': typeof import('./src/ui/c-input-text/c-input-text.theme.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'] | ||||||
|     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] |     CollapsibleToolMenu: typeof import('./src/components/CollapsibleToolMenu.vue')['default'] | ||||||
|     ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] |     ColorConverter: typeof import('./src/tools/color-converter/color-converter.vue')['default'] | ||||||
|     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] |     ColoredCard: typeof import('./src/components/ColoredCard.vue')['default'] | ||||||
|  |     copy: typeof import('./src/ui/c-input-text/c-input-text copy.vue')['default'] | ||||||
|     CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default'] |     CopyableIpLike: typeof import('./src/tools/ipv4-subnet-calculator/copyable-ip-like.vue')['default'] | ||||||
|     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.vue')['default'] |     CrontabGenerator: typeof import('./src/tools/crontab-generator/crontab-generator.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'] | ||||||
| @ -52,6 +56,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'] | ||||||
|  |     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||||
|     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] |     InputCopyable: typeof import('./src/components/InputCopyable.vue')['default'] | ||||||
|     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] |     IntegerBaseConverter: typeof import('./src/tools/integer-base-converter/integer-base-converter.vue')['default'] | ||||||
|     Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] |     Ipv4AddressConverter: typeof import('./src/tools/ipv4-address-converter/ipv4-address-converter.vue')['default'] | ||||||
|  | |||||||
| @ -79,6 +79,7 @@ | |||||||
|     "yaml": "^2.2.1" |     "yaml": "^2.2.1" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|  |     "@iconify-json/mdi": "^1.1.50", | ||||||
|     "@playwright/test": "^1.32.3", |     "@playwright/test": "^1.32.3", | ||||||
|     "@rushstack/eslint-patch": "^1.2.0", |     "@rushstack/eslint-patch": "^1.2.0", | ||||||
|     "@types/bcryptjs": "^2.4.2", |     "@types/bcryptjs": "^2.4.2", | ||||||
| @ -98,8 +99,10 @@ | |||||||
|     "@unocss/eslint-config": "^0.50.8", |     "@unocss/eslint-config": "^0.50.8", | ||||||
|     "@vitejs/plugin-vue": "^2.3.4", |     "@vitejs/plugin-vue": "^2.3.4", | ||||||
|     "@vitejs/plugin-vue-jsx": "^1.3.10", |     "@vitejs/plugin-vue-jsx": "^1.3.10", | ||||||
|  |     "@vue/compiler-sfc": "^3.2.47", | ||||||
|     "@vue/eslint-config-prettier": "^7.1.0", |     "@vue/eslint-config-prettier": "^7.1.0", | ||||||
|     "@vue/eslint-config-typescript": "^10.0.0", |     "@vue/eslint-config-typescript": "^10.0.0", | ||||||
|  |     "@vue/runtime-core": "^3.2.47", | ||||||
|     "@vue/test-utils": "^2.3.2", |     "@vue/test-utils": "^2.3.2", | ||||||
|     "@vue/tsconfig": "^0.1.3", |     "@vue/tsconfig": "^0.1.3", | ||||||
|     "c8": "^7.13.0", |     "c8": "^7.13.0", | ||||||
| @ -116,6 +119,7 @@ | |||||||
|     "typescript": "~4.5.5", |     "typescript": "~4.5.5", | ||||||
|     "unocss": "^0.50.8", |     "unocss": "^0.50.8", | ||||||
|     "unplugin-auto-import": "^0.15.2", |     "unplugin-auto-import": "^0.15.2", | ||||||
|  |     "unplugin-icons": "^0.16.1", | ||||||
|     "unplugin-vue-components": "^0.24.1", |     "unplugin-vue-components": "^0.24.1", | ||||||
|     "vite": "^2.9.15", |     "vite": "^2.9.15", | ||||||
|     "vite-plugin-md": "^0.12.4", |     "vite-plugin-md": "^0.12.4", | ||||||
|  | |||||||
							
								
								
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										47
									
								
								pnpm-lock.yaml
									
									
									
										generated
									
									
									
								
							| @ -135,6 +135,9 @@ dependencies: | |||||||
|     version: 2.2.1 |     version: 2.2.1 | ||||||
| 
 | 
 | ||||||
| devDependencies: | devDependencies: | ||||||
|  |   '@iconify-json/mdi': | ||||||
|  |     specifier: ^1.1.50 | ||||||
|  |     version: 1.1.50 | ||||||
|   '@playwright/test': |   '@playwright/test': | ||||||
|     specifier: ^1.32.3 |     specifier: ^1.32.3 | ||||||
|     version: 1.32.3 |     version: 1.32.3 | ||||||
| @ -192,12 +195,18 @@ devDependencies: | |||||||
|   '@vitejs/plugin-vue-jsx': |   '@vitejs/plugin-vue-jsx': | ||||||
|     specifier: ^1.3.10 |     specifier: ^1.3.10 | ||||||
|     version: 1.3.10 |     version: 1.3.10 | ||||||
|  |   '@vue/compiler-sfc': | ||||||
|  |     specifier: ^3.2.47 | ||||||
|  |     version: 3.2.47 | ||||||
|   '@vue/eslint-config-prettier': |   '@vue/eslint-config-prettier': | ||||||
|     specifier: ^7.1.0 |     specifier: ^7.1.0 | ||||||
|     version: 7.1.0(eslint@8.38.0)(prettier@2.8.7) |     version: 7.1.0(eslint@8.38.0)(prettier@2.8.7) | ||||||
|   '@vue/eslint-config-typescript': |   '@vue/eslint-config-typescript': | ||||||
|     specifier: ^10.0.0 |     specifier: ^10.0.0 | ||||||
|     version: 10.0.0(eslint-plugin-vue@8.7.1)(eslint@8.38.0)(typescript@4.5.5) |     version: 10.0.0(eslint-plugin-vue@8.7.1)(eslint@8.38.0)(typescript@4.5.5) | ||||||
|  |   '@vue/runtime-core': | ||||||
|  |     specifier: ^3.2.47 | ||||||
|  |     version: 3.2.47 | ||||||
|   '@vue/test-utils': |   '@vue/test-utils': | ||||||
|     specifier: ^2.3.2 |     specifier: ^2.3.2 | ||||||
|     version: 2.3.2(vue@3.2.47) |     version: 2.3.2(vue@3.2.47) | ||||||
| @ -246,6 +255,9 @@ devDependencies: | |||||||
|   unplugin-auto-import: |   unplugin-auto-import: | ||||||
|     specifier: ^0.15.2 |     specifier: ^0.15.2 | ||||||
|     version: 0.15.2(@vueuse/core@8.9.4)(rollup@2.79.1) |     version: 0.15.2(@vueuse/core@8.9.4)(rollup@2.79.1) | ||||||
|  |   unplugin-icons: | ||||||
|  |     specifier: ^0.16.1 | ||||||
|  |     version: 0.16.1(@vue/compiler-sfc@3.2.47) | ||||||
|   unplugin-vue-components: |   unplugin-vue-components: | ||||||
|     specifier: ^0.24.1 |     specifier: ^0.24.1 | ||||||
|     version: 0.24.1(rollup@2.79.1)(vue@3.2.47) |     version: 0.24.1(rollup@2.79.1)(vue@3.2.47) | ||||||
| @ -1612,6 +1624,12 @@ packages: | |||||||
|     resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} |     resolution: {integrity: sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==} | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /@iconify-json/mdi@1.1.50: | ||||||
|  |     resolution: {integrity: sha512-SgbT5w5eHCdOG74ZWPz7HlTGk6VsifIJhNi6lAsxj/5Nlqt6Cz4LlQmSa9eecU9p075Jub2aAx/o7YI+GCahRQ==} | ||||||
|  |     dependencies: | ||||||
|  |       '@iconify/types': 2.0.0 | ||||||
|  |     dev: true | ||||||
|  | 
 | ||||||
|   /@iconify/types@2.0.0: |   /@iconify/types@2.0.0: | ||||||
|     resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} |     resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} | ||||||
|     dev: true |     dev: true | ||||||
| @ -7561,6 +7579,35 @@ packages: | |||||||
|       - rollup |       - rollup | ||||||
|     dev: true |     dev: true | ||||||
| 
 | 
 | ||||||
|  |   /unplugin-icons@0.16.1(@vue/compiler-sfc@3.2.47): | ||||||
|  |     resolution: {integrity: sha512-qTunFUkpAyDnwzwV7YV1ZgCWRYfLuURcCurhhXOWMy2ipY88qx1pADvral2hJu4Xymh0X0t3Zcll3BIru2AVLQ==} | ||||||
|  |     peerDependencies: | ||||||
|  |       '@svgr/core': '>=7.0.0' | ||||||
|  |       '@vue/compiler-sfc': ^3.0.2 || ^2.7.0 | ||||||
|  |       vue-template-compiler: ^2.6.12 | ||||||
|  |       vue-template-es2015-compiler: ^1.9.0 | ||||||
|  |     peerDependenciesMeta: | ||||||
|  |       '@svgr/core': | ||||||
|  |         optional: true | ||||||
|  |       '@vue/compiler-sfc': | ||||||
|  |         optional: true | ||||||
|  |       vue-template-compiler: | ||||||
|  |         optional: true | ||||||
|  |       vue-template-es2015-compiler: | ||||||
|  |         optional: true | ||||||
|  |     dependencies: | ||||||
|  |       '@antfu/install-pkg': 0.1.1 | ||||||
|  |       '@antfu/utils': 0.7.2 | ||||||
|  |       '@iconify/utils': 2.1.5 | ||||||
|  |       '@vue/compiler-sfc': 3.2.47 | ||||||
|  |       debug: 4.3.4 | ||||||
|  |       kolorist: 1.7.0 | ||||||
|  |       local-pkg: 0.4.3 | ||||||
|  |       unplugin: 1.3.1 | ||||||
|  |     transitivePeerDependencies: | ||||||
|  |       - supports-color | ||||||
|  |     dev: true | ||||||
|  | 
 | ||||||
|   /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47): |   /unplugin-vue-components@0.24.1(rollup@2.79.1)(vue@3.2.47): | ||||||
|     resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} |     resolution: {integrity: sha512-T3A8HkZoIE1Cja95xNqolwza0yD5IVlgZZ1PVAGvVCx8xthmjsv38xWRCtHtwl+rvZyL9uif42SRkDGw9aCfMA==} | ||||||
|     engines: {node: '>=14'} |     engines: {node: '>=14'} | ||||||
|  | |||||||
| @ -1,3 +1,4 @@ | |||||||
|  | import { get, type MaybeRef } from '@vueuse/core'; | ||||||
| import _ from 'lodash'; | import _ from 'lodash'; | ||||||
| import { reactive, watch, type Ref } from 'vue'; | import { reactive, watch, type Ref } from 'vue'; | ||||||
| 
 | 
 | ||||||
| @ -31,7 +32,7 @@ export function useValidation<T>({ | |||||||
|   watch: watchRefs = [], |   watch: watchRefs = [], | ||||||
| }: { | }: { | ||||||
|   source: Ref<T>; |   source: Ref<T>; | ||||||
|   rules: UseValidationRule<T>[]; |   rules: MaybeRef<UseValidationRule<T>[]>; | ||||||
|   watch?: Ref<unknown>[]; |   watch?: Ref<unknown>[]; | ||||||
| }) { | }) { | ||||||
|   const state = reactive<{ |   const state = reactive<{ | ||||||
| @ -55,7 +56,7 @@ export function useValidation<T>({ | |||||||
|       state.message = ''; |       state.message = ''; | ||||||
|       state.status = undefined; |       state.status = undefined; | ||||||
| 
 | 
 | ||||||
|       for (const rule of rules) { |       for (const rule of get(rules)) { | ||||||
|         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { |         if (isFalsyOrHasThrown(() => rule.validator(source.value))) { | ||||||
|           state.message = rule.message; |           state.message = rule.message; | ||||||
|           state.status = 'error'; |           state.status = 'error'; | ||||||
|  | |||||||
							
								
								
									
										39
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/ui/c-input-text/c-input-text.demo.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,39 @@ | |||||||
|  | <template> | ||||||
|  |   <h2>Default</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text value="qsd" /> | ||||||
|  | 
 | ||||||
|  |   <h2>With placeholder</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text placeholder="Placeholder" /> | ||||||
|  | 
 | ||||||
|  |   <h2>With label</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text label="Label" mb-2 /> | ||||||
|  |   <c-input-text label="Label" mb-2 label-position="left" /> | ||||||
|  |   <c-input-text label="Label" mb-2 label-position="left" label-width="100px" /> | ||||||
|  |   <c-input-text label="Label" mb-2 label-position="left" label-width="100px" label-align="right" /> | ||||||
|  | 
 | ||||||
|  |   <h2>Readonly</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text value="value" readonly /> | ||||||
|  | 
 | ||||||
|  |   <h2>Disabled</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text value="value" disabled /> | ||||||
|  | 
 | ||||||
|  |   <h2>Validation</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text | ||||||
|  |     v-model:value="value" | ||||||
|  |     :validation-rules="[{ message: 'Length must be > 10', validator: (value) => value.length > 10 }]" | ||||||
|  |   /> | ||||||
|  | 
 | ||||||
|  |   <h2>Clearable</h2> | ||||||
|  | 
 | ||||||
|  |   <c-input-text v-model:value="value" clearable /> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | const value = ref('value'); | ||||||
|  | </script> | ||||||
							
								
								
									
										87
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										87
									
								
								src/ui/c-input-text/c-input-text.test.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,87 @@ | |||||||
|  | import { describe, expect, it, beforeEach } from 'vitest'; | ||||||
|  | import { shallowMount } from '@vue/test-utils'; | ||||||
|  | import { setActivePinia, createPinia } from 'pinia'; | ||||||
|  | import _ from 'lodash'; | ||||||
|  | import CInputText from './c-input-text.vue'; | ||||||
|  | 
 | ||||||
|  | describe('CInputText', () => { | ||||||
|  |   beforeEach(() => { | ||||||
|  |     setActivePinia(createPinia()); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Renders a label', () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { | ||||||
|  |         label: 'Label', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.get('.label').text()).to.equal('Label'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Renders a placeholder', () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { | ||||||
|  |         placeholder: 'Placeholder', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.get('.input').attributes('placeholder')).to.equal('Placeholder'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Renders a value', () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { | ||||||
|  |         value: 'Value', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.vm.value).to.equal('Value'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('Renders a provided id', () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { | ||||||
|  |         id: 'id', | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.get('.input').attributes('id')).to.equal('id'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('updates value on input', async () => { | ||||||
|  |     const wrapper = shallowMount(CInputText); | ||||||
|  | 
 | ||||||
|  |     await wrapper.get('input').setValue('Hello'); | ||||||
|  | 
 | ||||||
|  |     expect(_.get(wrapper.emitted(), 'update:value.0.0')).to.equal('Hello'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('cannot be edited when disabled', async () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { | ||||||
|  |         disabled: true, | ||||||
|  |       }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     await wrapper.get('input').setValue('Hello'); | ||||||
|  | 
 | ||||||
|  |     expect(_.get(wrapper.emitted(), 'update:value')).toBeUndefined(); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('renders a feedback message for invalid rules', async () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { rules: [{ validator: () => false, message: 'Message' }] }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.get('.feedback').text()).to.equal('Message'); | ||||||
|  |   }); | ||||||
|  | 
 | ||||||
|  |   it('feedback does not render for valid rules', async () => { | ||||||
|  |     const wrapper = shallowMount(CInputText, { | ||||||
|  |       props: { rules: [{ validator: () => true, message: 'Message' }] }, | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
|  |     expect(wrapper.find('.feedback').exists()).to.equal(false); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
							
								
								
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/ui/c-input-text/c-input-text.theme.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,20 @@ | |||||||
|  | import { defineThemes } from '../theme/theme.models'; | ||||||
|  | 
 | ||||||
|  | export const { useTheme } = defineThemes({ | ||||||
|  |   dark: { | ||||||
|  |     backgroundColor: '#333333', | ||||||
|  |     borderColor: '#333333', | ||||||
|  | 
 | ||||||
|  |     focus: { | ||||||
|  |       backgroundColor: '#1ea54c1a', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   light: { | ||||||
|  |     backgroundColor: '#ffffff', | ||||||
|  |     borderColor: '#e0e0e69e', | ||||||
|  | 
 | ||||||
|  |     focus: { | ||||||
|  |       backgroundColor: '#ffffff', | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  | }); | ||||||
							
								
								
									
										198
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										198
									
								
								src/ui/c-input-text/c-input-text.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,198 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="c-input-text" :class="{ disabled, error: !validation.isValid, 'label-left': labelPosition === 'left' }"> | ||||||
|  |     <label v-if="label" :for="id" class="label"> {{ label }} </label> | ||||||
|  | 
 | ||||||
|  |     <div class="input-wrapper"> | ||||||
|  |       <slot name="prefix" /> | ||||||
|  | 
 | ||||||
|  |       <input | ||||||
|  |         :id="id" | ||||||
|  |         v-model="value" | ||||||
|  |         type="text" | ||||||
|  |         class="input" | ||||||
|  |         :placeholder="placeholder" | ||||||
|  |         :readonly="readonly" | ||||||
|  |         :disabled="disabled" | ||||||
|  |         :data-test-id="testId" | ||||||
|  |         :autocapitalize="autocapitalize ?? (rawText ? 'off' : undefined)" | ||||||
|  |         :autocomplete="autocomplete ?? (rawText ? 'off' : undefined)" | ||||||
|  |         :autocorrect="autocorrect ?? (rawText ? 'off' : undefined)" | ||||||
|  |         :spellcheck="spellcheck ?? (rawText ? false : undefined)" | ||||||
|  |       /> | ||||||
|  | 
 | ||||||
|  |       <c-button v-if="clearable && value" variant="text" circle size="small" @click="value = ''"> | ||||||
|  |         <icon-mdi-close /> | ||||||
|  |       </c-button> | ||||||
|  |       <slot name="suffix" /> | ||||||
|  |     </div> | ||||||
|  | 
 | ||||||
|  |     <span v-if="!validation.isValid" class="feedback"> {{ validation.message }} </span> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  | 
 | ||||||
|  | <script lang="ts" setup> | ||||||
|  | import { generateRandomId } from '@/utils/random'; | ||||||
|  | import { useValidation, type UseValidationRule } from '@/composable/validation'; | ||||||
|  | import { useTheme } from './c-input-text.theme'; | ||||||
|  | import { useAppTheme } from '../theme/themes'; | ||||||
|  | 
 | ||||||
|  | const props = withDefaults( | ||||||
|  |   defineProps<{ | ||||||
|  |     value?: string; | ||||||
|  |     id?: string; | ||||||
|  |     placeholder?: string; | ||||||
|  |     label?: string; | ||||||
|  |     readonly?: boolean; | ||||||
|  |     disabled?: boolean; | ||||||
|  |     validationRules?: UseValidationRule<string>[]; | ||||||
|  |     labelPosition?: 'top' | 'left'; | ||||||
|  |     labelWidth?: string; | ||||||
|  |     labelAlign?: 'left' | 'right'; | ||||||
|  |     clearable?: boolean; | ||||||
|  |     testId?: string; | ||||||
|  |     autocapitalize?: 'none' | 'sentences' | 'words' | 'characters' | 'on' | 'off' | string; | ||||||
|  |     autocomplete?: 'on' | 'off' | string; | ||||||
|  |     autocorrect?: 'on' | 'off' | string; | ||||||
|  |     spellcheck?: 'true' | 'false' | boolean; | ||||||
|  |     rawText?: boolean; | ||||||
|  |   }>(), | ||||||
|  |   { | ||||||
|  |     value: '', | ||||||
|  |     id: generateRandomId, | ||||||
|  |     placeholder: 'Input text', | ||||||
|  |     label: undefined, | ||||||
|  |     readonly: false, | ||||||
|  |     disabled: false, | ||||||
|  |     validationRules: () => [], | ||||||
|  |     labelPosition: 'top', | ||||||
|  |     labelWidth: 'auto', | ||||||
|  |     labelAlign: 'left', | ||||||
|  |     clearable: false, | ||||||
|  |     testId: undefined, | ||||||
|  |     autocapitalize: undefined, | ||||||
|  |     autocomplete: undefined, | ||||||
|  |     autocorrect: undefined, | ||||||
|  |     spellcheck: undefined, | ||||||
|  |     rawText: false, | ||||||
|  |   }, | ||||||
|  | ); | ||||||
|  | const emit = defineEmits(['update:value']); | ||||||
|  | const value = useVModel(props, 'value', emit); | ||||||
|  | 
 | ||||||
|  | const { id, placeholder, label, validationRules, labelPosition, labelWidth, labelAlign } = toRefs(props); | ||||||
|  | 
 | ||||||
|  | const validation = useValidation({ | ||||||
|  |   rules: validationRules, | ||||||
|  |   source: value, | ||||||
|  | }); | ||||||
|  | 
 | ||||||
|  | const theme = useTheme(); | ||||||
|  | const appTheme = useAppTheme(); | ||||||
|  | </script> | ||||||
|  | 
 | ||||||
|  | <style lang="less" scoped> | ||||||
|  | .c-input-text { | ||||||
|  |   display: inline-flex; | ||||||
|  |   flex-direction: column; | ||||||
|  |   width: 100%; | ||||||
|  | 
 | ||||||
|  |   &.label-left { | ||||||
|  |     flex-direction: row; | ||||||
|  |     align-items: baseline; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &.error { | ||||||
|  |     & > .input { | ||||||
|  |       border-color: v-bind('appTheme.error.color'); | ||||||
|  |       &:hover, | ||||||
|  |       &:focus { | ||||||
|  |         border-color: v-bind('appTheme.error.color'); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|  |       &:focus { | ||||||
|  |         background-color: v-bind('appTheme.error.color + 22'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     & > .feedback { | ||||||
|  |       color: v-bind('appTheme.error.color'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   & > .label { | ||||||
|  |     margin-bottom: 5px; | ||||||
|  |     flex: 0 0 v-bind('labelWidth'); | ||||||
|  |     text-align: v-bind('labelAlign'); | ||||||
|  |     padding-right: 10px; | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   .input-wrapper { | ||||||
|  |     flex: 1 1 0; | ||||||
|  |     min-width: 0; | ||||||
|  | 
 | ||||||
|  |     display: flex; | ||||||
|  |     flex-direction: row; | ||||||
|  |     align-items: center; | ||||||
|  |     background-color: v-bind('theme.backgroundColor'); | ||||||
|  |     border: 1px solid v-bind('theme.borderColor'); | ||||||
|  |     border-radius: 4px; | ||||||
|  |     padding: 0 4px 0 12px; | ||||||
|  | 
 | ||||||
|  |     & > .input { | ||||||
|  |       flex: 1 1 0; | ||||||
|  |       min-width: 0; | ||||||
|  | 
 | ||||||
|  |       padding: 8px 0; | ||||||
|  |       outline: none; | ||||||
|  |       transition: border-color 0.2s ease-in-out; | ||||||
|  |       background-color: transparent; | ||||||
|  |       background-image: none; | ||||||
|  |       -webkit-box-shadow: none; | ||||||
|  |       -moz-box-shadow: none; | ||||||
|  |       box-shadow: none; | ||||||
|  |       background-color: transparent; | ||||||
|  |       border: none; | ||||||
|  |       color: v-bind('appTheme.text.baseColor'); | ||||||
|  | 
 | ||||||
|  |       &::placeholder { | ||||||
|  |         color: v-bind('appTheme.text.mutedColor'); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:hover, | ||||||
|  |     &:focus { | ||||||
|  |       border-color: v-bind('appTheme.primary.color'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:focus { | ||||||
|  |       background-color: v-bind('theme.focus.backgroundColor'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &.error .input-wrapper { | ||||||
|  |     border-color: v-bind('appTheme.error.color'); | ||||||
|  | 
 | ||||||
|  |     &:hover, | ||||||
|  |     &:focus { | ||||||
|  |       border-color: v-bind('appTheme.error.color'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &:focus { | ||||||
|  |       background-color: v-bind('appTheme.error.color + 22'); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   &.disabled .input-wrapper { | ||||||
|  |     opacity: 0.5; | ||||||
|  | 
 | ||||||
|  |     &:hover, | ||||||
|  |     &:focus { | ||||||
|  |       border-color: v-bind('theme.borderColor'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     & > .input { | ||||||
|  |       cursor: not-allowed; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @ -18,6 +18,8 @@ | |||||||
|       </div> |       </div> | ||||||
| 
 | 
 | ||||||
|       <div flex-1 pl-4> |       <div flex-1 pl-4> | ||||||
|  |         <h1>{{ componentName }}</h1> | ||||||
|  | 
 | ||||||
|         <router-view /> |         <router-view /> | ||||||
|       </div> |       </div> | ||||||
|     </div> |     </div> | ||||||
| @ -25,9 +27,12 @@ | |||||||
| </template> | </template> | ||||||
| 
 | 
 | ||||||
| <script lang="ts" setup> | <script lang="ts" setup> | ||||||
|  | import _ from 'lodash'; | ||||||
| import { demoRoutes } from './demo.routes'; | import { demoRoutes } from './demo.routes'; | ||||||
| 
 | 
 | ||||||
| const route = useRoute(); | const route = useRoute(); | ||||||
|  | 
 | ||||||
|  | const componentName = computed(() => _.startCase(String(route.name).replace(/^c-/, ''))); | ||||||
| </script> | </script> | ||||||
| 
 | 
 | ||||||
| <style lang="less" scoped></style> | <style lang="less" scoped></style> | ||||||
|  | |||||||
| @ -6,8 +6,6 @@ export const demoRoutes = Object.keys(demoPages).map((path) => { | |||||||
|   const [, , fileName] = path.split('/'); |   const [, , fileName] = path.split('/'); | ||||||
|   const name = fileName.split('.').shift(); |   const name = fileName.split('.').shift(); | ||||||
| 
 | 
 | ||||||
|   console.log(path); |  | ||||||
| 
 |  | ||||||
|   return { |   return { | ||||||
|     path: name, |     path: name, | ||||||
|     name, |     name, | ||||||
|  | |||||||
| @ -18,4 +18,14 @@ const shuffleArray = <T>(array: T[]): T[] => shuffleArrayMutate([...array]); | |||||||
| 
 | 
 | ||||||
| const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter); | const shuffleString = (str: string, delimiter = ''): string => shuffleArrayMutate(str.split(delimiter)).join(delimiter); | ||||||
| 
 | 
 | ||||||
| export { randFromArray, randIntFromInterval, random, shuffleArray, shuffleArrayMutate, shuffleString }; | const generateRandomId = () => `id-${random().toString(36).substring(2, 12)}`; | ||||||
|  | 
 | ||||||
|  | export { | ||||||
|  |   randFromArray, | ||||||
|  |   randIntFromInterval, | ||||||
|  |   random, | ||||||
|  |   shuffleArray, | ||||||
|  |   shuffleArrayMutate, | ||||||
|  |   shuffleString, | ||||||
|  |   generateRandomId, | ||||||
|  | }; | ||||||
|  | |||||||
| @ -9,6 +9,6 @@ | |||||||
|     "paths": { |     "paths": { | ||||||
|       "@/*": ["./src/*"] |       "@/*": ["./src/*"] | ||||||
|     }, |     }, | ||||||
|     "types": ["naive-ui/volar"] |     "types": ["naive-ui/volar", "unplugin-icons/types/vue"] | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -10,6 +10,9 @@ import AutoImport from 'unplugin-auto-import/vite'; | |||||||
| import Components from 'unplugin-vue-components/vite'; | import Components from 'unplugin-vue-components/vite'; | ||||||
| import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'; | ||||||
| import Unocss from 'unocss/vite'; | import Unocss from 'unocss/vite'; | ||||||
|  | import { configDefaults } from 'vitest/config'; | ||||||
|  | import Icons from 'unplugin-icons/vite'; | ||||||
|  | import IconsResolver from 'unplugin-icons/resolver'; | ||||||
| 
 | 
 | ||||||
| // https://vitejs.dev/config/
 | // https://vitejs.dev/config/
 | ||||||
| export default defineConfig({ | export default defineConfig({ | ||||||
| @ -28,7 +31,7 @@ export default defineConfig({ | |||||||
|         enabled: true, |         enabled: true, | ||||||
|       }, |       }, | ||||||
|     }), |     }), | ||||||
| 
 |     Icons({ compiler: 'vue3' }), | ||||||
|     vue({ |     vue({ | ||||||
|       include: [/\.vue$/, /\.md$/], |       include: [/\.vue$/, /\.md$/], | ||||||
|     }), |     }), | ||||||
| @ -76,7 +79,7 @@ export default defineConfig({ | |||||||
|       dirs: ['src/'], |       dirs: ['src/'], | ||||||
|       extensions: ['vue', 'md'], |       extensions: ['vue', 'md'], | ||||||
|       include: [/\.vue$/, /\.vue\?vue/, /\.md$/], |       include: [/\.vue$/, /\.vue\?vue/, /\.md$/], | ||||||
|       resolvers: [NaiveUiResolver()], |       resolvers: [NaiveUiResolver(), IconsResolver({ prefix: 'icon' })], | ||||||
|     }), |     }), | ||||||
|     Unocss(), |     Unocss(), | ||||||
|   ], |   ], | ||||||
| @ -88,4 +91,7 @@ export default defineConfig({ | |||||||
|   define: { |   define: { | ||||||
|     'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), |     'import.meta.env.PACKAGE_VERSION': JSON.stringify(process.env.npm_package_version), | ||||||
|   }, |   }, | ||||||
|  |   test: { | ||||||
|  |     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], | ||||||
|  |   }, | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -1,13 +0,0 @@ | |||||||
| import { configDefaults, defineConfig } from 'vitest/config'; |  | ||||||
| import path from 'path'; |  | ||||||
| 
 |  | ||||||
| export default defineConfig({ |  | ||||||
|   resolve: { |  | ||||||
|     alias: { |  | ||||||
|       '@': path.resolve(__dirname, './src'), |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|   test: { |  | ||||||
|     exclude: [...configDefaults.exclude, '**/*.e2e.spec.ts'], |  | ||||||
|   }, |  | ||||||
| }); |  | ||||||
		Loading…
	
		Reference in New Issue
	
	Block a user