This commit is contained in:
utf26 2024-08-06 16:31:00 +07:00 committed by GitHub
commit b9535d42db
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 190 additions and 0 deletions

12
components.d.ts vendored
View file

@ -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']

View file

@ -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.

View file

@ -387,3 +387,7 @@ tools:
text-to-binary:
title: 文本到 ASCII 二进制
description: 将文本转换为其 ASCII 二进制表示形式,反之亦然。
smart-text-replacer:
title: 智能文本替换器
description: 像windows记事本一样搜索和替换单个或多个出现的单词。

View file

@ -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,
],

View 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'),
});

View 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>