mirror of https://github.com/halo-dev/halo
refactor: editor code block to be extensible by plugins (#6428)
#### What type of PR is this? /kind improvement /area editor /area ui #### What this PR does / why we need it: 此 PR 重构了默认编辑器中代码块的相关代码,使编辑器能够被插件扩展。这样做的优点是当用户使用例如 [highlightjs](https://github.com/halo-sigs/plugin-highlightjs) 这类的高亮插件时,可以保证在 Console 端选择的语言及主题可以在主题端完美适配。 具体做了以下几处重构: 1. 在默认编辑器的代码块中,不再提供高亮样式,代码块高亮将完全交由插件来适配。(但可以在默认的代码块中自定义输入语言,主题端自行处理)。 2. 为了防止出现重复的插件功能,将会根据插件优先级及加载顺序(后加载优先级高于先加载),对同类型且同名的插件进行过滤,只保留一个。 3. 重构代码块 `Select` 组件,使得选择语言或主题更加方便。 4. 为代码块提供主题的扩展设置项。 建议在此 PR 合并之后,由 Halo 默认提供一个高亮插件作为预设插件,这样可以用于解决原有功能升级之后丢失的问题。 <img width="1067" alt="image" src="https://github.com/user-attachments/assets/f9e2c5eb-a48a-4d2c-9fee-442e9d16ef19"> #### How to test it? 测试是否会改变已有代码块的语言等。 测试使用第三方插件之后,是否具有高亮。 设置高亮语言后,保存并刷新,查看高亮语言是否存在。主题同理。 查看主题端是否能够正常渲染。 #### Does this PR introduce a user-facing change? ```release-note 重构默认编辑器代码块使其能够被插件扩展。 ```pull/6515/head
parent
fe842e8d77
commit
21db06a507
|
@ -47,7 +47,6 @@
|
||||||
"@tiptap/extension-bullet-list": "^2.6.5",
|
"@tiptap/extension-bullet-list": "^2.6.5",
|
||||||
"@tiptap/extension-code": "^2.6.5",
|
"@tiptap/extension-code": "^2.6.5",
|
||||||
"@tiptap/extension-code-block": "^2.6.5",
|
"@tiptap/extension-code-block": "^2.6.5",
|
||||||
"@tiptap/extension-code-block-lowlight": "^2.6.5",
|
|
||||||
"@tiptap/extension-color": "^2.6.5",
|
"@tiptap/extension-color": "^2.6.5",
|
||||||
"@tiptap/extension-document": "^2.6.5",
|
"@tiptap/extension-document": "^2.6.5",
|
||||||
"@tiptap/extension-dropcursor": "^2.6.5",
|
"@tiptap/extension-dropcursor": "^2.6.5",
|
||||||
|
@ -80,9 +79,7 @@
|
||||||
"@tiptap/vue-3": "^2.6.5",
|
"@tiptap/vue-3": "^2.6.5",
|
||||||
"floating-vue": "^5.2.2",
|
"floating-vue": "^5.2.2",
|
||||||
"github-markdown-css": "^5.2.0",
|
"github-markdown-css": "^5.2.0",
|
||||||
"highlight.js": "11.8.0",
|
|
||||||
"linkifyjs": "^4.1.3",
|
"linkifyjs": "^4.1.3",
|
||||||
"lowlight": "^3.0.0",
|
|
||||||
"scroll-into-view-if-needed": "^3.1.0",
|
"scroll-into-view-if-needed": "^3.1.0",
|
||||||
"tippy.js": "^6.3.7"
|
"tippy.js": "^6.3.7"
|
||||||
},
|
},
|
||||||
|
|
|
@ -45,7 +45,6 @@ import {
|
||||||
ExtensionUnderline,
|
ExtensionUnderline,
|
||||||
ExtensionVideo,
|
ExtensionVideo,
|
||||||
RichTextEditor,
|
RichTextEditor,
|
||||||
lowlight,
|
|
||||||
useEditor,
|
useEditor,
|
||||||
} from "../index";
|
} from "../index";
|
||||||
|
|
||||||
|
@ -99,9 +98,7 @@ const editor = useEditor({
|
||||||
ExtensionVideo,
|
ExtensionVideo,
|
||||||
ExtensionAudio,
|
ExtensionAudio,
|
||||||
ExtensionCommands,
|
ExtensionCommands,
|
||||||
ExtensionCodeBlock.configure({
|
ExtensionCodeBlock,
|
||||||
lowlight,
|
|
||||||
}),
|
|
||||||
ExtensionIframe,
|
ExtensionIframe,
|
||||||
ExtensionColor,
|
ExtensionColor,
|
||||||
ExtensionFontSize,
|
ExtensionFontSize,
|
||||||
|
|
|
@ -0,0 +1,215 @@
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed, defineProps, ref, watch } from "vue";
|
||||||
|
import IconArrowDownLine from "~icons/ri/arrow-down-s-line";
|
||||||
|
import { Dropdown as VDropdown } from "floating-vue";
|
||||||
|
|
||||||
|
export interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
const props = defineProps<{
|
||||||
|
container?: any;
|
||||||
|
containerClass?: string;
|
||||||
|
options: Option[];
|
||||||
|
filterSort?: (options: Option[], query: string) => number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const value = defineModel({
|
||||||
|
default: "",
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: "select"): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const isFocus = ref(false);
|
||||||
|
const inputValue = ref<string>("");
|
||||||
|
const selectedOption = ref<Option | null>(null);
|
||||||
|
const inputRef = ref<HTMLInputElement | null>(null);
|
||||||
|
|
||||||
|
const displayLabel = computed(() => {
|
||||||
|
if (selectedOption.value) {
|
||||||
|
return selectedOption.value.label;
|
||||||
|
}
|
||||||
|
return value.value;
|
||||||
|
});
|
||||||
|
|
||||||
|
const filterOptions = computed(() => {
|
||||||
|
if (!inputValue.value) {
|
||||||
|
return props.options;
|
||||||
|
}
|
||||||
|
return props.options.filter((option) =>
|
||||||
|
option.value
|
||||||
|
.toLocaleLowerCase()
|
||||||
|
.includes(inputValue.value.toLocaleLowerCase())
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
isFocus.value = true;
|
||||||
|
setTimeout(() => {
|
||||||
|
handleScrollIntoView();
|
||||||
|
}, 50);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = () => {
|
||||||
|
isFocus.value = false;
|
||||||
|
if (inputValue.value) {
|
||||||
|
value.value = inputValue.value;
|
||||||
|
inputValue.value = "";
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSelectOption = (option: Option) => {
|
||||||
|
selectedOption.value = option;
|
||||||
|
value.value = option.value;
|
||||||
|
inputValue.value = "";
|
||||||
|
inputRef.value?.blur();
|
||||||
|
emit("select");
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedIndex = ref(-1);
|
||||||
|
|
||||||
|
const handleOptionKeydown = (event: KeyboardEvent) => {
|
||||||
|
const key = event.key;
|
||||||
|
if (key === "ArrowUp") {
|
||||||
|
selectedIndex.value =
|
||||||
|
(selectedIndex.value - 1 + filterOptions.value.length) %
|
||||||
|
filterOptions.value.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "ArrowDown") {
|
||||||
|
selectedIndex.value =
|
||||||
|
(selectedIndex.value + 1) % filterOptions.value.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "Enter") {
|
||||||
|
if (selectedIndex.value === -1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
handleSelectOption(filterOptions.value[selectedIndex.value]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
watch(
|
||||||
|
value,
|
||||||
|
(newValue) => {
|
||||||
|
if (newValue) {
|
||||||
|
selectedOption.value =
|
||||||
|
props.options.find((option) => option.value === newValue) || null;
|
||||||
|
selectedIndex.value = props.options.findIndex(
|
||||||
|
(option) => option.value === newValue
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(
|
||||||
|
selectedIndex,
|
||||||
|
() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleScrollIntoView();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
{
|
||||||
|
immediate: true,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
const handleScrollIntoView = () => {
|
||||||
|
if (selectedIndex.value === -1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const optionElement = document.querySelector(
|
||||||
|
`.select > div:nth-child(${selectedIndex.value + 1})`
|
||||||
|
);
|
||||||
|
if (optionElement) {
|
||||||
|
optionElement.scrollIntoView({
|
||||||
|
behavior: "instant",
|
||||||
|
block: "nearest",
|
||||||
|
inline: "nearest",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<template>
|
||||||
|
<VDropdown
|
||||||
|
:triggers="[]"
|
||||||
|
:shown="isFocus"
|
||||||
|
:auto-hide="false"
|
||||||
|
:distance="0"
|
||||||
|
auto-size
|
||||||
|
:container="container || 'body'"
|
||||||
|
>
|
||||||
|
<div class="relative inline-block w-full" @keydown="handleOptionKeydown">
|
||||||
|
<div class="h-8">
|
||||||
|
<div
|
||||||
|
class="select-input w-full h-full grid items-center text-sm rounded-md px-3 cursor-pointer box-border"
|
||||||
|
:class="{
|
||||||
|
'bg-white': isFocus,
|
||||||
|
'border-[1px]': isFocus,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="absolute top-0 bottom-0">
|
||||||
|
<input
|
||||||
|
ref="inputRef"
|
||||||
|
v-model="inputValue"
|
||||||
|
class="appearance-none bg-transparent h-full ps-0 pe-0 border-none outline-none m-0 p-0 cursor-auto"
|
||||||
|
:placeholder="isFocus ? displayLabel : ''"
|
||||||
|
@focus="handleInputFocus"
|
||||||
|
@blur="handleInputBlur"
|
||||||
|
/>
|
||||||
|
</span>
|
||||||
|
<span v-show="!isFocus" class="text-ellipsis text-sm">
|
||||||
|
{{ displayLabel }}
|
||||||
|
</span>
|
||||||
|
<span class="justify-self-end" @click="inputRef?.focus()">
|
||||||
|
<IconArrowDownLine />
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #popper>
|
||||||
|
<div class="bg-white">
|
||||||
|
<div class="select max-h-64 cursor-pointer p-1">
|
||||||
|
<template v-if="filterOptions && filterOptions.length > 0">
|
||||||
|
<div
|
||||||
|
v-for="(option, index) in filterOptions"
|
||||||
|
:key="option.value"
|
||||||
|
:index="index"
|
||||||
|
class="w-full h-8 flex items-center rounded-md text-base px-3 py-1 hover:bg-zinc-100"
|
||||||
|
:class="{
|
||||||
|
'bg-zinc-200': option.value === value,
|
||||||
|
'bg-zinc-100': selectedIndex === index,
|
||||||
|
}"
|
||||||
|
@mousedown="handleSelectOption(option)"
|
||||||
|
>
|
||||||
|
<span class="flex-1 text-ellipsis text-sm">
|
||||||
|
{{ option.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="w-full h-8 flex items-center rounded-md text-base px-3 py-1"
|
||||||
|
>
|
||||||
|
<span class="flex-1 text-ellipsis text-sm">No options</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</VDropdown>
|
||||||
|
</template>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.select-input {
|
||||||
|
grid-template-columns: 1fr auto;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,40 +1,80 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
import type { Decoration, Node as ProseMirrorNode } from "@/tiptap/pm";
|
import {
|
||||||
import type { Editor, Node } from "@/tiptap/vue-3";
|
NodeViewContent,
|
||||||
import { NodeViewContent, NodeViewWrapper } from "@/tiptap/vue-3";
|
NodeViewWrapper,
|
||||||
|
type NodeViewProps,
|
||||||
|
} from "@/tiptap/vue-3";
|
||||||
import { useTimeout } from "@vueuse/core";
|
import { useTimeout } from "@vueuse/core";
|
||||||
import { computed } from "vue";
|
import { computed } from "vue";
|
||||||
import BxBxsCopy from "~icons/bx/bxs-copy";
|
import BxBxsCopy from "~icons/bx/bxs-copy";
|
||||||
import RiArrowDownSFill from "~icons/ri/arrow-down-s-fill";
|
import RiArrowDownSFill from "~icons/ri/arrow-down-s-fill";
|
||||||
import RiArrowRightSFill from "~icons/ri/arrow-right-s-fill";
|
import RiArrowRightSFill from "~icons/ri/arrow-right-s-fill";
|
||||||
import IconCheckboxCircle from "~icons/ri/checkbox-circle-line";
|
import IconCheckboxCircle from "~icons/ri/checkbox-circle-line";
|
||||||
import lowlight from "./lowlight";
|
import CodeBlockSelect from "./CodeBlockSelect.vue";
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<NodeViewProps>();
|
||||||
editor: Editor;
|
|
||||||
node: ProseMirrorNode;
|
|
||||||
decorations: Decoration[];
|
|
||||||
selected: boolean;
|
|
||||||
extension: Node<any, any>;
|
|
||||||
getPos: () => number;
|
|
||||||
updateAttributes: (attributes: Record<string, any>) => void;
|
|
||||||
deleteNode: () => void;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const languages = computed(() => {
|
const languageOptions = computed(() => {
|
||||||
return lowlight.listLanguages();
|
let languages: Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}> = [];
|
||||||
|
const lang = props.extension.options.languages;
|
||||||
|
if (typeof lang === "function") {
|
||||||
|
languages = lang(props.editor.state);
|
||||||
|
} else {
|
||||||
|
languages = lang;
|
||||||
|
}
|
||||||
|
languages = languages || [];
|
||||||
|
const languageValues = languages.map((language) => language.value);
|
||||||
|
if (languageValues.indexOf("auto") === -1) {
|
||||||
|
languages.unshift({
|
||||||
|
label: "Auto",
|
||||||
|
value: "auto",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return languages;
|
||||||
});
|
});
|
||||||
|
|
||||||
const selectedLanguage = computed({
|
const selectedLanguage = computed({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.node?.attrs.language;
|
return props.node?.attrs.language || "auto";
|
||||||
},
|
},
|
||||||
set: (language: string) => {
|
set: (language: string) => {
|
||||||
props.updateAttributes({ language: language });
|
props.updateAttributes({ language: language });
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const themeOptions = computed(() => {
|
||||||
|
let themes:
|
||||||
|
| Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>
|
||||||
|
| undefined = [];
|
||||||
|
const theme = props.extension.options.themes;
|
||||||
|
if (typeof theme === "function") {
|
||||||
|
themes = theme(props.editor.state);
|
||||||
|
} else {
|
||||||
|
themes = theme;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!themes) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return themes;
|
||||||
|
});
|
||||||
|
|
||||||
|
const selectedTheme = computed({
|
||||||
|
get: () => {
|
||||||
|
return props.node?.attrs.theme || themeOptions.value?.[0].value;
|
||||||
|
},
|
||||||
|
set: (theme: string) => {
|
||||||
|
props.updateAttributes({ theme: theme });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const collapsed = computed<boolean>({
|
const collapsed = computed<boolean>({
|
||||||
get: () => {
|
get: () => {
|
||||||
return props.node.attrs.collapsed || false;
|
return props.node.attrs.collapsed || false;
|
||||||
|
@ -76,19 +116,23 @@ const handleCopyCode = () => {
|
||||||
<RiArrowDownSFill v-else />
|
<RiArrowDownSFill v-else />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<select
|
<CodeBlockSelect
|
||||||
v-model="selectedLanguage"
|
v-model="selectedLanguage"
|
||||||
class="block !leading-8 text-sm text-gray-900 border select-none border-transparent rounded-md bg-transparent focus:ring-blue-500 focus:border-blue-500 cursor-pointer hover:bg-zinc-200"
|
class="w-48"
|
||||||
|
:container="editor.options.element"
|
||||||
|
:options="languageOptions"
|
||||||
|
@select="editor.commands.focus()"
|
||||||
>
|
>
|
||||||
<option :value="null">auto</option>
|
</CodeBlockSelect>
|
||||||
<option
|
<CodeBlockSelect
|
||||||
v-for="(language, index) in languages"
|
v-if="themeOptions && themeOptions.length > 0"
|
||||||
:key="index"
|
v-model="selectedTheme"
|
||||||
:value="language"
|
:container="editor.options.element"
|
||||||
|
class="w-48"
|
||||||
|
:options="themeOptions"
|
||||||
|
@select="editor.commands.focus()"
|
||||||
>
|
>
|
||||||
{{ language }}
|
</CodeBlockSelect>
|
||||||
</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pr-3 flex items-center">
|
<div class="pr-3 flex items-center">
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -19,17 +19,10 @@ import {
|
||||||
type Range,
|
type Range,
|
||||||
} from "@/tiptap/vue-3";
|
} from "@/tiptap/vue-3";
|
||||||
import { deleteNode } from "@/utils";
|
import { deleteNode } from "@/utils";
|
||||||
import type { CodeBlockLowlightOptions } from "@tiptap/extension-code-block-lowlight";
|
|
||||||
import CodeBlockLowlight from "@tiptap/extension-code-block-lowlight";
|
|
||||||
import { markRaw } from "vue";
|
import { markRaw } from "vue";
|
||||||
import MdiCodeBracesBox from "~icons/mdi/code-braces-box";
|
import MdiCodeBracesBox from "~icons/mdi/code-braces-box";
|
||||||
import CodeBlockViewRenderer from "./CodeBlockViewRenderer.vue";
|
import CodeBlockViewRenderer from "./CodeBlockViewRenderer.vue";
|
||||||
|
import TiptapCodeBlock from "@tiptap/extension-code-block";
|
||||||
export interface CustomCodeBlockLowlightOptions
|
|
||||||
extends CodeBlockLowlightOptions {
|
|
||||||
lowlight: any;
|
|
||||||
defaultLanguage: string | null | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
declare module "@/tiptap" {
|
declare module "@/tiptap" {
|
||||||
interface Commands<ReturnType> {
|
interface Commands<ReturnType> {
|
||||||
|
@ -97,13 +90,77 @@ const getRenderContainer = (node: HTMLElement) => {
|
||||||
return container;
|
return container;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default CodeBlockLowlight.extend<
|
export interface Option {
|
||||||
CustomCodeBlockLowlightOptions & CodeBlockLowlightOptions
|
label: string;
|
||||||
>({
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CodeBlockOptions {
|
||||||
|
/**
|
||||||
|
* Define whether the node should be exited on triple enter.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
exitOnTripleEnter: boolean;
|
||||||
|
/**
|
||||||
|
* Define whether the node should be exited on arrow down if there is no node after it.
|
||||||
|
* @default true
|
||||||
|
*/
|
||||||
|
exitOnArrowDown: boolean;
|
||||||
|
/**
|
||||||
|
* Custom HTML attributes that should be added to the rendered HTML tag.
|
||||||
|
* @default {}
|
||||||
|
* @example { class: 'foo' }
|
||||||
|
*/
|
||||||
|
HTMLAttributes: Record<string, any>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default language for code block
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
defaultLanguage: string | null | undefined;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The default theme for code block
|
||||||
|
* @default null
|
||||||
|
*/
|
||||||
|
defaultTheme: string | null | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ExtensionCodeBlockOptions extends CodeBlockOptions {
|
||||||
|
/**
|
||||||
|
* Used for language list
|
||||||
|
*
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
languages:
|
||||||
|
| Array<Option>
|
||||||
|
| ((state: EditorState) => Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used for theme list
|
||||||
|
*
|
||||||
|
* @default []
|
||||||
|
*/
|
||||||
|
themes?:
|
||||||
|
| Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>
|
||||||
|
| ((state: EditorState) => Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
}>);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TiptapCodeBlock.extend<ExtensionCodeBlockOptions>({
|
||||||
allowGapCursor: true,
|
allowGapCursor: true,
|
||||||
// It needs to have a higher priority than range-selection,
|
// It needs to have a higher priority than range-selection,
|
||||||
// otherwise the Mod-a shortcut key will be overridden.
|
// otherwise the Mod-a shortcut key will be overridden.
|
||||||
priority: 110,
|
priority: 110,
|
||||||
|
|
||||||
fakeSelection: true,
|
fakeSelection: true,
|
||||||
|
|
||||||
addAttributes() {
|
addAttributes() {
|
||||||
|
@ -121,6 +178,18 @@ export default CodeBlockLowlight.extend<
|
||||||
return {};
|
return {};
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
theme: {
|
||||||
|
default: this.options.defaultTheme,
|
||||||
|
parseHTML: (element) => element.getAttribute("theme") || null,
|
||||||
|
renderHTML: (attributes) => {
|
||||||
|
if (attributes.theme) {
|
||||||
|
return {
|
||||||
|
theme: attributes.theme,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -197,7 +266,7 @@ export default CodeBlockLowlight.extend<
|
||||||
if (this.editor.isActive("codeBlock")) {
|
if (this.editor.isActive("codeBlock")) {
|
||||||
const { tr, selection } = this.editor.state;
|
const { tr, selection } = this.editor.state;
|
||||||
const codeBlack = findParentNode(
|
const codeBlack = findParentNode(
|
||||||
(node) => node.type.name === CodeBlockLowlight.name
|
(node) => node.type.name === TiptapCodeBlock.name
|
||||||
)(selection);
|
)(selection);
|
||||||
if (!codeBlack) {
|
if (!codeBlack) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -218,9 +287,14 @@ export default CodeBlockLowlight.extend<
|
||||||
addNodeView() {
|
addNodeView() {
|
||||||
return VueNodeViewRenderer(CodeBlockViewRenderer);
|
return VueNodeViewRenderer(CodeBlockViewRenderer);
|
||||||
},
|
},
|
||||||
|
|
||||||
addOptions() {
|
addOptions() {
|
||||||
return {
|
return {
|
||||||
...this.parent?.(),
|
...this.parent?.(),
|
||||||
|
languages: [],
|
||||||
|
themes: [],
|
||||||
|
defaultLanguage: null,
|
||||||
|
defaultTheme: null,
|
||||||
getToolbarItems({ editor }: { editor: Editor }) {
|
getToolbarItems({ editor }: { editor: Editor }) {
|
||||||
return {
|
return {
|
||||||
priority: 160,
|
priority: 160,
|
||||||
|
@ -265,7 +339,7 @@ export default CodeBlockLowlight.extend<
|
||||||
return {
|
return {
|
||||||
pluginKey: "codeBlockBubbleMenu",
|
pluginKey: "codeBlockBubbleMenu",
|
||||||
shouldShow: ({ state }: { state: EditorState }) => {
|
shouldShow: ({ state }: { state: EditorState }) => {
|
||||||
return isActive(state, CodeBlockLowlight.name);
|
return isActive(state, TiptapCodeBlock.name);
|
||||||
},
|
},
|
||||||
getRenderContainer: (node: HTMLElement) => {
|
getRenderContainer: (node: HTMLElement) => {
|
||||||
return getRenderContainer(node);
|
return getRenderContainer(node);
|
||||||
|
@ -277,7 +351,7 @@ export default CodeBlockLowlight.extend<
|
||||||
icon: markRaw(MdiDeleteForeverOutline),
|
icon: markRaw(MdiDeleteForeverOutline),
|
||||||
title: i18n.global.t("editor.common.button.delete"),
|
title: i18n.global.t("editor.common.button.delete"),
|
||||||
action: ({ editor }: { editor: Editor }) =>
|
action: ({ editor }: { editor: Editor }) =>
|
||||||
deleteNode(CodeBlockLowlight.name, editor),
|
deleteNode(TiptapCodeBlock.name, editor),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
export { default as ExtensionCodeBlock } from "./code-block";
|
export { default as ExtensionCodeBlock } from "./code-block";
|
||||||
export { default as lowlight } from "./lowlight";
|
export * from "./code-block";
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
import dart from "highlight.js/lib/languages/dart";
|
|
||||||
import xml from "highlight.js/lib/languages/xml";
|
|
||||||
import { common, createLowlight } from "lowlight";
|
|
||||||
|
|
||||||
const lowlight = createLowlight(common);
|
|
||||||
lowlight.register("html", xml);
|
|
||||||
lowlight.register("dart", dart);
|
|
||||||
export default lowlight;
|
|
|
@ -29,7 +29,10 @@ import ExtensionUnderline from "./underline";
|
||||||
|
|
||||||
// Custom extensions
|
// Custom extensions
|
||||||
import ExtensionTextStyle from "@/extensions/text-style";
|
import ExtensionTextStyle from "@/extensions/text-style";
|
||||||
import { ExtensionCodeBlock, lowlight } from "@/extensions/code-block";
|
import {
|
||||||
|
ExtensionCodeBlock,
|
||||||
|
type ExtensionCodeBlockOptions,
|
||||||
|
} from "@/extensions/code-block";
|
||||||
import { ExtensionCommands } from "../extensions/commands-menu";
|
import { ExtensionCommands } from "../extensions/commands-menu";
|
||||||
import ExtensionAudio from "./audio";
|
import ExtensionAudio from "./audio";
|
||||||
import ExtensionClearFormat from "./clear-format";
|
import ExtensionClearFormat from "./clear-format";
|
||||||
|
@ -67,7 +70,6 @@ const allExtensions = [
|
||||||
ExtensionOrderedList,
|
ExtensionOrderedList,
|
||||||
ExtensionStrike,
|
ExtensionStrike,
|
||||||
ExtensionText,
|
ExtensionText,
|
||||||
ExtensionTextStyle,
|
|
||||||
ExtensionImage,
|
ExtensionImage,
|
||||||
ExtensionTaskList,
|
ExtensionTaskList,
|
||||||
ExtensionHighlight,
|
ExtensionHighlight,
|
||||||
|
@ -92,9 +94,7 @@ const allExtensions = [
|
||||||
ExtensionCommands.configure({
|
ExtensionCommands.configure({
|
||||||
suggestion: {},
|
suggestion: {},
|
||||||
}),
|
}),
|
||||||
ExtensionCodeBlock.configure({
|
ExtensionCodeBlock,
|
||||||
lowlight,
|
|
||||||
}),
|
|
||||||
ExtensionIframe,
|
ExtensionIframe,
|
||||||
ExtensionVideo,
|
ExtensionVideo,
|
||||||
ExtensionAudio,
|
ExtensionAudio,
|
||||||
|
@ -157,5 +157,6 @@ export {
|
||||||
ExtensionVideo,
|
ExtensionVideo,
|
||||||
RangeSelection,
|
RangeSelection,
|
||||||
allExtensions,
|
allExtensions,
|
||||||
lowlight,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type { ExtensionCodeBlockOptions };
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import "floating-vue/dist/style.css";
|
import "floating-vue/dist/style.css";
|
||||||
import "github-markdown-css/github-markdown-light.css";
|
import "github-markdown-css/github-markdown-light.css";
|
||||||
import "highlight.js/styles/github.css";
|
|
||||||
import type { App, Plugin } from "vue";
|
import type { App, Plugin } from "vue";
|
||||||
import { RichTextEditor } from "./components";
|
import { RichTextEditor } from "./components";
|
||||||
import "./styles/index.scss";
|
import "./styles/index.scss";
|
||||||
|
|
|
@ -83,6 +83,10 @@
|
||||||
display: initial;
|
display: initial;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.v-popper__arrow-container {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.v-popper--theme-tooltip {
|
.v-popper--theme-tooltip {
|
||||||
|
|
|
@ -470,9 +470,6 @@ importers:
|
||||||
'@tiptap/extension-code-block':
|
'@tiptap/extension-code-block':
|
||||||
specifier: ^2.6.5
|
specifier: ^2.6.5
|
||||||
version: 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)
|
version: 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)
|
||||||
'@tiptap/extension-code-block-lowlight':
|
|
||||||
specifier: ^2.6.5
|
|
||||||
version: 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/extension-code-block@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)(highlight.js@11.8.0)(lowlight@3.0.0)
|
|
||||||
'@tiptap/extension-color':
|
'@tiptap/extension-color':
|
||||||
specifier: ^2.6.5
|
specifier: ^2.6.5
|
||||||
version: 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/extension-text-style@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5)))
|
version: 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/extension-text-style@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5)))
|
||||||
|
@ -569,15 +566,9 @@ importers:
|
||||||
github-markdown-css:
|
github-markdown-css:
|
||||||
specifier: ^5.2.0
|
specifier: ^5.2.0
|
||||||
version: 5.2.0
|
version: 5.2.0
|
||||||
highlight.js:
|
|
||||||
specifier: 11.8.0
|
|
||||||
version: 11.8.0
|
|
||||||
linkifyjs:
|
linkifyjs:
|
||||||
specifier: ^4.1.3
|
specifier: ^4.1.3
|
||||||
version: 4.1.3
|
version: 4.1.3
|
||||||
lowlight:
|
|
||||||
specifier: ^3.0.0
|
|
||||||
version: 3.0.0
|
|
||||||
scroll-into-view-if-needed:
|
scroll-into-view-if-needed:
|
||||||
specifier: ^3.1.0
|
specifier: ^3.1.0
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
|
@ -3603,15 +3594,6 @@ packages:
|
||||||
'@tiptap/core': ^2.6.5
|
'@tiptap/core': ^2.6.5
|
||||||
'@tiptap/pm': ^2.6.5
|
'@tiptap/pm': ^2.6.5
|
||||||
|
|
||||||
'@tiptap/extension-code-block-lowlight@2.6.5':
|
|
||||||
resolution: {integrity: sha512-zb8zQuOBhsVJ3UWTHh1xvPWnpNFA5etuD7rQdzLHXGtowz9DUOqxdMmVMnBcwXE5g5MKQQBJJwqqDw0jGJn/jw==}
|
|
||||||
peerDependencies:
|
|
||||||
'@tiptap/core': ^2.6.5
|
|
||||||
'@tiptap/extension-code-block': ^2.6.5
|
|
||||||
'@tiptap/pm': ^2.6.5
|
|
||||||
highlight.js: ^11
|
|
||||||
lowlight: ^2 || ^3
|
|
||||||
|
|
||||||
'@tiptap/extension-code-block@2.6.5':
|
'@tiptap/extension-code-block@2.6.5':
|
||||||
resolution: {integrity: sha512-kV8VAerd3z23zv6vZSVkq0JJs0emzWb3KyrLsiUuhR1Yj+zgcxer3zw4IJlmDeDhp6qIXK/qTgHzNcxS+fV4Rw==}
|
resolution: {integrity: sha512-kV8VAerd3z23zv6vZSVkq0JJs0emzWb3KyrLsiUuhR1Yj+zgcxer3zw4IJlmDeDhp6qIXK/qTgHzNcxS+fV4Rw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -3873,9 +3855,6 @@ packages:
|
||||||
'@types/graceful-fs@4.1.9':
|
'@types/graceful-fs@4.1.9':
|
||||||
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
resolution: {integrity: sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==}
|
||||||
|
|
||||||
'@types/hast@3.0.1':
|
|
||||||
resolution: {integrity: sha512-hs/iBJx2aydugBQx5ETV3ZgeSS0oIreQrFJ4bjBl0XvM4wAmDjFEALY7p0rTSLt2eL+ibjRAAs9dTPiCLtmbqQ==}
|
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4':
|
'@types/http-cache-semantics@4.0.4':
|
||||||
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
resolution: {integrity: sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==}
|
||||||
|
|
||||||
|
@ -4011,9 +3990,6 @@ packages:
|
||||||
'@types/unist@2.0.10':
|
'@types/unist@2.0.10':
|
||||||
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
resolution: {integrity: sha512-IfYcSBWE3hLpBg8+X2SEa8LVkJdJEkT2Ese2aaLs3ptGdVtABxndrMaxuFlQ1qdFf9Q5rDvDpxI3WwgvKFAsQA==}
|
||||||
|
|
||||||
'@types/unist@3.0.0':
|
|
||||||
resolution: {integrity: sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w==}
|
|
||||||
|
|
||||||
'@types/uuid@9.0.7':
|
'@types/uuid@9.0.7':
|
||||||
resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
|
resolution: {integrity: sha512-WUtIVRUZ9i5dYXefDEAI7sh9/O7jGvHg7Df/5O/gtH3Yabe5odI3UWopVR1qbPXQtvOxWu3mM4XxlYeZtMWF4g==}
|
||||||
|
|
||||||
|
@ -5582,9 +5558,6 @@ packages:
|
||||||
resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==}
|
resolution: {integrity: sha512-aBzdj76lueB6uUst5iAs7+0H/oOjqI5D16XUWxlWMIMROhcM0rfsNVk93zTngq1dDNpoXRr++Sus7ETAExppAQ==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
devlop@1.1.0:
|
|
||||||
resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==}
|
|
||||||
|
|
||||||
didyoumean@1.2.2:
|
didyoumean@1.2.2:
|
||||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||||
|
|
||||||
|
@ -6613,10 +6586,6 @@ packages:
|
||||||
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
resolution: {integrity: sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
|
|
||||||
highlight.js@11.8.0:
|
|
||||||
resolution: {integrity: sha512-MedQhoqVdr0U6SSnWPzfiadUcDHfN/Wzq25AkXiQv9oiOO/sG0S7XkvpFIqWBl9Yq1UYyYOOVORs5UW2XlPyzg==}
|
|
||||||
engines: {node: '>=12.0.0'}
|
|
||||||
|
|
||||||
hookable@5.4.2:
|
hookable@5.4.2:
|
||||||
resolution: {integrity: sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg==}
|
resolution: {integrity: sha512-6rOvaUiNKy9lET1X0ECnyZ5O5kSV0PJbtA5yZUgdEF7fGJEVwSLSislltyt7nFwVVALYHQJtfGeAR2Y0A0uJkg==}
|
||||||
|
|
||||||
|
@ -7488,9 +7457,6 @@ packages:
|
||||||
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
resolution: {integrity: sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==}
|
||||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||||
|
|
||||||
lowlight@3.0.0:
|
|
||||||
resolution: {integrity: sha512-kedX6yxvgak8P4LGh3vKRDQuMbVcnP+qRuDJlve2w+mNJAbEhEQPjYCp9QJnpVL5F2aAAVjeIzzrbQZUKHiDJw==}
|
|
||||||
|
|
||||||
lru-cache@10.1.0:
|
lru-cache@10.1.0:
|
||||||
resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
|
resolution: {integrity: sha512-/1clY/ui8CzjKFyjdvwPWJUYKiFVXG2I2cY0ssG7h4+hwk+XOIX7ZSG9Q7TW8TW3Kp3BUSqgFWBLgL4PJ+Blag==}
|
||||||
engines: {node: 14 || >=16.14}
|
engines: {node: 14 || >=16.14}
|
||||||
|
@ -14601,14 +14567,6 @@ snapshots:
|
||||||
'@tiptap/core': 2.6.5(@tiptap/pm@2.6.5)
|
'@tiptap/core': 2.6.5(@tiptap/pm@2.6.5)
|
||||||
'@tiptap/pm': 2.6.5
|
'@tiptap/pm': 2.6.5
|
||||||
|
|
||||||
'@tiptap/extension-code-block-lowlight@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/extension-code-block@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)(highlight.js@11.8.0)(lowlight@3.0.0)':
|
|
||||||
dependencies:
|
|
||||||
'@tiptap/core': 2.6.5(@tiptap/pm@2.6.5)
|
|
||||||
'@tiptap/extension-code-block': 2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)
|
|
||||||
'@tiptap/pm': 2.6.5
|
|
||||||
highlight.js: 11.8.0
|
|
||||||
lowlight: 3.0.0
|
|
||||||
|
|
||||||
'@tiptap/extension-code-block@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)':
|
'@tiptap/extension-code-block@2.6.5(@tiptap/core@2.6.5(@tiptap/pm@2.6.5))(@tiptap/pm@2.6.5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@tiptap/core': 2.6.5(@tiptap/pm@2.6.5)
|
'@tiptap/core': 2.6.5(@tiptap/pm@2.6.5)
|
||||||
|
@ -14871,10 +14829,6 @@ snapshots:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 18.19.34
|
'@types/node': 18.19.34
|
||||||
|
|
||||||
'@types/hast@3.0.1':
|
|
||||||
dependencies:
|
|
||||||
'@types/unist': 3.0.0
|
|
||||||
|
|
||||||
'@types/http-cache-semantics@4.0.4': {}
|
'@types/http-cache-semantics@4.0.4': {}
|
||||||
|
|
||||||
'@types/http-errors@2.0.4': {}
|
'@types/http-errors@2.0.4': {}
|
||||||
|
@ -14999,8 +14953,6 @@ snapshots:
|
||||||
|
|
||||||
'@types/unist@2.0.10': {}
|
'@types/unist@2.0.10': {}
|
||||||
|
|
||||||
'@types/unist@3.0.0': {}
|
|
||||||
|
|
||||||
'@types/uuid@9.0.7': {}
|
'@types/uuid@9.0.7': {}
|
||||||
|
|
||||||
'@types/web-bluetooth@0.0.20': {}
|
'@types/web-bluetooth@0.0.20': {}
|
||||||
|
@ -16838,10 +16790,6 @@ snapshots:
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
devlop@1.1.0:
|
|
||||||
dependencies:
|
|
||||||
dequal: 2.0.3
|
|
||||||
|
|
||||||
didyoumean@1.2.2: {}
|
didyoumean@1.2.2: {}
|
||||||
|
|
||||||
diff-sequences@29.4.3: {}
|
diff-sequences@29.4.3: {}
|
||||||
|
@ -18144,8 +18092,6 @@ snapshots:
|
||||||
|
|
||||||
he@1.2.0: {}
|
he@1.2.0: {}
|
||||||
|
|
||||||
highlight.js@11.8.0: {}
|
|
||||||
|
|
||||||
hookable@5.4.2: {}
|
hookable@5.4.2: {}
|
||||||
|
|
||||||
hookable@5.5.3: {}
|
hookable@5.5.3: {}
|
||||||
|
@ -19070,12 +19016,6 @@ snapshots:
|
||||||
|
|
||||||
lowercase-keys@3.0.0: {}
|
lowercase-keys@3.0.0: {}
|
||||||
|
|
||||||
lowlight@3.0.0:
|
|
||||||
dependencies:
|
|
||||||
'@types/hast': 3.0.1
|
|
||||||
devlop: 1.1.0
|
|
||||||
highlight.js: 11.8.0
|
|
||||||
|
|
||||||
lru-cache@10.1.0: {}
|
lru-cache@10.1.0: {}
|
||||||
|
|
||||||
lru-cache@4.1.5:
|
lru-cache@4.1.5:
|
||||||
|
|
|
@ -48,8 +48,7 @@ import {
|
||||||
RichTextEditor,
|
RichTextEditor,
|
||||||
ToolbarItem,
|
ToolbarItem,
|
||||||
ToolboxItem,
|
ToolboxItem,
|
||||||
lowlight,
|
type Extensions,
|
||||||
type AnyExtension,
|
|
||||||
} from "@halo-dev/richtext-editor";
|
} from "@halo-dev/richtext-editor";
|
||||||
// ui custom extension
|
// ui custom extension
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
|
@ -98,6 +97,7 @@ import {
|
||||||
UiExtensionVideo,
|
UiExtensionVideo,
|
||||||
} from "./extensions";
|
} from "./extensions";
|
||||||
import { getContents } from "./utils/attachment";
|
import { getContents } from "./utils/attachment";
|
||||||
|
import { useExtension } from "./composables/use-extension";
|
||||||
|
|
||||||
const { t } = useI18n();
|
const { t } = useI18n();
|
||||||
const { currentUserHasPermission } = usePermission();
|
const { currentUserHasPermission } = usePermission();
|
||||||
|
@ -190,33 +190,9 @@ const handleCloseAttachmentSelectorModal = () => {
|
||||||
attachmentOptions.value = initAttachmentOptions;
|
attachmentOptions.value = initAttachmentOptions;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(async () => {
|
const { filterDuplicateExtensions } = useExtension();
|
||||||
const extensionsFromPlugins: AnyExtension[] = [];
|
|
||||||
|
|
||||||
for (const pluginModule of pluginModules) {
|
const presetExtensions = [
|
||||||
const callbackFunction =
|
|
||||||
pluginModule?.extensionPoints?.["default:editor:extension:create"];
|
|
||||||
|
|
||||||
if (typeof callbackFunction !== "function") {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const extensions = await callbackFunction();
|
|
||||||
|
|
||||||
extensionsFromPlugins.push(...extensions);
|
|
||||||
}
|
|
||||||
|
|
||||||
// debounce OnUpdate
|
|
||||||
const debounceOnUpdate = useDebounceFn(() => {
|
|
||||||
const html = editor.value?.getHTML() + "";
|
|
||||||
emit("update:raw", html);
|
|
||||||
emit("update:content", html);
|
|
||||||
emit("update", html);
|
|
||||||
}, 250);
|
|
||||||
|
|
||||||
editor.value = new Editor({
|
|
||||||
content: props.raw,
|
|
||||||
extensions: [
|
|
||||||
ExtensionBlockquote,
|
ExtensionBlockquote,
|
||||||
ExtensionBold,
|
ExtensionBold,
|
||||||
ExtensionBulletList,
|
ExtensionBulletList,
|
||||||
|
@ -265,9 +241,7 @@ onMounted(async () => {
|
||||||
}),
|
}),
|
||||||
ExtensionHighlight,
|
ExtensionHighlight,
|
||||||
ExtensionCommands,
|
ExtensionCommands,
|
||||||
ExtensionCodeBlock.configure({
|
ExtensionCodeBlock,
|
||||||
lowlight,
|
|
||||||
}),
|
|
||||||
ExtensionIframe,
|
ExtensionIframe,
|
||||||
UiExtensionVideo.configure({
|
UiExtensionVideo.configure({
|
||||||
uploadVideo: props.uploadImage,
|
uploadVideo: props.uploadImage,
|
||||||
|
@ -279,7 +253,6 @@ onMounted(async () => {
|
||||||
ExtensionFontSize,
|
ExtensionFontSize,
|
||||||
ExtensionColor,
|
ExtensionColor,
|
||||||
ExtensionIndent,
|
ExtensionIndent,
|
||||||
...extensionsFromPlugins,
|
|
||||||
Extension.create({
|
Extension.create({
|
||||||
addGlobalAttributes() {
|
addGlobalAttributes() {
|
||||||
return [
|
return [
|
||||||
|
@ -403,7 +376,40 @@ onMounted(async () => {
|
||||||
ExtensionClearFormat,
|
ExtensionClearFormat,
|
||||||
ExtensionFormatBrush,
|
ExtensionFormatBrush,
|
||||||
ExtensionRangeSelection,
|
ExtensionRangeSelection,
|
||||||
],
|
];
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const extensionsFromPlugins: Extensions = [];
|
||||||
|
|
||||||
|
for (const pluginModule of pluginModules) {
|
||||||
|
const callbackFunction =
|
||||||
|
pluginModule?.extensionPoints?.["default:editor:extension:create"];
|
||||||
|
|
||||||
|
if (typeof callbackFunction !== "function") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extensions = await callbackFunction();
|
||||||
|
|
||||||
|
extensionsFromPlugins.push(...extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// debounce OnUpdate
|
||||||
|
const debounceOnUpdate = useDebounceFn(() => {
|
||||||
|
const html = editor.value?.getHTML() + "";
|
||||||
|
emit("update:raw", html);
|
||||||
|
emit("update:content", html);
|
||||||
|
emit("update", html);
|
||||||
|
}, 250);
|
||||||
|
|
||||||
|
const extensions = filterDuplicateExtensions([
|
||||||
|
...presetExtensions,
|
||||||
|
...extensionsFromPlugins,
|
||||||
|
]);
|
||||||
|
|
||||||
|
editor.value = new Editor({
|
||||||
|
content: props.raw,
|
||||||
|
extensions,
|
||||||
parseOptions: {
|
parseOptions: {
|
||||||
preserveWhitespace: true,
|
preserveWhitespace: true,
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,94 @@
|
||||||
|
import {
|
||||||
|
getExtensionField,
|
||||||
|
type AnyConfig,
|
||||||
|
type AnyExtension,
|
||||||
|
type Extensions,
|
||||||
|
} from "@halo-dev/richtext-editor";
|
||||||
|
|
||||||
|
export function useExtension() {
|
||||||
|
const filterDuplicateExtensions = (extensions: Extensions | undefined) => {
|
||||||
|
if (!extensions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedExtensions = sort(flatten(extensions));
|
||||||
|
|
||||||
|
const map = new Map<string, AnyExtension>();
|
||||||
|
|
||||||
|
resolvedExtensions.forEach((extension) => {
|
||||||
|
const key = `${extension.type}-${extension.name}`;
|
||||||
|
if (map.has(key)) {
|
||||||
|
console.warn(
|
||||||
|
`Duplicate found for Extension, type: ${extension.type}, name: ${extension.name}. Keeping the later one.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
map.set(key, extension);
|
||||||
|
});
|
||||||
|
|
||||||
|
return Array.from(map.values());
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a flattened array of extensions by traversing the `addExtensions` field.
|
||||||
|
* @param extensions An array of Tiptap extensions
|
||||||
|
* @returns A flattened array of Tiptap extensions
|
||||||
|
*/
|
||||||
|
const flatten = (extensions: Extensions): Extensions => {
|
||||||
|
return (
|
||||||
|
extensions
|
||||||
|
.map((extension) => {
|
||||||
|
const context = {
|
||||||
|
name: extension.name,
|
||||||
|
options: extension.options,
|
||||||
|
storage: extension.storage,
|
||||||
|
};
|
||||||
|
|
||||||
|
const addExtensions = getExtensionField<AnyConfig["addExtensions"]>(
|
||||||
|
extension,
|
||||||
|
"addExtensions",
|
||||||
|
context
|
||||||
|
);
|
||||||
|
|
||||||
|
if (addExtensions) {
|
||||||
|
return [extension, ...flatten(addExtensions())];
|
||||||
|
}
|
||||||
|
|
||||||
|
return extension;
|
||||||
|
})
|
||||||
|
// `Infinity` will break TypeScript so we set a number that is probably high enough
|
||||||
|
.flat(10)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort extensions by priority.
|
||||||
|
* @param extensions An array of Tiptap extensions
|
||||||
|
* @returns A sorted array of Tiptap extensions by priority
|
||||||
|
*/
|
||||||
|
const sort = (extensions: Extensions): Extensions => {
|
||||||
|
const defaultPriority = 100;
|
||||||
|
|
||||||
|
return extensions.sort((a, b) => {
|
||||||
|
const priorityA =
|
||||||
|
getExtensionField<AnyConfig["priority"]>(a, "priority") ||
|
||||||
|
defaultPriority;
|
||||||
|
const priorityB =
|
||||||
|
getExtensionField<AnyConfig["priority"]>(b, "priority") ||
|
||||||
|
defaultPriority;
|
||||||
|
|
||||||
|
if (priorityA > priorityB) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (priorityA < priorityB) {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
filterDuplicateExtensions,
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue