272 lines
6.3 KiB
Vue
272 lines
6.3 KiB
Vue
<template>
|
|
<div v-if="visible" class="tui-editor-search" @mousedown.stop>
|
|
<div class="row">
|
|
<t-button
|
|
class="toggle-btn"
|
|
shape="square"
|
|
size="small"
|
|
type="button"
|
|
variant="outline"
|
|
:title="expanded ? '收起替换' : '展开替换'"
|
|
@click="emit('update:expanded', !expanded)"
|
|
>
|
|
<t-icon :name="expanded ? 'chevron-down-s' : 'chevron-right-s'" />
|
|
</t-button>
|
|
<t-input
|
|
ref="keywordInputRef"
|
|
:value="keyword"
|
|
class="text-input"
|
|
placeholder="查找"
|
|
size="small"
|
|
@change="onKeywordChange"
|
|
@enter="onKeywordEnter"
|
|
@keydown="onKeywordKeydown"
|
|
>
|
|
<template #suffix-icon>
|
|
<t-button
|
|
shape="square"
|
|
size="small"
|
|
type="button"
|
|
variant="text"
|
|
:theme="caseSensitive ? 'primary' : 'default'"
|
|
title="大小写匹配"
|
|
@click="emit('update:case-sensitive', !caseSensitive)"
|
|
>
|
|
Aa
|
|
</t-button>
|
|
<t-button
|
|
shape="square"
|
|
size="small"
|
|
type="button"
|
|
variant="text"
|
|
:theme="useRegex ? 'primary' : 'default'"
|
|
title="正则搜索"
|
|
@click="emit('update:use-regex', !useRegex)"
|
|
>
|
|
.*
|
|
</t-button>
|
|
</template>
|
|
</t-input>
|
|
<span class="status" v-text="resolvedStatusText" />
|
|
<t-button shape="square" size="small" type="button" variant="text" title="上一个" @click="emit('previous')">
|
|
<t-icon name="arrow-up" />
|
|
</t-button>
|
|
<t-button shape="square" size="small" type="button" variant="text" title="下一个" @click="emit('next', false)">
|
|
<t-icon name="arrow-down" />
|
|
</t-button>
|
|
<t-button shape="square" size="small" type="button" variant="text" title="关闭" @click="emit('close')">
|
|
<t-icon name="close" />
|
|
</t-button>
|
|
</div>
|
|
<div v-if="expanded" class="row replace-row">
|
|
<div class="toggle-holder" />
|
|
<t-input
|
|
ref="replaceInputRef"
|
|
:value="replaceValue"
|
|
class="text-input"
|
|
placeholder="替换"
|
|
size="small"
|
|
@change="onReplaceChange"
|
|
@enter="emit('replace')"
|
|
@keydown="onReplaceKeydown"
|
|
/>
|
|
<div class="replace-actions">
|
|
<t-button size="small" type="button" theme="primary" @click="emit('replace')">替换</t-button>
|
|
<t-button size="small" type="button" variant="outline" @click="emit('replace-all')">全部替换</t-button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script lang="ts" setup>
|
|
import type { ComponentPublicInstance } from "vue";
|
|
|
|
defineOptions({
|
|
name: "EditorSearchBar"
|
|
});
|
|
|
|
const props = withDefaults(defineProps<{
|
|
visible?: boolean;
|
|
expanded?: boolean;
|
|
keyword?: string;
|
|
replaceValue?: string;
|
|
statusText?: string;
|
|
statusError?: string;
|
|
matchIndex?: number;
|
|
matchCount?: number;
|
|
caseSensitive?: boolean;
|
|
useRegex?: boolean;
|
|
}>(), {
|
|
visible: false,
|
|
expanded: false,
|
|
keyword: "",
|
|
replaceValue: "",
|
|
statusText: undefined,
|
|
statusError: "",
|
|
matchIndex: -1,
|
|
matchCount: 0,
|
|
caseSensitive: false,
|
|
useRegex: false
|
|
});
|
|
|
|
const emit = defineEmits<{
|
|
(event: "update:expanded", value: boolean): void;
|
|
(event: "update:keyword", value: string): void;
|
|
(event: "update:replace-value", value: string): void;
|
|
(event: "update:case-sensitive", value: boolean): void;
|
|
(event: "update:use-regex", value: boolean): void;
|
|
(event: "previous"): void;
|
|
(event: "next", shiftKey: boolean): void;
|
|
(event: "replace"): void;
|
|
(event: "replace-all"): void;
|
|
(event: "close"): void;
|
|
}>();
|
|
|
|
type InputInstance = ComponentPublicInstance & {
|
|
$el: HTMLElement;
|
|
};
|
|
|
|
type InputKeyboardContext = {
|
|
e: KeyboardEvent;
|
|
};
|
|
|
|
const keywordInputRef = ref<InputInstance>();
|
|
const replaceInputRef = ref<InputInstance>();
|
|
|
|
const resolvedStatusText = computed(() => {
|
|
if (undefined !== props.statusText) {
|
|
return props.statusText;
|
|
}
|
|
if (props.statusError) {
|
|
return props.statusError;
|
|
}
|
|
if (!props.keyword) {
|
|
return "无结果";
|
|
}
|
|
if (1 > props.matchCount) {
|
|
return "未找到";
|
|
}
|
|
return `${props.matchIndex + 1} / ${props.matchCount}`;
|
|
});
|
|
|
|
const onKeywordChange = (value: string | number) => {
|
|
emit("update:keyword", `${value}`);
|
|
};
|
|
|
|
const onReplaceChange = (value: string | number) => {
|
|
emit("update:replace-value", `${value}`);
|
|
};
|
|
|
|
const onKeywordEnter = (_value: string | number, context: InputKeyboardContext) => {
|
|
emit("next", context.e.shiftKey);
|
|
};
|
|
|
|
const onKeywordKeydown = (_value: string | number, context: InputKeyboardContext) => {
|
|
if ("Escape" !== context.e.key) {
|
|
return;
|
|
}
|
|
context.e.preventDefault();
|
|
emit("close");
|
|
};
|
|
|
|
const onReplaceKeydown = (_value: string | number, context: InputKeyboardContext) => {
|
|
if ("Escape" !== context.e.key) {
|
|
return;
|
|
}
|
|
context.e.preventDefault();
|
|
emit("close");
|
|
};
|
|
|
|
const getNativeInput = (instance?: InputInstance) => instance?.$el.querySelector("input") || undefined;
|
|
|
|
const focusKeyword = (selectAll = false) => {
|
|
const input = getNativeInput(keywordInputRef.value);
|
|
input?.focus();
|
|
if (selectAll && input) {
|
|
input.selectionStart = 0;
|
|
input.selectionEnd = input.value.length;
|
|
}
|
|
};
|
|
|
|
const focusReplace = (selectAll = false) => {
|
|
const input = getNativeInput(replaceInputRef.value);
|
|
input?.focus();
|
|
if (selectAll && input) {
|
|
input.selectionStart = 0;
|
|
input.selectionEnd = input.value.length;
|
|
}
|
|
};
|
|
|
|
defineExpose({
|
|
focusKeyword,
|
|
focusReplace,
|
|
keywordInputRef,
|
|
replaceInputRef
|
|
});
|
|
</script>
|
|
|
|
<style lang="less" scoped>
|
|
.tui-editor-search {
|
|
top: .5rem;
|
|
right: .5rem;
|
|
gap: .375rem;
|
|
z-index: 3;
|
|
display: flex;
|
|
padding: .5rem;
|
|
position: absolute;
|
|
min-width: 20rem;
|
|
max-width: calc(100% - 1rem);
|
|
box-sizing: border-box;
|
|
border-radius: var(--td-radius-default, .375rem);
|
|
background: var(--td-bg-color-container, #fff);
|
|
border: 1px solid var(--td-component-stroke, #B8BBC9);
|
|
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .12);
|
|
flex-direction: column;
|
|
|
|
.row {
|
|
gap: .375rem;
|
|
display: flex;
|
|
min-width: 0;
|
|
align-items: center;
|
|
|
|
.toggle-holder {
|
|
flex: none;
|
|
width: 24px;
|
|
}
|
|
|
|
.toggle-btn {
|
|
flex: none;
|
|
}
|
|
|
|
.text-input {
|
|
width: 12rem;
|
|
|
|
:deep(.t-input) {
|
|
padding-right: 0;
|
|
}
|
|
}
|
|
|
|
.status {
|
|
width: 3rem;
|
|
color: var(--td-text-color-secondary, #666);
|
|
font-size: .875rem;
|
|
white-space: nowrap;
|
|
}
|
|
}
|
|
|
|
.replace-actions {
|
|
gap: .375rem;
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
}
|
|
|
|
@media (prefers-color-scheme: dark) {
|
|
.tui-editor-search {
|
|
background: var(--td-bg-color-container, #1f1f1f);
|
|
border-color: var(--td-component-stroke, #434343);
|
|
box-shadow: 0 .25rem .75rem rgba(0, 0, 0, .28);
|
|
}
|
|
}
|
|
</style>
|