Merge 839bafe035 into 76a19d218d
				
					
				
			This commit is contained in:
		
						commit
						b9535d42db
					
				
							
								
								
									
										12
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								components.d.ts
									
									
									
									
										vendored
									
									
								
							| @ -89,7 +89,9 @@ declare module '@vue/runtime-core' { | ||||
|     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:contentCopy': typeof import('~icons/mdi/content-copy')['default'] | ||||
|     'IconMdi:kettleSteamOutline': typeof import('~icons/mdi/kettle-steam-outline')['default'] | ||||
|     IconMdiArrowDown: typeof import('~icons/mdi/arrow-down')['default'] | ||||
|     IconMdiChevronDown: typeof import('~icons/mdi/chevron-down')['default'] | ||||
|     IconMdiChevronRight: typeof import('~icons/mdi/chevron-right')['default'] | ||||
|     IconMdiClose: typeof import('~icons/mdi/close')['default'] | ||||
| @ -126,12 +128,16 @@ declare module '@vue/runtime-core' { | ||||
|     MenuLayout: typeof import('./src/components/MenuLayout.vue')['default'] | ||||
|     MetaTagGenerator: typeof import('./src/tools/meta-tag-generator/meta-tag-generator.vue')['default'] | ||||
|     MimeTypes: typeof import('./src/tools/mime-types/mime-types.vue')['default'] | ||||
|     NAlert: typeof import('naive-ui')['NAlert'] | ||||
|     NavbarButtons: typeof import('./src/components/NavbarButtons.vue')['default'] | ||||
|     NCheckbox: typeof import('naive-ui')['NCheckbox'] | ||||
|     NCode: typeof import('naive-ui')['NCode'] | ||||
|     NCollapseTransition: typeof import('naive-ui')['NCollapseTransition'] | ||||
|     NConfigProvider: typeof import('naive-ui')['NConfigProvider'] | ||||
|     NDatePicker: typeof import('naive-ui')['NDatePicker'] | ||||
|     NDivider: typeof import('naive-ui')['NDivider'] | ||||
|     NEllipsis: typeof import('naive-ui')['NEllipsis'] | ||||
|     NForm: typeof import('naive-ui')['NForm'] | ||||
|     NFormItem: typeof import('naive-ui')['NFormItem'] | ||||
|     NGi: typeof import('naive-ui')['NGi'] | ||||
|     NGrid: typeof import('naive-ui')['NGrid'] | ||||
| @ -139,12 +145,17 @@ declare module '@vue/runtime-core' { | ||||
|     NH3: typeof import('naive-ui')['NH3'] | ||||
|     NIcon: typeof import('naive-ui')['NIcon'] | ||||
|     NInputNumber: typeof import('naive-ui')['NInputNumber'] | ||||
|     NInputText: typeof import('naive-ui')['NInputText'] | ||||
|     NLabel: typeof import('naive-ui')['NLabel'] | ||||
|     NLayout: typeof import('naive-ui')['NLayout'] | ||||
|     NLayoutSider: typeof import('naive-ui')['NLayoutSider'] | ||||
|     NMenu: typeof import('naive-ui')['NMenu'] | ||||
|     NScrollbar: typeof import('naive-ui')['NScrollbar'] | ||||
|     NSlider: typeof import('naive-ui')['NSlider'] | ||||
|     NSpin: typeof import('naive-ui')['NSpin'] | ||||
|     NStatistic: typeof import('naive-ui')['NStatistic'] | ||||
|     NSwitch: typeof import('naive-ui')['NSwitch'] | ||||
|     NTable: typeof import('naive-ui')['NTable'] | ||||
|     NumeronymGenerator: typeof import('./src/tools/numeronym-generator/numeronym-generator.vue')['default'] | ||||
|     OtpCodeGeneratorAndValidator: typeof import('./src/tools/otp-code-generator-and-validator/otp-code-generator-and-validator.vue')['default'] | ||||
|     PasswordStrengthAnalyser: typeof import('./src/tools/password-strength-analyser/password-strength-analyser.vue')['default'] | ||||
| @ -161,6 +172,7 @@ declare module '@vue/runtime-core' { | ||||
|     RsaKeyPairGenerator: typeof import('./src/tools/rsa-key-pair-generator/rsa-key-pair-generator.vue')['default'] | ||||
|     SafelinkDecoder: typeof import('./src/tools/safelink-decoder/safelink-decoder.vue')['default'] | ||||
|     SlugifyString: typeof import('./src/tools/slugify-string/slugify-string.vue')['default'] | ||||
|     SmartTextReplacer: typeof import('./src/tools/smart-text-replacer/smart-text-replacer.vue')['default'] | ||||
|     SpanCopyable: typeof import('./src/components/SpanCopyable.vue')['default'] | ||||
|     SqlPrettify: typeof import('./src/tools/sql-prettify/sql-prettify.vue')['default'] | ||||
|     StringObfuscator: typeof import('./src/tools/string-obfuscator/string-obfuscator.vue')['default'] | ||||
|  | ||||
| @ -388,6 +388,10 @@ tools: | ||||
|     title: Encode/decode URL-formatted strings | ||||
|     description: Encode text to URL-encoded format (also known as "percent-encoded"), or decode from it. | ||||
| 
 | ||||
|   smart-text-replacer: | ||||
|     title: Smart text replacer | ||||
|     description: Search and replace a word on single or multiple occurrences just like windows notepad search and replace. | ||||
| 
 | ||||
|   text-to-binary: | ||||
|     title: Text to ASCII binary | ||||
|     description: Convert text to its ASCII binary representation and vice-versa. | ||||
|  | ||||
| @ -387,3 +387,7 @@ tools: | ||||
|   text-to-binary: | ||||
|     title: 文本到 ASCII 二进制 | ||||
|     description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。 | ||||
| 
 | ||||
|   smart-text-replacer: | ||||
|     title: 智能文本替换器 | ||||
|     description: 像windows记事本一样搜索和替换单个或多个出现的单词。 | ||||
|  | ||||
| @ -72,6 +72,7 @@ import { tool as romanNumeralConverter } from './roman-numeral-converter'; | ||||
| import { tool as sqlPrettify } from './sql-prettify'; | ||||
| import { tool as svgPlaceholderGenerator } from './svg-placeholder-generator'; | ||||
| import { tool as temperatureConverter } from './temperature-converter'; | ||||
| import { tool as smartTextReplacer } from './smart-text-replacer'; | ||||
| import { tool as textStatistics } from './text-statistics'; | ||||
| import { tool as tokenGenerator } from './token-generator'; | ||||
| import type { ToolCategory } from './tools.types'; | ||||
| @ -170,6 +171,7 @@ export const toolsByCategory: ToolCategory[] = [ | ||||
|       emojiPicker, | ||||
|       stringObfuscator, | ||||
|       textDiff, | ||||
|       smartTextReplacer, | ||||
|       numeronymGenerator, | ||||
|       asciiTextDrawer, | ||||
|     ], | ||||
|  | ||||
							
								
								
									
										13
									
								
								src/tools/smart-text-replacer/index.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/tools/smart-text-replacer/index.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,13 @@ | ||||
| import { Search } from '@vicons/tabler'; | ||||
| import { defineTool } from '../tool'; | ||||
| import { translate } from '@/plugins/i18n.plugin'; | ||||
| 
 | ||||
| export const tool = defineTool({ | ||||
|   name: translate('tools.smart-text-replacer.title'), | ||||
|   path: '/smart-text-replacer', | ||||
|   description: translate('tools.smart-text-replacer.description'), | ||||
|   keywords: ['smart', 'text-replacer', 'search', 'replace'], | ||||
|   component: () => import('./smart-text-replacer.vue'), | ||||
|   icon: Search, | ||||
|   createdAt: new Date('2024-04-03'), | ||||
| }); | ||||
							
								
								
									
										155
									
								
								src/tools/smart-text-replacer/smart-text-replacer.vue
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/tools/smart-text-replacer/smart-text-replacer.vue
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,155 @@ | ||||
| <script setup lang="ts"> | ||||
| import { useCopy } from '@/composable/copy'; | ||||
| 
 | ||||
| const str = ref('Lorem ipsum dolor sit amet DOLOR Lorem ipsum dolor sit amet DOLOR'); | ||||
| const findWhat = ref(''); | ||||
| const replaceWith = ref(''); | ||||
| const matchCase = ref(false); | ||||
| 
 | ||||
| // Tracks the index of the currently active highlight. | ||||
| const currentActiveIndex = ref(0); | ||||
| // Tracks the total number of matches found to cycle through them. | ||||
| const totalMatches = ref(0); | ||||
| 
 | ||||
| const highlightedText = computed(() => { | ||||
|   const findWhatValue = findWhat.value.trim(); | ||||
|   const strValue = str.value; | ||||
| 
 | ||||
|   if (!findWhatValue || !strValue) { | ||||
|     return strValue; | ||||
|   } | ||||
| 
 | ||||
|   const regex = new RegExp(findWhatValue, matchCase.value ? 'g' : 'gi'); | ||||
|   let index = 0; | ||||
|   const newStr = strValue.replace(regex, (match) => { | ||||
|     index++; | ||||
|     return `<span class="${match === findWhatValue ? 'highlight' : 'outline'}">${match}</span>`; | ||||
|   }); | ||||
| 
 | ||||
|   totalMatches.value = index; | ||||
|   // Reset to -1 to ensure the first match is highlighted upon next search | ||||
|   currentActiveIndex.value = -1; | ||||
|   return newStr; | ||||
| }); | ||||
| 
 | ||||
| // Automatically highlight the first occurrence after any change | ||||
| watchEffect(async () => { | ||||
|   if (highlightedText.value) { | ||||
|     await nextTick(); | ||||
|     updateHighlighting(); | ||||
|   } | ||||
| }); | ||||
| 
 | ||||
| watch(matchCase, () => { | ||||
|   // Use nextTick to wait for the DOM to update after highlightedText re-reaction | ||||
|   nextTick().then(() => { | ||||
|     const matches = document.querySelectorAll('.outline, .highlight'); | ||||
|     if (matches.length === 0) { | ||||
|       // No matches after change, reset | ||||
|       currentActiveIndex.value = -1; | ||||
|       totalMatches.value = 0; | ||||
|     } | ||||
|     else if (matches.length <= currentActiveIndex.value || currentActiveIndex.value === -1) { | ||||
|       // Current selection is out of range or reset, select the first match | ||||
|       currentActiveIndex.value = 0; | ||||
|       updateHighlighting(); // Ensure correct highlighting | ||||
|     } | ||||
|     else { | ||||
|       // The current selection is still valid, ensure it's highlighted correctly | ||||
|       updateHighlighting(); // This might need adjustment to not advance the index | ||||
|     } | ||||
|   }); | ||||
| }); | ||||
| 
 | ||||
| // Function to add active highlighting | ||||
| function updateHighlighting() { | ||||
|   currentActiveIndex.value = (currentActiveIndex.value + 1) % totalMatches.value; | ||||
|   const matches = document.querySelectorAll('.outline, .highlight'); | ||||
|   matches.forEach((match, index) => { | ||||
|     match.className = index === currentActiveIndex.value ? 'highlight' : 'outline'; | ||||
|   }); | ||||
| } | ||||
| 
 | ||||
| function replaceSelected() { | ||||
|   const matches = document.querySelectorAll('.outline, .highlight'); | ||||
|   if (matches.length > currentActiveIndex.value) { | ||||
|     const selectedMatch = matches[currentActiveIndex.value]; | ||||
|     if (selectedMatch) { | ||||
|       const newText = replaceWith.value; | ||||
|       selectedMatch.textContent = newText; | ||||
|       selectedMatch.classList.remove('highlight'); | ||||
|       currentActiveIndex.value--; | ||||
|       totalMatches.value--; | ||||
|     } | ||||
|   } | ||||
|   updateHighlighting(); | ||||
| } | ||||
| 
 | ||||
| function replaceAll() { | ||||
|   const matches = document.querySelectorAll('.outline, .highlight'); | ||||
|   matches.forEach((match) => { | ||||
|     match.textContent = replaceWith.value; | ||||
|     match.classList.remove('highlight'); | ||||
|     match.classList.remove('outline'); | ||||
|   }); | ||||
|   currentActiveIndex.value = -1; | ||||
|   totalMatches.value = matches.length; | ||||
| } | ||||
| 
 | ||||
| function findNext() { | ||||
|   updateHighlighting(); | ||||
| } | ||||
| 
 | ||||
| const { copy } = useCopy({ source: highlightedText }); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|   <div> | ||||
|     <c-input-text v-model:value="str" raw-text placeholder="Enter text here..." label="Text to search and replace:" clearable multiline rows="10" /> | ||||
| 
 | ||||
|     <div mt-4 w-full flex gap-10px> | ||||
|       <div flex-1> | ||||
|         <div>Find what:</div> | ||||
|         <c-input-text v-model:value="findWhat" @keyup.enter="findNext()" /> | ||||
|       </div> | ||||
|       <div flex-1> | ||||
|         <div>Replace with:</div> | ||||
|         <c-input-text v-model:value="replaceWith" @keyup.enter="replaceSelected()" /> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <div mt-4 w-full flex gap-10px> | ||||
|       <div flex flex-1 gap-10px> | ||||
|         <c-button @click="findNext()"> | ||||
|           <label>Find Next</label> | ||||
|         </c-button> | ||||
|         <n-checkbox v-model:checked="matchCase"> | ||||
|           <label>Match case</label> | ||||
|         </n-checkbox> | ||||
|       </div> | ||||
|       <div flex flex-1 justify-end gap-10px> | ||||
|         <c-button @click="replaceSelected()"> | ||||
|           <label>Replace</label> | ||||
|         </c-button> | ||||
|         <c-button @click="replaceAll()"> | ||||
|           <label>Replace All</label> | ||||
|         </c-button> | ||||
|       </div> | ||||
|     </div> | ||||
| 
 | ||||
|     <c-card v-if="highlightedText" mt-60px max-w-600px flex items-center gap-5px font-mono> | ||||
|       <div flex-1 break-anywhere text-wrap v-html="highlightedText" /> | ||||
| 
 | ||||
|       <c-button @click="copy()"> | ||||
|         <icon-mdi:content-copy /> | ||||
|       </c-button> | ||||
|     </c-card> | ||||
|   </div> | ||||
| </template> | ||||
| 
 | ||||
| <style lang="less"> | ||||
| .highlight { | ||||
|   background-color: #ff0; | ||||
|   color: black; | ||||
| } | ||||
| </style> | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user