mirror of
https://github.com/docmost/docmost.git
synced 2025-11-13 09:12:36 +10:00
Editor link components
* other minor fixes
This commit is contained in:
@ -61,7 +61,7 @@ export default function App() {
|
||||
<Routes>
|
||||
<Route index element={<Navigate to="/home" />} />
|
||||
<Route path={"/login"} element={<LoginPage />} />
|
||||
<Route path={"/installation/setup"} element={<SetupWorkspace />} />
|
||||
<Route path={"/setup/register"} element={<SetupWorkspace />} />
|
||||
<Route path={"/invites/:invitationId"} element={<InviteSignup />} />
|
||||
|
||||
<Route path={"/p/:pageSlug"} element={<PageRedirect />} />
|
||||
|
||||
@ -8,21 +8,4 @@
|
||||
.active {
|
||||
color: light-dark(var(--mantine-color-blue-8), var(--mantine-color-gray-5));
|
||||
}
|
||||
|
||||
.colorButton {
|
||||
border: none;
|
||||
}
|
||||
|
||||
.colorButton::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 1px;
|
||||
background-color: light-dark(
|
||||
var(--mantine-color-gray-3),
|
||||
var(--mantine-color-gray-8)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,7 +4,7 @@ import {
|
||||
isNodeSelection,
|
||||
useEditor,
|
||||
} from "@tiptap/react";
|
||||
import { FC, useEffect, useRef, useState } from "react";
|
||||
import { FC, memo, useEffect, useRef, useState } from "react";
|
||||
import {
|
||||
IconBold,
|
||||
IconCode,
|
||||
@ -25,6 +25,7 @@ import {
|
||||
import { useAtom } from "jotai";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { isCellSelection } from "@docmost/editor-ext";
|
||||
import { LinkSelector } from "@/features/editor/components/bubble-menu/link-selector.tsx";
|
||||
|
||||
export interface BubbleMenuItem {
|
||||
name: string;
|
||||
@ -113,7 +114,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
},
|
||||
tippyOptions: {
|
||||
moveTransition: "transform 0.15s ease-out",
|
||||
onHidden: () => {
|
||||
onHide: () => {
|
||||
setIsNodeSelectorOpen(false);
|
||||
setIsColorSelectorOpen(false);
|
||||
setIsLinkSelectorOpen(false);
|
||||
@ -155,6 +156,14 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
))}
|
||||
</ActionIcon.Group>
|
||||
|
||||
<LinkSelector
|
||||
editor={props.editor}
|
||||
isOpen={isLinkSelectorOpen}
|
||||
setIsOpen={() => {
|
||||
setIsLinkSelectorOpen(!isLinkSelectorOpen);
|
||||
}}
|
||||
/>
|
||||
|
||||
<ColorSelector
|
||||
editor={props.editor}
|
||||
isOpen={isColorSelectorOpen}
|
||||
@ -171,7 +180,7 @@ export const EditorBubbleMenu: FC<EditorBubbleMenuProps> = (props) => {
|
||||
style={{ border: "none" }}
|
||||
onClick={commentItem.command}
|
||||
>
|
||||
<IconMessage style={{ width: rem(16) }} stroke={2} />
|
||||
<IconMessage size={16} stroke={2} />
|
||||
</ActionIcon>
|
||||
</div>
|
||||
</BubbleMenu>
|
||||
|
||||
@ -1,7 +1,14 @@
|
||||
import { Dispatch, FC, SetStateAction } from "react";
|
||||
import { IconCheck, IconChevronDown } from "@tabler/icons-react";
|
||||
import { Button, Popover, rem, ScrollArea, Text, Tooltip } from "@mantine/core";
|
||||
import classes from "./bubble-menu.module.css";
|
||||
import { IconCheck, IconPalette } from "@tabler/icons-react";
|
||||
import {
|
||||
ActionIcon,
|
||||
Button,
|
||||
Popover,
|
||||
rem,
|
||||
ScrollArea,
|
||||
Text,
|
||||
Tooltip,
|
||||
} from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
|
||||
export interface BubbleColorMenuItem {
|
||||
@ -110,21 +117,19 @@ export const ColorSelector: FC<ColorSelectorProps> = ({
|
||||
return (
|
||||
<Popover width={200} opened={isOpen} withArrow>
|
||||
<Popover.Target>
|
||||
<Tooltip label="text color" withArrow>
|
||||
<Button
|
||||
<Tooltip label="Text color" withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
className={classes.colorButton}
|
||||
style={{
|
||||
border: "none",
|
||||
color: activeColorItem?.color,
|
||||
paddingLeft: "8px",
|
||||
paddingRight: "8px",
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
A
|
||||
</Button>
|
||||
<IconPalette size={16} stroke={2} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
|
||||
|
||||
@ -0,0 +1,61 @@
|
||||
import { Dispatch, FC, SetStateAction, useCallback } from "react";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import { ActionIcon, Popover, Tooltip } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
|
||||
export interface BubbleColorMenuItem {
|
||||
name: string;
|
||||
color: string;
|
||||
}
|
||||
|
||||
interface LinkSelectorProps {
|
||||
editor: ReturnType<typeof useEditor>;
|
||||
isOpen: boolean;
|
||||
setIsOpen: Dispatch<SetStateAction<boolean>>;
|
||||
}
|
||||
|
||||
export const LinkSelector: FC<LinkSelectorProps> = ({
|
||||
editor,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}) => {
|
||||
const onLink = useCallback(
|
||||
(url: string) => {
|
||||
editor.chain().focus().setLink({ href: url }).run();
|
||||
setIsOpen(false);
|
||||
console.log("is p[e ");
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
return (
|
||||
<Popover
|
||||
width={300}
|
||||
opened={isOpen}
|
||||
trapFocus
|
||||
offset={{ mainAxis: 35, crossAxis: 0 }}
|
||||
withArrow
|
||||
>
|
||||
<Popover.Target>
|
||||
<Tooltip label="Add link" withArrow>
|
||||
<ActionIcon
|
||||
variant="default"
|
||||
size="lg"
|
||||
radius="0"
|
||||
style={{
|
||||
border: "none",
|
||||
}}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
<IconLink size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Popover.Target>
|
||||
|
||||
<Popover.Dropdown>
|
||||
<LinkEditorPanel onSetLink={onLink} />
|
||||
</Popover.Dropdown>
|
||||
</Popover>
|
||||
);
|
||||
};
|
||||
@ -12,8 +12,7 @@ import {
|
||||
IconListNumbers,
|
||||
IconTypography,
|
||||
} from "@tabler/icons-react";
|
||||
import { Popover, Button, rem, ScrollArea } from "@mantine/core";
|
||||
import classes from "@/features/editor/components/bubble-menu/bubble-menu.module.css";
|
||||
import { Popover, Button, ScrollArea } from "@mantine/core";
|
||||
import { useEditor } from "@tiptap/react";
|
||||
|
||||
interface NodeSelectorProps {
|
||||
@ -110,9 +109,9 @@ export const NodeSelector: FC<NodeSelectorProps> = ({
|
||||
<Popover.Target>
|
||||
<Button
|
||||
variant="default"
|
||||
style={{ border: "none", height: "34px" }}
|
||||
radius="0"
|
||||
rightSection={<IconChevronDown size={16} />}
|
||||
className={classes.colorButton}
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
>
|
||||
{activeItem?.name}
|
||||
|
||||
@ -26,7 +26,7 @@ export default function ImageView(props: NodeViewProps) {
|
||||
fit="contain"
|
||||
w={width}
|
||||
src={getFileUrl(src)}
|
||||
className={selected && "ProseMirror-selectednode"}
|
||||
className={selected ? "ProseMirror-selectednode" : ""}
|
||||
/>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
||||
@ -0,0 +1,34 @@
|
||||
import React from "react";
|
||||
import { Button, Group, TextInput } from "@mantine/core";
|
||||
import { IconLink } from "@tabler/icons-react";
|
||||
import { useLinkEditorState } from "@/features/editor/components/link/use-link-editor-state.tsx";
|
||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||
|
||||
export const LinkEditorPanel = ({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
}: LinkEditorPanelProps) => {
|
||||
const state = useLinkEditorState({
|
||||
onSetLink,
|
||||
initialUrl,
|
||||
});
|
||||
|
||||
return (
|
||||
<div>
|
||||
<form onSubmit={state.handleSubmit}>
|
||||
<Group gap="xs" style={{ flex: 1 }} wrap="nowrap">
|
||||
<TextInput
|
||||
leftSection={<IconLink size={16} />}
|
||||
variant="filled"
|
||||
placeholder="Paste link"
|
||||
value={state.url}
|
||||
onChange={state.onChange}
|
||||
/>
|
||||
<Button p={"xs"} type="submit" disabled={!state.isValidUrl}>
|
||||
Save
|
||||
</Button>
|
||||
</Group>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -0,0 +1,86 @@
|
||||
import { BubbleMenu as BaseBubbleMenu } from "@tiptap/react";
|
||||
import React, { useCallback, useState } from "react";
|
||||
import { EditorMenuProps } from "@/features/editor/components/table/types/types.ts";
|
||||
import { LinkEditorPanel } from "@/features/editor/components/link/link-editor-panel.tsx";
|
||||
import { LinkPreviewPanel } from "@/features/editor/components/link/link-preview.tsx";
|
||||
import { Card } from "@mantine/core";
|
||||
|
||||
export function LinkMenu({ editor, appendTo }: EditorMenuProps) {
|
||||
const [showEdit, setShowEdit] = useState(false);
|
||||
|
||||
const shouldShow = useCallback(() => {
|
||||
return editor.isActive("link");
|
||||
}, [editor]);
|
||||
|
||||
const { href: link } = editor.getAttributes("link");
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
setShowEdit(true);
|
||||
}, []);
|
||||
|
||||
const onSetLink = useCallback(
|
||||
(url: string) => {
|
||||
editor
|
||||
.chain()
|
||||
.focus()
|
||||
.extendMarkRange("link")
|
||||
.setLink({ href: url })
|
||||
.run();
|
||||
setShowEdit(false);
|
||||
},
|
||||
[editor],
|
||||
);
|
||||
|
||||
const onUnsetLink = useCallback(() => {
|
||||
editor.chain().focus().extendMarkRange("link").unsetLink().run();
|
||||
setShowEdit(false);
|
||||
return null;
|
||||
}, [editor]);
|
||||
|
||||
const onShowEdit = useCallback(() => {
|
||||
setShowEdit(true);
|
||||
}, []);
|
||||
|
||||
const onHideEdit = useCallback(() => {
|
||||
setShowEdit(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BaseBubbleMenu
|
||||
editor={editor}
|
||||
pluginKey={`link-menu}`}
|
||||
updateDelay={0}
|
||||
tippyOptions={{
|
||||
appendTo: () => {
|
||||
return appendTo?.current;
|
||||
},
|
||||
onHidden: () => {
|
||||
setShowEdit(false);
|
||||
},
|
||||
placement: "bottom",
|
||||
offset: [0, 5],
|
||||
zIndex: 101,
|
||||
}}
|
||||
shouldShow={shouldShow}
|
||||
>
|
||||
{showEdit ? (
|
||||
<Card
|
||||
withBorder
|
||||
radius="md"
|
||||
padding="xs"
|
||||
bg="var(--mantine-color-body)"
|
||||
>
|
||||
<LinkEditorPanel initialUrl={link} onSetLink={onSetLink} />
|
||||
</Card>
|
||||
) : (
|
||||
<LinkPreviewPanel
|
||||
url={link}
|
||||
onClear={onUnsetLink}
|
||||
onEdit={handleEdit}
|
||||
/>
|
||||
)}
|
||||
</BaseBubbleMenu>
|
||||
);
|
||||
}
|
||||
|
||||
export default LinkMenu;
|
||||
@ -0,0 +1,61 @@
|
||||
import {
|
||||
Tooltip,
|
||||
ActionIcon,
|
||||
Card,
|
||||
Divider,
|
||||
Anchor,
|
||||
Flex,
|
||||
} from "@mantine/core";
|
||||
import { IconLinkOff, IconPencil } from "@tabler/icons-react";
|
||||
|
||||
export type LinkPreviewPanelProps = {
|
||||
url: string;
|
||||
onEdit: () => void;
|
||||
onClear: () => void;
|
||||
};
|
||||
|
||||
export const LinkPreviewPanel = ({
|
||||
onClear,
|
||||
onEdit,
|
||||
url,
|
||||
}: LinkPreviewPanelProps) => {
|
||||
return (
|
||||
<>
|
||||
<Card withBorder radius="md" padding="xs" bg="var(--mantine-color-body)">
|
||||
<Flex align="center">
|
||||
<Tooltip label={url}>
|
||||
<Anchor
|
||||
href={url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
inherit
|
||||
style={{
|
||||
overflow: "hidden",
|
||||
textOverflow: "ellipsis",
|
||||
whiteSpace: "nowrap",
|
||||
}}
|
||||
>
|
||||
{url}
|
||||
</Anchor>
|
||||
</Tooltip>
|
||||
|
||||
<Flex align="center">
|
||||
<Divider mx={4} orientation="vertical" />
|
||||
|
||||
<Tooltip label="Edit link">
|
||||
<ActionIcon onClick={onEdit} variant="subtle" color="gray">
|
||||
<IconPencil size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip label="Remove link">
|
||||
<ActionIcon onClick={onClear} variant="subtle" color="red">
|
||||
<IconLinkOff size={16} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Card>
|
||||
</>
|
||||
);
|
||||
};
|
||||
4
apps/client/src/features/editor/components/link/types.ts
Normal file
4
apps/client/src/features/editor/components/link/types.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export type LinkEditorPanelProps = {
|
||||
initialUrl?: string;
|
||||
onSetLink: (url: string, openInNewTab?: boolean) => void;
|
||||
};
|
||||
@ -0,0 +1,33 @@
|
||||
import React, { useCallback, useMemo, useState } from "react";
|
||||
import { LinkEditorPanelProps } from "@/features/editor/components/link/types.ts";
|
||||
|
||||
export const useLinkEditorState = ({
|
||||
initialUrl,
|
||||
onSetLink,
|
||||
}: LinkEditorPanelProps) => {
|
||||
const [url, setUrl] = useState(initialUrl || "");
|
||||
|
||||
const onChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setUrl(event.target.value);
|
||||
}, []);
|
||||
|
||||
const isValidUrl = useMemo(() => /^(\S+):(\/\/)?\S+$/.test(url), [url]);
|
||||
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
if (isValidUrl) {
|
||||
onSetLink(url);
|
||||
}
|
||||
},
|
||||
[url, isValidUrl, onSetLink],
|
||||
);
|
||||
|
||||
return {
|
||||
url,
|
||||
setUrl,
|
||||
onChange,
|
||||
handleSubmit,
|
||||
isValidUrl,
|
||||
};
|
||||
};
|
||||
@ -30,7 +30,7 @@ export default function MathBlockView(props: NodeViewProps) {
|
||||
});
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
console.error(e.message);
|
||||
//console.error(e.message);
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -24,7 +24,7 @@ export default function MathInlineView(props: NodeViewProps) {
|
||||
katex.render(katexString, container);
|
||||
setError(null);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
//console.error(e);
|
||||
setError(e.message);
|
||||
}
|
||||
};
|
||||
|
||||
@ -25,7 +25,7 @@ export default function VideoView(props: NodeViewProps) {
|
||||
width={width}
|
||||
controls
|
||||
src={getFileUrl(src)}
|
||||
className={selected && "ProseMirror-selectednode"}
|
||||
className={selected ? "ProseMirror-selectednode" : ""}
|
||||
/>
|
||||
</NodeViewWrapper>
|
||||
);
|
||||
|
||||
@ -31,6 +31,8 @@ import {
|
||||
TiptapImage,
|
||||
Callout,
|
||||
TiptapVideo,
|
||||
LinkExtension,
|
||||
Selection,
|
||||
} from "@docmost/editor-ext";
|
||||
import {
|
||||
randomElement,
|
||||
@ -57,7 +59,16 @@ export const mainExtensions = [
|
||||
codeBlock: false,
|
||||
}),
|
||||
Placeholder.configure({
|
||||
placeholder: 'Enter "/" for commands',
|
||||
placeholder: ({ node }) => {
|
||||
if (node.type.name === "heading") {
|
||||
return `Heading ${node.attrs.level}`;
|
||||
}
|
||||
if (node.type.name === "detailsSummary") {
|
||||
return "Toggle title";
|
||||
}
|
||||
return 'Write anything. Enter "/" for commands';
|
||||
},
|
||||
includeChildren: true,
|
||||
}),
|
||||
TextAlign.configure({ types: ["heading", "paragraph"] }),
|
||||
TaskList,
|
||||
@ -65,7 +76,9 @@ export const mainExtensions = [
|
||||
nested: true,
|
||||
}),
|
||||
Underline,
|
||||
Link,
|
||||
LinkExtension.configure({
|
||||
openOnClick: false,
|
||||
}),
|
||||
Superscript,
|
||||
SubScript,
|
||||
Highlight.configure({
|
||||
@ -112,6 +125,7 @@ export const mainExtensions = [
|
||||
CodeBlockLowlight.configure({
|
||||
lowlight,
|
||||
}),
|
||||
Selection,
|
||||
] as any;
|
||||
|
||||
type CollabExtensions = (provider: HocuspocusProvider, user: IUser) => any[];
|
||||
|
||||
@ -38,6 +38,7 @@ import {
|
||||
handleFileDrop,
|
||||
handleFilePaste,
|
||||
} from "@/features/editor/components/common/file-upload-handler.tsx";
|
||||
import LinkMenu from "@/features/editor/components/link/link-menu.tsx";
|
||||
|
||||
interface PageEditorProps {
|
||||
pageId: string;
|
||||
@ -172,6 +173,7 @@ export default function PageEditor({ pageId, editable }: PageEditorProps) {
|
||||
<ImageMenu editor={editor} />
|
||||
<VideoMenu editor={editor} />
|
||||
<CalloutMenu editor={editor} />
|
||||
<LinkMenu editor={editor} appendTo={menuContainerRef} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
|
||||
@ -9,10 +9,10 @@
|
||||
);
|
||||
font-size: var(--mantine-font-size-md);
|
||||
line-height: var(--mantine-line-height-xl);
|
||||
font-weight: 400;
|
||||
font-weight: 415;
|
||||
width: 100%;
|
||||
|
||||
{
|
||||
> * + * {
|
||||
margin-top: 0.75em;
|
||||
}
|
||||
|
||||
@ -46,7 +46,7 @@
|
||||
|
||||
a {
|
||||
color: light-dark(#207af1, #587da9);
|
||||
font-weight: bold;
|
||||
/*font-weight: bold;*/
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
@ -86,11 +86,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
.react-renderer {
|
||||
&.node-callout {
|
||||
padding-top: var(--mantine-spacing-xs);
|
||||
padding-bottom: var(--mantine-spacing-xs);
|
||||
& > .react-renderer {
|
||||
margin-top: var(--mantine-spacing-xl);
|
||||
margin-bottom: var(--mantine-spacing-xl);
|
||||
|
||||
&:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
&.node-callout {
|
||||
div[style*="white-space: inherit;"] {
|
||||
> :first-child {
|
||||
margin: 0;
|
||||
@ -98,16 +106,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
||||
.selection {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.comment-mark {
|
||||
.selection,
|
||||
*::selection {
|
||||
background-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-gray-7));
|
||||
}
|
||||
|
||||
.comment-mark {
|
||||
background: rgba(255, 215, 0, 0.14);
|
||||
border-bottom: 2px solid rgb(166, 158, 12);
|
||||
}
|
||||
|
||||
.resize-cursor {
|
||||
cursor: ew-resize;
|
||||
cursor: col-resize;
|
||||
}
|
||||
}
|
||||
|
||||
.ProseMirror-icon {
|
||||
|
||||
@ -5,9 +5,6 @@
|
||||
}
|
||||
|
||||
.node-image, .node-video {
|
||||
margin-top: 8px;
|
||||
margin-bottom: 8px;
|
||||
|
||||
&.ProseMirror-selectednode {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
.ProseMirror p.is-editor-empty:first-child::before {
|
||||
.ProseMirror .is-editor-empty:first-child::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #adb5bd;
|
||||
@ -6,7 +6,7 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.ProseMirror h1.is-editor-empty:first-child::before {
|
||||
.ProseMirror .is-empty::before {
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
color: #adb5bd;
|
||||
@ -14,11 +14,7 @@
|
||||
height: 0;
|
||||
}
|
||||
|
||||
/* Placeholder (on every new line) */
|
||||
/*.ProseMirror p.is-empty::before {
|
||||
color: #adb5bd;
|
||||
content: attr(data-placeholder);
|
||||
float: left;
|
||||
height: 0;
|
||||
pointer-events: none;
|
||||
}*/
|
||||
.ProseMirror table .is-editor-empty:first-child::before,
|
||||
.ProseMirror table .is-empty::before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
@ -29,7 +29,7 @@ export function useGetGroupsQuery(
|
||||
|
||||
export function useGroupQuery(groupId: string): UseQueryResult<IGroup, Error> {
|
||||
return useQuery({
|
||||
queryKey: ["group", groupId],
|
||||
queryKey: ["groups", groupId],
|
||||
queryFn: () => getGroupById(groupId),
|
||||
enabled: !!groupId,
|
||||
});
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
.treeContainer {
|
||||
display: flex;
|
||||
height: 60vh;
|
||||
height: 68vh;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
@ -8,3 +8,5 @@ export * from "./lib/image";
|
||||
export * from "./lib/video";
|
||||
export * from "./lib/callout";
|
||||
export * from "./lib/media-utils";
|
||||
export * from "./lib/link";
|
||||
export * from "./lib/selection";
|
||||
|
||||
47
packages/editor-ext/src/lib/link.ts
Normal file
47
packages/editor-ext/src/lib/link.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import { mergeAttributes } from "@tiptap/core";
|
||||
import TiptapLink from "@tiptap/extension-link";
|
||||
import { Plugin } from "@tiptap/pm/state";
|
||||
import { EditorView } from "@tiptap/pm/view";
|
||||
|
||||
export const LinkExtension = TiptapLink.extend({
|
||||
inclusive: false,
|
||||
|
||||
parseHTML() {
|
||||
return [
|
||||
{
|
||||
tag: 'a[href]:not([data-type="button"]):not([href *= "javascript:" i])',
|
||||
},
|
||||
];
|
||||
},
|
||||
|
||||
renderHTML({ HTMLAttributes }) {
|
||||
return [
|
||||
"a",
|
||||
mergeAttributes(this.options.HTMLAttributes, HTMLAttributes, {
|
||||
class: "link",
|
||||
}),
|
||||
0,
|
||||
];
|
||||
},
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
|
||||
return [
|
||||
...(this.parent?.() || []),
|
||||
new Plugin({
|
||||
props: {
|
||||
handleKeyDown: (view: EditorView, event: KeyboardEvent) => {
|
||||
const { selection } = editor.state;
|
||||
|
||||
if (event.key === "Escape" && selection.empty !== true) {
|
||||
editor.commands.focus(selection.to, { scrollIntoView: false });
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
36
packages/editor-ext/src/lib/selection.ts
Normal file
36
packages/editor-ext/src/lib/selection.ts
Normal file
@ -0,0 +1,36 @@
|
||||
import { Extension } from "@tiptap/core";
|
||||
import { Plugin, PluginKey } from "@tiptap/pm/state";
|
||||
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
||||
|
||||
export const Selection = Extension.create({
|
||||
name: "selection",
|
||||
|
||||
addProseMirrorPlugins() {
|
||||
const { editor } = this;
|
||||
|
||||
return [
|
||||
new Plugin({
|
||||
key: new PluginKey("selection"),
|
||||
props: {
|
||||
decorations(state) {
|
||||
if (state.selection.empty) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (editor.isFocused === true) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return DecorationSet.create(state.doc, [
|
||||
Decoration.inline(state.selection.from, state.selection.to, {
|
||||
class: "selection",
|
||||
}),
|
||||
]);
|
||||
},
|
||||
},
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
export default Selection;
|
||||
Reference in New Issue
Block a user