mirror of https://github.com/halo-dev/halo
feat: add supports for hide the sidebar of editor (#4942)
#### What type of PR is this? /area console /area editor /kind feature /milestone 2.11.0 #### What this PR does / why we need it: 支持隐藏/显示默认编辑器的侧边栏。 <img width="1451" alt="图片" src="https://github.com/halo-dev/halo/assets/21301288/6f641230-8f31-4f6b-83d2-e92dbc0303e8"> <img width="1363" alt="图片" src="https://github.com/halo-dev/halo/assets/21301288/921aa75e-ffd5-4785-9507-0b3b361efa31"> #### Which issue(s) this PR fixes: Fixes # #### Special notes for your reviewer: 测试截图中隐藏/显示侧边栏的按钮是否正常工作即可。 #### Does this PR introduce a user-facing change? ```release-note 支持隐藏/显示默认编辑器的侧边栏。 ```pull/4955/head v2.11.0-rc.2
parent
8f83df415c
commit
7a84f55300
|
@ -5,9 +5,10 @@ import { VTooltip } from "floating-vue";
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
tooltip?: string;
|
||||
modelValue: string;
|
||||
modelValue?: string;
|
||||
}>(),
|
||||
{
|
||||
modelValue: "",
|
||||
tooltip: undefined,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -6,7 +6,7 @@ import { ref, type Component } from "vue";
|
|||
const props = withDefaults(
|
||||
defineProps<{
|
||||
editor: Editor;
|
||||
isActive: ({ editor }: { editor: Editor }) => boolean;
|
||||
isActive?: ({ editor }: { editor: Editor }) => boolean;
|
||||
visible?: ({ editor }: { editor: Editor }) => boolean;
|
||||
icon?: Component;
|
||||
iconStyle?: string;
|
||||
|
|
|
@ -44,7 +44,7 @@ onMounted(() => {
|
|||
<template>
|
||||
<node-view-wrapper as="div" class="inline-block w-full">
|
||||
<div
|
||||
class="inline-block overflow-hidden transition-all text-center relative h-full"
|
||||
class="inline-block overflow-hidden transition-all text-center relative h-full max-w-full"
|
||||
:style="{
|
||||
width: node.attrs.width,
|
||||
}"
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<script setup lang="ts">
|
||||
import { i18n } from "@/locales";
|
||||
import type { Editor } from "@/tiptap/vue-3";
|
||||
import { computed, type Component, onUnmounted, ref, watch } from "vue";
|
||||
import { computed, type Component } from "vue";
|
||||
import Image from "./index";
|
||||
import {
|
||||
BlockActionButton,
|
||||
|
@ -12,30 +12,16 @@ import MdiBackupRestore from "~icons/mdi/backup-restore";
|
|||
import MdiImageSizeSelectActual from "~icons/mdi/image-size-select-actual";
|
||||
import MdiImageSizeSelectLarge from "~icons/mdi/image-size-select-large";
|
||||
import MdiImageSizeSelectSmall from "~icons/mdi/image-size-select-small";
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
isActive: ({ editor }: { editor: Editor }) => boolean;
|
||||
isActive?: ({ editor }: { editor: Editor }) => boolean;
|
||||
visible?: ({ editor }: { editor: Editor }) => boolean;
|
||||
icon?: Component;
|
||||
title?: string;
|
||||
action?: ({ editor }: { editor: Editor }) => void;
|
||||
}>();
|
||||
|
||||
const nodeDom = computed(() => {
|
||||
if (!props.editor.isActive(Image.name)) {
|
||||
return;
|
||||
}
|
||||
const nodeDomParent = props.editor.view.nodeDOM(
|
||||
props.editor.state.selection.from
|
||||
) as HTMLElement;
|
||||
if (nodeDomParent && nodeDomParent.hasChildNodes()) {
|
||||
return nodeDomParent.childNodes[0] as HTMLElement;
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const width = computed({
|
||||
get: () => {
|
||||
return props.editor.getAttributes(Image.name).width;
|
||||
|
@ -54,70 +40,13 @@ const height = computed({
|
|||
},
|
||||
});
|
||||
|
||||
let mounted = false;
|
||||
const imgScale = ref<number>(0);
|
||||
|
||||
watch(nodeDom, () => {
|
||||
resetResizeObserver();
|
||||
});
|
||||
|
||||
const reuseResizeObserver = () => {
|
||||
let init = true;
|
||||
return useResizeObserver(
|
||||
nodeDom.value,
|
||||
(entries) => {
|
||||
// Skip first call
|
||||
if (!mounted) {
|
||||
mounted = true;
|
||||
return;
|
||||
}
|
||||
const entry = entries[0];
|
||||
const { width: w, height: h } = entry.contentRect;
|
||||
if (init) {
|
||||
imgScale.value = parseFloat((h / w).toFixed(2));
|
||||
init = false;
|
||||
return;
|
||||
}
|
||||
const node = props.editor.view.nodeDOM(props.editor.state.selection.from);
|
||||
if (!node) {
|
||||
return;
|
||||
}
|
||||
props.editor
|
||||
.chain()
|
||||
.updateAttributes(Image.name, {
|
||||
width: w + "px",
|
||||
height: w * imgScale.value + "px",
|
||||
})
|
||||
.setNodeSelection(props.editor.state.selection.from)
|
||||
.focus()
|
||||
.run();
|
||||
},
|
||||
{ box: "border-box" }
|
||||
);
|
||||
};
|
||||
|
||||
let resizeObserver = reuseResizeObserver();
|
||||
|
||||
window.addEventListener("resize", resetResizeObserver);
|
||||
|
||||
onUnmounted(() => {
|
||||
window.removeEventListener("resize", resetResizeObserver);
|
||||
});
|
||||
|
||||
function resetResizeObserver() {
|
||||
resizeObserver.stop();
|
||||
resizeObserver = reuseResizeObserver();
|
||||
}
|
||||
|
||||
function handleSetSize(width?: string, height?: string) {
|
||||
resizeObserver.stop();
|
||||
props.editor
|
||||
.chain()
|
||||
.updateAttributes(Image.name, { width, height })
|
||||
.setNodeSelection(props.editor.state.selection.from)
|
||||
.focus()
|
||||
.run();
|
||||
resizeObserver = reuseResizeObserver();
|
||||
}
|
||||
</script>
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import type { Editor, Node } from "@/tiptap/vue-3";
|
|||
import { NodeViewWrapper } from "@/tiptap/vue-3";
|
||||
import type { Node as ProseMirrorNode, Decoration } from "@/tiptap/pm";
|
||||
import { computed, onMounted, ref } from "vue";
|
||||
import { useResizeObserver } from "@vueuse/core";
|
||||
import Image from "./index";
|
||||
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
|
@ -45,29 +45,55 @@ const href = computed({
|
|||
props.updateAttributes({ href: href });
|
||||
},
|
||||
});
|
||||
|
||||
function handleSetFocus() {
|
||||
props.editor.commands.setNodeSelection(props.getPos());
|
||||
}
|
||||
|
||||
const inputRef = ref();
|
||||
const resizeRef = ref();
|
||||
const init = ref(true);
|
||||
const resizeRef = ref<HTMLDivElement>();
|
||||
|
||||
onMounted(() => {
|
||||
if (!src.value) {
|
||||
inputRef.value.focus();
|
||||
} else {
|
||||
useResizeObserver(resizeRef.value, (entries) => {
|
||||
const entry = entries[0];
|
||||
const { height } = entry.contentRect;
|
||||
if (height == 0) {
|
||||
return;
|
||||
}
|
||||
if (!props.selected && !init.value) {
|
||||
handleSetFocus();
|
||||
}
|
||||
init.value = false;
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!resizeRef.value) return;
|
||||
|
||||
let startX: number, startWidth: number;
|
||||
|
||||
resizeRef.value.addEventListener("mousedown", function (e) {
|
||||
startX = e.clientX;
|
||||
startWidth = resizeRef.value?.clientWidth || 1;
|
||||
document.documentElement.addEventListener("mousemove", doDrag, false);
|
||||
document.documentElement.addEventListener("mouseup", stopDrag, false);
|
||||
});
|
||||
|
||||
function doDrag(e: MouseEvent) {
|
||||
if (!resizeRef.value) return;
|
||||
|
||||
const aspectRatio =
|
||||
resizeRef.value.clientWidth / resizeRef.value.clientHeight;
|
||||
|
||||
const newWidth = Math.min(
|
||||
startWidth + e.clientX - startX,
|
||||
resizeRef.value.parentElement?.clientWidth || 0
|
||||
);
|
||||
|
||||
const width = newWidth.toFixed(0) + "px";
|
||||
const height = (newWidth / aspectRatio).toFixed(0) + "px";
|
||||
props.editor
|
||||
.chain()
|
||||
.updateAttributes(Image.name, { width, height })
|
||||
.setNodeSelection(props.editor.state.selection.from)
|
||||
.focus()
|
||||
.run();
|
||||
}
|
||||
|
||||
function stopDrag() {
|
||||
document.documentElement.removeEventListener("mousemove", doDrag, false);
|
||||
document.documentElement.removeEventListener("mouseup", stopDrag, false);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
@ -87,7 +113,7 @@ onMounted(() => {
|
|||
<div
|
||||
v-else
|
||||
ref="resizeRef"
|
||||
class="resize-x inline-block overflow-hidden text-center relative rounded-md"
|
||||
class="resize-x inline-block overflow-hidden text-center relative rounded-md max-w-full"
|
||||
:class="{
|
||||
'ring-2 rounded': selected,
|
||||
}"
|
||||
|
|
|
@ -131,6 +131,7 @@ const Image = TiptapImage.extend<ExtensionOptions & ImageOptions>({
|
|||
shouldShow: ({ state }: { state: EditorState }): boolean => {
|
||||
return isActive(state, Image.name);
|
||||
},
|
||||
defaultAnimation: false,
|
||||
items: [
|
||||
{
|
||||
priority: 10,
|
||||
|
|
|
@ -6,7 +6,7 @@ import Video from "./index";
|
|||
import { i18n } from "@/locales";
|
||||
const props = defineProps<{
|
||||
editor: Editor;
|
||||
isActive: ({ editor }: { editor: Editor }) => boolean;
|
||||
isActive?: ({ editor }: { editor: Editor }) => boolean;
|
||||
visible?: ({ editor }: { editor: Editor }) => boolean;
|
||||
icon?: Component;
|
||||
title?: string;
|
||||
|
|
|
@ -53,7 +53,7 @@ onMounted(() => {
|
|||
<template>
|
||||
<node-view-wrapper as="div" class="inline-block w-full">
|
||||
<div
|
||||
class="inline-block overflow-hidden transition-all text-center relative h-full"
|
||||
class="inline-block overflow-hidden transition-all text-center relative h-full max-w-full"
|
||||
:style="{
|
||||
width: node.attrs.width,
|
||||
}"
|
||||
|
|
|
@ -44,6 +44,7 @@ import {
|
|||
ExtensionColumn,
|
||||
ExtensionNodeSelected,
|
||||
ExtensionTrailingNode,
|
||||
ToolbarItem,
|
||||
} from "@halo-dev/richtext-editor";
|
||||
import {
|
||||
IconCalendar,
|
||||
|
@ -62,6 +63,7 @@ import MdiFormatHeader3 from "~icons/mdi/format-header-3";
|
|||
import MdiFormatHeader4 from "~icons/mdi/format-header-4";
|
||||
import MdiFormatHeader5 from "~icons/mdi/format-header-5";
|
||||
import MdiFormatHeader6 from "~icons/mdi/format-header-6";
|
||||
import RiLayoutRightLine from "~icons/ri/layout-right-line";
|
||||
import {
|
||||
inject,
|
||||
markRaw,
|
||||
|
@ -82,7 +84,7 @@ import { i18n } from "@/locales";
|
|||
import { OverlayScrollbarsComponent } from "overlayscrollbars-vue";
|
||||
import { usePluginModuleStore } from "@/stores/plugin";
|
||||
import type { PluginModule } from "@halo-dev/console-shared";
|
||||
import { useDebounceFn } from "@vueuse/core";
|
||||
import { useDebounceFn, useLocalStorage } from "@vueuse/core";
|
||||
import { onBeforeUnmount } from "vue";
|
||||
import { generateAnchor } from "@/utils/anchor";
|
||||
import { usePermission } from "@/utils/permission";
|
||||
|
@ -137,6 +139,8 @@ const editor = shallowRef<Editor>();
|
|||
|
||||
const { pluginModules } = usePluginModuleStore();
|
||||
|
||||
const showSidebar = useLocalStorage("halo:editor:show-sidebar", true);
|
||||
|
||||
onMounted(() => {
|
||||
const extensionsFromPlugins: AnyExtension[] = [];
|
||||
pluginModules.forEach((pluginModule: PluginModule) => {
|
||||
|
@ -259,6 +263,23 @@ onMounted(() => {
|
|||
},
|
||||
];
|
||||
},
|
||||
getToolbarItems({ editor }: { editor: Editor }) {
|
||||
return {
|
||||
priority: 1000,
|
||||
component: markRaw(ToolbarItem),
|
||||
props: {
|
||||
editor,
|
||||
isActive: showSidebar.value,
|
||||
icon: markRaw(RiLayoutRightLine),
|
||||
title: i18n.global.t(
|
||||
"core.components.default_editor.toolbox.show_hide_sidebar"
|
||||
),
|
||||
action: () => {
|
||||
showSidebar.value = !showSidebar.value;
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
}),
|
||||
|
@ -463,7 +484,7 @@ const currentLocale = i18n.global.locale.value as
|
|||
@select="onAttachmentSelect"
|
||||
/>
|
||||
<RichTextEditor v-if="editor" :editor="editor" :locale="currentLocale">
|
||||
<template #extra>
|
||||
<template v-if="showSidebar" #extra>
|
||||
<OverlayScrollbarsComponent
|
||||
element="div"
|
||||
:options="{ scrollbars: { autoHide: 'scroll' } }"
|
||||
|
@ -496,7 +517,9 @@ const currentLocale = i18n.global.locale.value as
|
|||
:is="headingIcons[node.level]"
|
||||
class="h-4 w-4 rounded-sm bg-gray-100 p-0.5 group-hover:bg-white"
|
||||
:class="[
|
||||
{ '!bg-white': node.id === selectedHeadingNode?.id },
|
||||
{
|
||||
'!bg-white': node.id === selectedHeadingNode?.id,
|
||||
},
|
||||
]"
|
||||
/>
|
||||
<span class="flex-1 truncate">{{ node.text }}</span>
|
||||
|
|
|
@ -1205,6 +1205,7 @@ core:
|
|||
placeholder: "Enter / to select input type."
|
||||
toolbox:
|
||||
attachment: Attachment
|
||||
show_hide_sidebar: Show/Hide Sidebar
|
||||
global_search:
|
||||
placeholder: Enter keywords to search
|
||||
no_results: No search results
|
||||
|
|
|
@ -1205,6 +1205,7 @@ core:
|
|||
placeholder: "输入 / 以选择输入类型"
|
||||
toolbox:
|
||||
attachment: 选择附件
|
||||
show_hide_sidebar: 显示 / 隐藏侧边栏
|
||||
global_search:
|
||||
placeholder: 输入关键词以搜索
|
||||
no_results: 没有搜索结果
|
||||
|
|
|
@ -1205,6 +1205,7 @@ core:
|
|||
placeholder: "輸入 / 以選擇輸入類型"
|
||||
toolbox:
|
||||
attachment: 選擇附件
|
||||
show_hide_sidebar: 顯示 / 隱藏側邊欄
|
||||
global_search:
|
||||
placeholder: 輸入關鍵字以搜尋
|
||||
no_results: 沒有搜尋結果
|
||||
|
|
Loading…
Reference in New Issue