add tiptap
This commit is contained in:
@@ -0,0 +1,271 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user