Browse Source

feat: tiptap preview

pull/7683/head
xingyu4j 1 week ago
parent
commit
e6cde05b59
  1. 4
      packages/effects/plugins/src/tiptap/index.ts
  2. 34
      packages/effects/plugins/src/tiptap/preview.vue
  3. 56
      packages/effects/plugins/src/tiptap/style.css
  4. 97
      packages/effects/plugins/src/tiptap/tiptap.vue
  5. 7
      packages/effects/plugins/src/tiptap/types.ts
  6. 1
      packages/locales/src/langs/en-US/ui.json
  7. 1
      packages/locales/src/langs/zh-CN/ui.json
  8. 8
      playground/src/views/examples/tiptap/index.vue

4
packages/effects/plugins/src/tiptap/index.ts

@ -1,7 +1,11 @@
import './style.css';
export { createDefaultTiptapExtensions } from './extensions';
export { default as VbenTiptapPreview } from './preview.vue';
export { default as VbenTiptap } from './tiptap.vue';
export type {
TipTapPreviewProps,
VbenTiptapChangeEvent,
VbenTiptapExtensionOptions,
} from './types';

34
packages/effects/plugins/src/tiptap/preview.vue

@ -0,0 +1,34 @@
<script setup lang="ts">
import type { TipTapPreviewProps } from './types';
import { computed } from 'vue';
import { cn } from '@vben-core/shared/utils';
const props = withDefaults(defineProps<TipTapPreviewProps>(), {
content: '',
minHeight: 160,
});
const contentMinHeight = computed(() =>
typeof props.minHeight === 'number'
? `${props.minHeight}px`
: props.minHeight,
);
const previewClass = computed(() =>
cn(
'vben-tiptap-content bg-transparent p-0 leading-7 text-foreground',
props.class,
),
);
</script>
<template>
<!-- eslint-disable vue/no-v-html -->
<div
:class="previewClass"
:style="{ minHeight: contentMinHeight }"
v-html="content"
></div>
</template>

56
packages/effects/plugins/src/tiptap/style.css

@ -0,0 +1,56 @@
@reference "@vben-core/design/theme";
.vben-tiptap-content > * + * {
@apply mt-3;
}
.vben-tiptap-content h1 {
@apply text-2xl font-bold leading-[1.4];
}
.vben-tiptap-content h2 {
@apply text-xl font-bold leading-[1.45];
}
.vben-tiptap-content h3 {
@apply text-lg font-semibold leading-[1.5];
}
.vben-tiptap-content h4 {
@apply text-base font-semibold leading-[1.55];
}
.vben-tiptap-content ul {
@apply list-disc pl-6;
}
.vben-tiptap-content ol {
@apply list-decimal pl-6;
}
.vben-tiptap-content blockquote {
@apply border-l-4 border-primary pl-4 text-muted-foreground;
}
.vben-tiptap-content a {
@apply text-primary underline decoration-1 underline-offset-[3px];
}
.vben-tiptap-content code {
@apply rounded-[0.45rem] border border-border bg-secondary px-[0.35rem] py-[0.15rem] text-[0.9em] text-primary;
}
.vben-tiptap-content pre {
@apply overflow-x-auto rounded-[0.9rem] border border-border bg-popover p-4 text-popover-foreground;
}
.vben-tiptap-content pre code {
@apply border-none bg-transparent p-0 text-inherit;
}
.vben-tiptap-content img,
.vben-tiptap-content .vben-tiptap__image {
@apply my-4 block h-auto rounded-2xl border border-border;
max-width: min(100%, 640px);
}

97
packages/effects/plugins/src/tiptap/tiptap.vue

