Skip to content

Commit 5b5c8c0

Browse files
authored
feat: properly impl find for files (#5741)
* Initial file search impl * Add replace functionality * Rename to find, remove extra icon * Put into seperate component * Fix lint * Change remaining search stuff to find * Use ButtonStyled for buttons, use types from ace editor * Make results label oriented left, add clear button to replace input * Run fix --------- Signed-off-by: Arthur <creeperkatze.dev@gmail.com> Co-authored-by: Creeperkatze <178587183+Creeperkatze@users.noreply.github.com>
1 parent 4d68f3c commit 5b5c8c0

5 files changed

Lines changed: 441 additions & 21 deletions

File tree

packages/ui/src/layouts/shared/files-tab/components/FileNavbar.vue

Lines changed: 29 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -179,16 +179,30 @@
179179
</ButtonStyled>
180180
</div>
181181

182-
<div v-else-if="!isEditingImage && isLogFile" class="flex gap-2">
183-
<Button
184-
v-tooltip="formatMessage(messages.shareToMclogs)"
185-
icon-only
186-
transparent
187-
:aria-label="formatMessage(messages.shareToMclogs)"
188-
@click="$emit('share')"
182+
<div v-else-if="!isEditingImage" class="flex gap-2">
183+
<ButtonStyled v-if="isLogFile" type="transparent" circular>
184+
<button
185+
v-tooltip="formatMessage(messages.shareToMclogs)"
186+
:aria-label="formatMessage(messages.shareToMclogs)"
187+
@click="$emit('share')"
188+
>
189+
<ShareIcon />
190+
</button>
191+
</ButtonStyled>
192+
<ButtonStyled
193+
circular
194+
:type="isEditorFindOpen ? 'standard' : 'transparent'"
195+
:color="isEditorFindOpen ? 'brand' : 'standard'"
189196
>
190-
<ShareIcon />
191-
</Button>
197+
<button
198+
v-tooltip="formatMessage(messages.findInFile)"
199+
:aria-label="formatMessage(messages.findInFile)"
200+
:aria-pressed="isEditorFindOpen"
201+
@click="$emit('find')"
202+
>
203+
<SearchIcon />
204+
</button>
205+
</ButtonStyled>
192206
</div>
193207
</div>
194208
</header>
@@ -212,7 +226,6 @@ import {
212226
} from '@modrinth/assets'
213227
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
214228
215-
import Button from '#ui/components/base/Button.vue'
216229
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
217230
import OverflowMenu from '#ui/components/base/OverflowMenu.vue'
218231
import StyledInput from '#ui/components/base/StyledInput.vue'
@@ -274,6 +287,10 @@ const messages = defineMessages({
274287
id: 'files.navbar.share-to-mclogs',
275288
defaultMessage: 'Share to mclo.gs',
276289
},
290+
findInFile: {
291+
id: 'files.navbar.find-in-file',
292+
defaultMessage: 'Find in file',
293+
},
277294
})
278295
279296
const props = defineProps<{
@@ -282,6 +299,7 @@ const props = defineProps<{
282299
editingFileName?: string
283300
editingFilePath?: string
284301
isEditingImage?: boolean
302+
isEditorFindOpen?: boolean
285303
searchQuery: string
286304
showRefreshButton?: boolean
287305
showInstallFromUrl?: boolean
@@ -301,6 +319,7 @@ const emit = defineEmits<{
301319
unzipFromUrl: [cf: boolean]
302320
refresh: []
303321
share: []
322+
find: []
304323
}>()
305324
306325
const refreshing = ref(false)
Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
<template>
2+
<Transition name="find">
3+
<div
4+
v-if="isFindOpen && !isEditingImage"
5+
class="absolute right-3 top-3 z-10 flex flex-col gap-1 rounded-2xl border border-solid border-surface-5 bg-surface-3 p-1.5 shadow-lg"
6+
@keydown.escape.stop="close"
7+
>
8+
<!-- Find row -->
9+
<div class="flex items-center gap-1">
10+
<ButtonStyled type="transparent" circular>
11+
<button
12+
v-tooltip="formatMessage(messages.toggleReplace)"
13+
:aria-label="formatMessage(messages.toggleReplace)"
14+
@click="toggleReplace"
15+
>
16+
<ChevronRightIcon
17+
class="transition-transform duration-150"
18+
:class="{ 'rotate-90': isReplaceOpen }"
19+
/>
20+
</button>
21+
</ButtonStyled>
22+
<div
23+
@keydown.enter.exact.prevent.stop="emit('findNext')"
24+
@keydown.shift.enter.prevent.stop="emit('findPrevious')"
25+
>
26+
<StyledInput
27+
ref="findInputRef"
28+
:model-value="findQuery"
29+
type="search"
30+
size="small"
31+
autocomplete="off"
32+
:placeholder="formatMessage(messages.findInFile)"
33+
wrapper-class="w-44"
34+
@update:model-value="emit('update:findQuery', $event as string)"
35+
/>
36+
</div>
37+
<span class="min-w-[6rem] px-1 text-sm text-secondary tabular-nums">
38+
{{
39+
findMatchCount > 0
40+
? formatMessage(messages.matchCount, {
41+
current: currentFindMatch,
42+
total: findMatchCount,
43+
})
44+
: findQuery
45+
? formatMessage(messages.noResults)
46+
: ''
47+
}}
48+
</span>
49+
<ButtonStyled type="transparent" circular>
50+
<button
51+
v-tooltip="formatMessage(messages.previousMatch)"
52+
:disabled="findMatchCount === 0"
53+
:aria-label="formatMessage(messages.previousMatch)"
54+
@click="emit('findPrevious')"
55+
>
56+
<ChevronUpIcon />
57+
</button>
58+
</ButtonStyled>
59+
<ButtonStyled type="transparent" circular>
60+
<button
61+
v-tooltip="formatMessage(messages.nextMatch)"
62+
:disabled="findMatchCount === 0"
63+
:aria-label="formatMessage(messages.nextMatch)"
64+
@click="emit('findNext')"
65+
>
66+
<ChevronDownIcon />
67+
</button>
68+
</ButtonStyled>
69+
<div class="mx-0.5 h-4 w-px bg-surface-5" />
70+
<ButtonStyled type="transparent" circular>
71+
<button
72+
v-tooltip="formatMessage(messages.closeFind)"
73+
:aria-label="formatMessage(messages.closeFind)"
74+
@click="close"
75+
>
76+
<XIcon />
77+
</button>
78+
</ButtonStyled>
79+
</div>
80+
81+
<!-- Replace row -->
82+
<div v-if="isReplaceOpen" class="flex items-center gap-1">
83+
<div class="w-9 flex-shrink-0" />
84+
<div @keydown.enter.prevent.stop="emit('replace', replaceQuery)">
85+
<StyledInput
86+
ref="replaceInputRef"
87+
v-model="replaceQuery"
88+
type="search"
89+
size="small"
90+
autocomplete="off"
91+
:placeholder="formatMessage(messages.replaceInFile)"
92+
wrapper-class="w-44"
93+
/>
94+
</div>
95+
<ButtonStyled type="outlined">
96+
<button
97+
class="!h-8 whitespace-nowrap !border !border-surface-5 px-2 text-sm disabled:opacity-50"
98+
:disabled="findMatchCount === 0"
99+
@click="emit('replace', replaceQuery)"
100+
>
101+
{{ formatMessage(messages.replace) }}
102+
</button>
103+
</ButtonStyled>
104+
<ButtonStyled type="outlined">
105+
<button
106+
class="!h-8 whitespace-nowrap !border !border-surface-5 px-2 text-sm disabled:opacity-50"
107+
:disabled="findMatchCount === 0"
108+
@click="emit('replaceAll', replaceQuery)"
109+
>
110+
{{ formatMessage(messages.replaceAll) }}
111+
</button>
112+
</ButtonStyled>
113+
</div>
114+
</div>
115+
</Transition>
116+
</template>
117+
118+
<script setup lang="ts">
119+
import { ChevronDownIcon, ChevronRightIcon, ChevronUpIcon, XIcon } from '@modrinth/assets'
120+
import { nextTick, ref, watch } from 'vue'
121+
122+
import ButtonStyled from '#ui/components/base/ButtonStyled.vue'
123+
import StyledInput from '#ui/components/base/StyledInput.vue'
124+
import { defineMessages, useVIntl } from '#ui/composables/i18n'
125+
126+
const props = defineProps<{
127+
isFindOpen: boolean
128+
findQuery: string
129+
findMatchCount: number
130+
currentFindMatch: number
131+
isEditingImage: boolean
132+
}>()
133+
134+
const emit = defineEmits<{
135+
'update:isFindOpen': [value: boolean]
136+
'update:findQuery': [value: string]
137+
close: []
138+
findNext: []
139+
findPrevious: []
140+
replace: [query: string]
141+
replaceAll: [query: string]
142+
}>()
143+
144+
const { formatMessage } = useVIntl()
145+
146+
const messages = defineMessages({
147+
findInFile: {
148+
id: 'files.editor.find-in-file',
149+
defaultMessage: 'Find',
150+
},
151+
matchCount: {
152+
id: 'files.editor.find-match-count',
153+
defaultMessage: '{current} of {total}',
154+
},
155+
noResults: {
156+
id: 'files.editor.find-no-results',
157+
defaultMessage: 'No results',
158+
},
159+
previousMatch: {
160+
id: 'files.editor.find-previous-match',
161+
defaultMessage: 'Previous match',
162+
},
163+
nextMatch: {
164+
id: 'files.editor.find-next-match',
165+
defaultMessage: 'Next match',
166+
},
167+
closeFind: {
168+
id: 'files.editor.find-close',
169+
defaultMessage: 'Close',
170+
},
171+
toggleReplace: {
172+
id: 'files.editor.find-toggle-replace',
173+
defaultMessage: 'Toggle replace',
174+
},
175+
replaceInFile: {
176+
id: 'files.editor.replace-in-file',
177+
defaultMessage: 'Replace',
178+
},
179+
replace: {
180+
id: 'files.editor.replace',
181+
defaultMessage: 'Replace',
182+
},
183+
replaceAll: {
184+
id: 'files.editor.replace-all',
185+
defaultMessage: 'Replace All',
186+
},
187+
})
188+
189+
const isReplaceOpen = ref(false)
190+
const replaceQuery = ref('')
191+
192+
const findInputRef = ref<{ focus: () => void } | null>(null)
193+
const replaceInputRef = ref<{ focus: () => void } | null>(null)
194+
195+
function toggleReplace() {
196+
isReplaceOpen.value = !isReplaceOpen.value
197+
if (isReplaceOpen.value) {
198+
nextTick(() => replaceInputRef.value?.focus())
199+
}
200+
}
201+
202+
function focusFindInput() {
203+
nextTick(() => findInputRef.value?.focus())
204+
}
205+
206+
function openReplace() {
207+
isReplaceOpen.value = true
208+
nextTick(() => replaceInputRef.value?.focus())
209+
}
210+
211+
function close() {
212+
isReplaceOpen.value = false
213+
replaceQuery.value = ''
214+
emit('close')
215+
}
216+
217+
watch(
218+
() => props.isFindOpen,
219+
(isOpen) => {
220+
if (!isOpen) {
221+
isReplaceOpen.value = false
222+
replaceQuery.value = ''
223+
}
224+
},
225+
)
226+
227+
defineExpose({
228+
focusFindInput,
229+
openReplace,
230+
})
231+
</script>
232+
233+
<style scoped>
234+
.find-enter-active,
235+
.find-leave-active {
236+
transition:
237+
opacity 0.15s ease,
238+
transform 0.15s ease;
239+
}
240+
241+
.find-enter-from,
242+
.find-leave-to {
243+
opacity: 0;
244+
transform: translateY(-4px) scale(0.97);
245+
}
246+
</style>

0 commit comments

Comments
 (0)