@ -7,15 +7,17 @@ import type {
import { computed, onBeforeUnmount, watch } from 'vue';
import { Check, ChevronDown } from '@vben/icons';
import { Check, ChevronDown, Eye } from '@vben/icons';
import { $t } from '@vben/locales';
import { useVbenModal } from '@vben-core/popup-ui';
import { VbenIconButton, VbenPopover } from '@vben-core/shadcn-ui';
import { cn } from '@vben-core/shared/utils';
import { EditorContent, useEditor } from '@tiptap/vue-3';
import { createDefaultTiptapExtensions } from './extensions';
import Preview from './preview.vue';
import { createToolbarGroups } from './toolbar';
import { useTiptapToolbar } from './use-tiptap-toolbar';
@ -24,6 +26,7 @@ const props = withDefaults(defineProps<TipTapProps>(), {
extensions: undefined,
minHeight: 240,
placeholder: $t('ui.tiptap.placeholder'),
previewable: true,
toolbar: true,
});
@ -39,7 +42,7 @@ const contentMinHeight = computed(() =>
: props.minHeight,
);
const tiptapContentClass = cn(
'vben-tiptap__content',
'vben-tiptap-content vben-tiptap__content',
'min-h-(--vben-tiptap-min-height) leading-7 text-foreground outline-none',
);
@ -74,6 +77,13 @@ const editor = useEditor({
const toolbarGroups = computed<ToolbarAction[][]>(() => {
return createToolbarGroups();
});
const previewContent = computed(
() => editor.value?.getHTML() ?? modelValue.value,
);
const [PreviewModal, previewModalApi] = useVbenModal({
footer: false,
fullscreenButton: false,
});
const {
applyPaletteColor,
canRunAction,
@ -92,6 +102,10 @@ const {
editor,
});
function openPreviewModal() {
previewModalApi.open();
}
watch(
() => props.editable,
(editable) => {
@ -244,67 +258,36 @@ onBeforeUnmount(() => {
class="ml-1 h-5 w-px bg-border"
></div>
</div>
<div v-if="previewable" class="ml-auto flex items-center">
<VbenIconButton
:aria-label="$t('ui.tiptap.toolbar.preview')"
:class="
getToolbarButtonClass({
action: () => {},
label: $t('ui.tiptap.toolbar.preview'),
})
"
:tooltip="$t('ui.tiptap.toolbar.preview')"
tooltip-side="top"
variant="ghost"
@click="openPreviewModal"
>
<Eye class="size-4" />
</VbenIconButton>
</div>
</div>
<EditorContent v-if="editor" :editor="editor" class="p-4" />
<PreviewModal
v-if="previewable"
:title="$t('ui.tiptap.toolbar.preview')"
class="w-4/5"
>
<Preview :content="previewContent" :min-height="320" variant="plain" />
</PreviewModal>
</div>
</template>
<style scoped>
.vben-tiptap :deep(.vben-tiptap__content > * + *) {
@apply mt-3;
}
.vben-tiptap :deep(.vben-tiptap__content h1) {
@apply text-2xl font-bold leading-[1.4];
}
.vben-tiptap :deep(.vben-tiptap__content h2) {
@apply text-xl font-bold leading-[1.45];
}
.vben-tiptap :deep(.vben-tiptap__content h3) {
@apply text-lg font-semibold leading-[1.5];
}
.vben-tiptap :deep(.vben-tiptap__content h4) {
@apply text-base font-semibold leading-[1.55];
}
.vben-tiptap :deep(.vben-tiptap__content ul) {
@apply list-disc pl-6;
}
.vben-tiptap :deep(.vben-tiptap__content ol) {
@apply list-decimal pl-6;
}
.vben-tiptap :deep(.vben-tiptap__content blockquote) {
@apply border-l-4 border-primary pl-4 text-muted-foreground;
}
.vben-tiptap :deep(.vben-tiptap__content a) {
@apply text-primary underline decoration-1 underline-offset-[3px];
}
.vben-tiptap :deep(.vben-tiptap__content code) {
@apply rounded-[0.45rem] border border-border bg-secondary px-[0.35rem] py-[0.15rem] text-[0.9em] text-primary;
}
.vben-tiptap :deep(.vben-tiptap__content pre) {
@apply overflow-x-auto rounded-[0.9rem] border border-border bg-popover p-4 text-popover-foreground;
}
.vben-tiptap :deep(.vben-tiptap__content pre code) {
@apply border-none bg-transparent p-0 text-inherit;
}
.vben-tiptap :deep(.vben-tiptap__content img),
.vben-tiptap :deep(.vben-tiptap__content .vben-tiptap__image) {
@apply my-4 block h-auto rounded-2xl border border-border;
max-width: min(100%, 640px);
}
.vben-tiptap
:deep(.vben-tiptap__content p.is-editor-empty:first-child::before) {
float: left;

7
packages/effects/plugins/src/tiptap/types.ts

@ -8,9 +8,16 @@ export interface TipTapProps {
extensions?: Extensions;
minHeight?: number | string;
placeholder?: string;
previewable?: boolean;
toolbar?: boolean;
}
export interface TipTapPreviewProps {
class?: any;
content?: string;
minHeight?: number | string;
}
export interface VbenTiptapChangeEvent {
html: string;
json: JSONContent;

1
packages/locales/src/langs/en-US/ui.json

@ -91,6 +91,7 @@
"alignLeft": "Left",
"alignCenter": "Center",
"alignRight": "Right",
"preview": "Preview",
"undo": "Undo",
"redo": "Redo",
"clear": "Clear"

1
packages/locales/src/langs/zh-CN/ui.json

@ -91,6 +91,7 @@
"alignLeft": "左对齐",
"alignCenter": "居中",
"alignRight": "右对齐",
"preview": "预览",
"undo": "撤销",
"redo": "重做",
"clear": "清除"

8
playground/src/views/examples/tiptap/index.vue

@ -2,7 +2,7 @@
import { computed, ref } from 'vue';
import { Page } from '@vben/common-ui';
import { VbenTiptap } from '@vben/plugins/tiptap';
import { VbenTiptap, VbenTiptapPreview } from '@vben/plugins/tiptap';
import { Card } from 'ant-design-vue';
@ -28,8 +28,12 @@ const previewContent = computed(() => content.value);
<VbenTiptap v-model="content" />
</Card>
<Card class="mb-5" title="富文本预览">
<VbenTiptapPreview :content="previewContent" />
</Card>
<Card title="HTML 输出">
<pre class="overflow-auto">
<pre class="overflow-auto rounded-xl border border-border bg-muted p-4">
{{ previewContent }}
</pre>
</Card>

Loading…
Cancel
Save