mirror of
https://github.com/suitenumerique/docs
synced 2026-04-21 13:37:20 +00:00
🐛(frontend) fix position interlinking when lost focus
When switching between a interlinking search to a interlinking link, we could lose the position of the interlinking. The interlinking was added at the beginning of the document or where the cursor was. We refactorize the interlinking to be only one type of inline content, by doing so we do not lose the position of the interlinking because we don't remove the interlinking search to add the interlinking link, we just update the interlinking search to be a interlinking link.
This commit is contained in:
parent
c20e71e21d
commit
3cc9655574
14 changed files with 313 additions and 343 deletions
|
|
@ -730,7 +730,7 @@ test.describe('Doc Editor', () => {
|
|||
await page.getByText('Link a doc').first().click();
|
||||
|
||||
const input = page.locator(
|
||||
"span[data-inline-content-type='interlinkingSearchInline'] input",
|
||||
"span[data-inline-content-type='interlinkingLinkInline'] input",
|
||||
);
|
||||
const searchContainer = page.locator('.quick-search-container');
|
||||
|
||||
|
|
|
|||
|
|
@ -53,10 +53,7 @@ const AIMenu = BlockNoteAI?.AIMenu;
|
|||
const AIMenuController = BlockNoteAI?.AIMenuController;
|
||||
const useAI = BlockNoteAI?.useAI;
|
||||
const localesBNAI = BlockNoteAI?.localesAI || {};
|
||||
import {
|
||||
InterlinkingLinkInlineContent,
|
||||
InterlinkingSearchInlineContent,
|
||||
} from './custom-inline-content';
|
||||
import { InterlinkingLinkInlineContent } from './custom-inline-content';
|
||||
import XLMultiColumn from './xl-multi-column';
|
||||
|
||||
const localesBNMultiColumn = XLMultiColumn?.locales;
|
||||
|
|
@ -74,7 +71,6 @@ const baseBlockNoteSchema = withPageBreak(
|
|||
},
|
||||
inlineContentSpecs: {
|
||||
...defaultInlineContentSpecs,
|
||||
interlinkingSearchInline: InterlinkingSearchInlineContent,
|
||||
interlinkingLinkInline: InterlinkingLinkInlineContent,
|
||||
},
|
||||
}),
|
||||
|
|
|
|||
|
|
@ -1,25 +1,57 @@
|
|||
import {
|
||||
PartialCustomInlineContentFromConfig,
|
||||
StyleSchema,
|
||||
} from '@blocknote/core';
|
||||
import { StyleSchema } from '@blocknote/core';
|
||||
import { createReactInlineContentSpec } from '@blocknote/react';
|
||||
import * as Sentry from '@sentry/nextjs';
|
||||
import { useRouter } from 'next/router';
|
||||
import { TFunction } from 'i18next';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
|
||||
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
|
||||
import { DocsBlockNoteEditor } from '@/docs/doc-editor';
|
||||
import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg';
|
||||
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
|
||||
import { useCreateChildDocTree, useDocStore } from '@/docs/doc-management';
|
||||
|
||||
export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
||||
import { LinkSelected } from './LinkSelected';
|
||||
import { SearchPage } from './SearchPage';
|
||||
|
||||
export type InterlinkingLinkInlineContentType = {
|
||||
type: 'interlinkingLinkInline';
|
||||
propSchema: {
|
||||
disabled?: {
|
||||
default: false;
|
||||
values: [true, false];
|
||||
};
|
||||
docId?: {
|
||||
default: '';
|
||||
};
|
||||
trigger?: {
|
||||
default: '/';
|
||||
values: readonly ['/', '@'];
|
||||
};
|
||||
title?: {
|
||||
default: '';
|
||||
};
|
||||
};
|
||||
content: 'none';
|
||||
};
|
||||
|
||||
export const InterlinkingLinkInlineContent = createReactInlineContentSpec<
|
||||
InterlinkingLinkInlineContentType,
|
||||
StyleSchema
|
||||
>(
|
||||
{
|
||||
type: 'interlinkingLinkInline',
|
||||
propSchema: {
|
||||
docId: {
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
values: [true, false],
|
||||
},
|
||||
trigger: {
|
||||
default: '/',
|
||||
values: ['/', '@'],
|
||||
},
|
||||
title: {
|
||||
default: '',
|
||||
},
|
||||
|
|
@ -27,189 +59,126 @@ export const InterlinkingLinkInlineContent = createReactInlineContentSpec(
|
|||
content: 'none',
|
||||
},
|
||||
{
|
||||
render: ({ editor, inlineContent, updateInlineContent }) => {
|
||||
if (!inlineContent.props.docId) {
|
||||
/**
|
||||
* Can have 3 render states:
|
||||
* 1. Disabled state: when the inline content is disabled, it renders nothing
|
||||
* 2. Search state: when the inline content has no docId, it renders the search page
|
||||
* 3. Linked state: when the inline content has a docId and title, it renders the linked doc
|
||||
*
|
||||
* Info: We keep everything in the same inline content to easily preserve
|
||||
* the element position when switching between states
|
||||
*/
|
||||
render: (props) => {
|
||||
const { disabled, docId, title } = props.inlineContent.props;
|
||||
|
||||
if (disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Should not happen
|
||||
*/
|
||||
if (!uuidValidate(inlineContent.props.docId)) {
|
||||
Sentry.captureException(
|
||||
new Error(`Invalid docId: ${inlineContent.props.docId}`),
|
||||
{
|
||||
extra: { info: 'InterlinkingLinkInlineContent' },
|
||||
},
|
||||
if (docId && title) {
|
||||
/**
|
||||
* Should not happen
|
||||
*/
|
||||
if (!uuidValidate(docId)) {
|
||||
return (
|
||||
<DisableInvalidInterlink
|
||||
docId={docId}
|
||||
onUpdateInlineContent={() => {
|
||||
props.updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
disabled: true,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkSelected
|
||||
docId={docId}
|
||||
title={title}
|
||||
isEditable={props.editor.isEditable}
|
||||
onUpdateTitle={(newTitle) =>
|
||||
props.updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId: docId,
|
||||
title: newTitle,
|
||||
trigger: props.inlineContent.props.trigger,
|
||||
disabled: false,
|
||||
},
|
||||
})
|
||||
}
|
||||
/>
|
||||
);
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId: '',
|
||||
title: '',
|
||||
},
|
||||
});
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<LinkSelected
|
||||
docId={inlineContent.props.docId}
|
||||
title={inlineContent.props.title}
|
||||
isEditable={editor.isEditable}
|
||||
updateInlineContent={updateInlineContent}
|
||||
/>
|
||||
);
|
||||
return <SearchPage {...props} />;
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
interface LinkSelectedProps {
|
||||
docId: string;
|
||||
title: string;
|
||||
isEditable: boolean;
|
||||
updateInlineContent: (
|
||||
update: PartialCustomInlineContentFromConfig<
|
||||
{
|
||||
readonly type: 'interlinkingLinkInline';
|
||||
readonly propSchema: {
|
||||
readonly docId: {
|
||||
readonly default: '';
|
||||
};
|
||||
readonly title: {
|
||||
readonly default: '';
|
||||
};
|
||||
};
|
||||
readonly content: 'none';
|
||||
},
|
||||
StyleSchema
|
||||
>,
|
||||
) => void;
|
||||
}
|
||||
export const LinkSelected = ({
|
||||
docId,
|
||||
title,
|
||||
isEditable,
|
||||
updateInlineContent,
|
||||
}: LinkSelectedProps) => {
|
||||
const { data: doc } = useDoc({ id: docId, withoutContent: true });
|
||||
|
||||
/**
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isEditable && doc?.title && doc.title !== title) {
|
||||
updateInlineContent({
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId,
|
||||
title: doc.title,
|
||||
export const getInterlinkinghMenuItems = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
createPage: () => void,
|
||||
) => [
|
||||
{
|
||||
key: 'link-doc',
|
||||
title: t('Link a doc'),
|
||||
onItemClick: () => {
|
||||
editor.insertInlineContent([
|
||||
{
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
trigger: '/',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
]);
|
||||
},
|
||||
aliases: ['interlinking', 'link', 'anchor', 'a'],
|
||||
group,
|
||||
icon: <LinkPageIcon />,
|
||||
subtext: t('Link this doc to another doc'),
|
||||
},
|
||||
{
|
||||
key: 'new-sub-doc',
|
||||
title: t('New sub-doc'),
|
||||
onItemClick: createPage,
|
||||
aliases: ['new sub-doc'],
|
||||
group,
|
||||
icon: <AddPageIcon />,
|
||||
subtext: t('Create a new sub-doc'),
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||
* causing an infinite loop of updates.
|
||||
* To prevent this, we only run this effect when doc?.title changes,
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title, docId, isEditable]);
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
|
||||
const router = useRouter();
|
||||
const href = `/docs/${docId}/`;
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If ctrl or command is pressed, it opens a new tab. If shift is pressed, it opens a new window
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
void router.push(href);
|
||||
};
|
||||
|
||||
// This triggers on middle-mouse click
|
||||
const handleAuxClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.button !== 1) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
export const useGetInterlinkingMenuItems = () => {
|
||||
const { currentDoc } = useDocStore();
|
||||
const createChildDoc = useCreateChildDocTree(currentDoc?.id);
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
as="span"
|
||||
className="--docs--interlinking-link-inline-content"
|
||||
onClick={handleClick}
|
||||
onAuxClick={handleAuxClick}
|
||||
draggable="false"
|
||||
$height="28px"
|
||||
$css={css`
|
||||
display: inline;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
& svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
transition: background-color var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
|
||||
.--docs--doc-deleted & {
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{emoji ? (
|
||||
<Text $size="16px">{emoji}</Text>
|
||||
) : (
|
||||
<SelectedPageIcon
|
||||
width={11.5}
|
||||
color="var(--c--contextuals--content--semantic--brand--tertiary)"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
$weight="500"
|
||||
spellCheck="false"
|
||||
$size="16px"
|
||||
$display="inline"
|
||||
$position="relative"
|
||||
$css={css`
|
||||
margin-left: 2px;
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
className="--docs-interlinking-underline"
|
||||
as="span"
|
||||
$height="1px"
|
||||
$width="100%"
|
||||
$background="var(--c--contextuals--border--semantic--neutral--tertiary)"
|
||||
$position="absolute"
|
||||
$hasTransition
|
||||
$radius="2px"
|
||||
$css={css`
|
||||
left: 0;
|
||||
bottom: 0px;
|
||||
`}
|
||||
/>
|
||||
<Box as="span" $zIndex="1" $position="relative">
|
||||
{titleWithoutEmoji}
|
||||
</Box>
|
||||
</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => getInterlinkinghMenuItems(editor, t, t('Links'), createChildDoc);
|
||||
};
|
||||
|
||||
const DisableInvalidInterlink = ({
|
||||
docId,
|
||||
onUpdateInlineContent,
|
||||
}: {
|
||||
docId: string;
|
||||
onUpdateInlineContent: () => void;
|
||||
}) => {
|
||||
useEffect(() => {
|
||||
Sentry.captureException(new Error(`Invalid docId: ${docId}`), {
|
||||
extra: { info: 'InterlinkingInlineContent' },
|
||||
});
|
||||
|
||||
onUpdateInlineContent();
|
||||
}, [docId, onUpdateInlineContent]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,87 +0,0 @@
|
|||
import { createReactInlineContentSpec } from '@blocknote/react';
|
||||
import { TFunction } from 'i18next';
|
||||
|
||||
import { DocsBlockNoteEditor } from '@/docs/doc-editor';
|
||||
import LinkPageIcon from '@/docs/doc-editor/assets/doc-link.svg';
|
||||
import AddPageIcon from '@/docs/doc-editor/assets/doc-plus.svg';
|
||||
import { useCreateChildDocTree, useDocStore } from '@/docs/doc-management';
|
||||
|
||||
import { SearchPage } from './SearchPage';
|
||||
|
||||
export const InterlinkingSearchInlineContent = createReactInlineContentSpec(
|
||||
{
|
||||
type: 'interlinkingSearchInline',
|
||||
propSchema: {
|
||||
trigger: {
|
||||
default: '/',
|
||||
values: ['/', '@'],
|
||||
},
|
||||
disabled: {
|
||||
default: false,
|
||||
values: [true, false],
|
||||
},
|
||||
},
|
||||
content: 'styled',
|
||||
},
|
||||
{
|
||||
render: (props) => {
|
||||
if (props.inlineContent.props.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<SearchPage
|
||||
{...props}
|
||||
trigger={props.inlineContent.props.trigger}
|
||||
contentRef={props.contentRef}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
export const getInterlinkinghMenuItems = (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
group: string,
|
||||
createPage: () => void,
|
||||
) => [
|
||||
{
|
||||
key: 'link-doc',
|
||||
title: t('Link a doc'),
|
||||
onItemClick: () => {
|
||||
editor.insertInlineContent([
|
||||
{
|
||||
type: 'interlinkingSearchInline',
|
||||
props: {
|
||||
disabled: false,
|
||||
trigger: '/',
|
||||
},
|
||||
},
|
||||
]);
|
||||
},
|
||||
aliases: ['interlinking', 'link', 'anchor', 'a'],
|
||||
group,
|
||||
icon: <LinkPageIcon />,
|
||||
subtext: t('Link this doc to another doc'),
|
||||
},
|
||||
{
|
||||
key: 'new-sub-doc',
|
||||
title: t('New sub-doc'),
|
||||
onItemClick: createPage,
|
||||
aliases: ['new sub-doc'],
|
||||
group,
|
||||
icon: <AddPageIcon />,
|
||||
subtext: t('Create a new sub-doc'),
|
||||
},
|
||||
];
|
||||
|
||||
export const useGetInterlinkingMenuItems = () => {
|
||||
const { currentDoc } = useDocStore();
|
||||
const createChildDoc = useCreateChildDocTree(currentDoc?.id);
|
||||
|
||||
return (
|
||||
editor: DocsBlockNoteEditor,
|
||||
t: TFunction<'translation', undefined>,
|
||||
) => getInterlinkinghMenuItems(editor, t, t('Links'), createChildDoc);
|
||||
};
|
||||
|
|
@ -0,0 +1,133 @@
|
|||
import { useRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, Text } from '@/components';
|
||||
import SelectedPageIcon from '@/docs/doc-editor/assets/doc-selected.svg';
|
||||
import { getEmojiAndTitle, useDoc } from '@/docs/doc-management/';
|
||||
|
||||
interface LinkSelectedProps {
|
||||
docId: string;
|
||||
title: string;
|
||||
isEditable: boolean;
|
||||
onUpdateTitle: (title: string) => void;
|
||||
}
|
||||
export const LinkSelected = ({
|
||||
docId,
|
||||
title,
|
||||
isEditable,
|
||||
onUpdateTitle,
|
||||
}: LinkSelectedProps) => {
|
||||
const { data: doc } = useDoc({ id: docId, withoutContent: true });
|
||||
|
||||
/**
|
||||
* Update the content title if the referenced doc title changes
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (isEditable && doc?.title && doc.title !== title) {
|
||||
onUpdateTitle(doc.title);
|
||||
}
|
||||
|
||||
/**
|
||||
* ⚠️ When doing collaborative editing, doc?.title might be out of sync
|
||||
* causing an infinite loop of updates.
|
||||
* To prevent this, we only run this effect when doc?.title changes,
|
||||
* not when inlineContent.props.title changes.
|
||||
*/
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [doc?.title, docId, isEditable]);
|
||||
|
||||
const { emoji, titleWithoutEmoji } = getEmojiAndTitle(title);
|
||||
const router = useRouter();
|
||||
const href = `/docs/${docId}/`;
|
||||
|
||||
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.preventDefault();
|
||||
|
||||
// If ctrl or command is pressed, it opens a new tab. If shift is pressed, it opens a new window
|
||||
if (e.metaKey || e.ctrlKey || e.shiftKey) {
|
||||
window.open(href, '_blank');
|
||||
return;
|
||||
}
|
||||
void router.push(href);
|
||||
};
|
||||
|
||||
// This triggers on middle-mouse click
|
||||
const handleAuxClick = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.button !== 1) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
window.open(href, '_blank');
|
||||
};
|
||||
|
||||
return (
|
||||
<BoxButton
|
||||
as="span"
|
||||
className="--docs--interlinking-link-inline-content"
|
||||
onClick={handleClick}
|
||||
onAuxClick={handleAuxClick}
|
||||
draggable="false"
|
||||
$height="28px"
|
||||
$css={css`
|
||||
display: inline;
|
||||
padding: 0.1rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
& svg {
|
||||
position: relative;
|
||||
top: 2px;
|
||||
margin-right: 0.2rem;
|
||||
}
|
||||
&:hover {
|
||||
background-color: var(
|
||||
--c--contextuals--background--semantic--contextual--primary
|
||||
);
|
||||
}
|
||||
transition: background-color var(--c--globals--transitions--duration)
|
||||
var(--c--globals--transitions--ease-out);
|
||||
|
||||
.--docs--doc-deleted & {
|
||||
pointer-events: none;
|
||||
}
|
||||
`}
|
||||
>
|
||||
{emoji ? (
|
||||
<Text $size="16px">{emoji}</Text>
|
||||
) : (
|
||||
<SelectedPageIcon
|
||||
width={11.5}
|
||||
color="var(--c--contextuals--content--semantic--brand--tertiary)"
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
$weight="500"
|
||||
spellCheck="false"
|
||||
$size="16px"
|
||||
$display="inline"
|
||||
$position="relative"
|
||||
$css={css`
|
||||
margin-left: 2px;
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
className="--docs-interlinking-underline"
|
||||
as="span"
|
||||
$height="1px"
|
||||
$width="100%"
|
||||
$background="var(--c--contextuals--border--semantic--neutral--tertiary)"
|
||||
$position="absolute"
|
||||
$hasTransition
|
||||
$radius="2px"
|
||||
$css={css`
|
||||
left: 0;
|
||||
bottom: 0px;
|
||||
`}
|
||||
/>
|
||||
<Box as="span" $zIndex="1" $position="relative">
|
||||
{titleWithoutEmoji}
|
||||
</Box>
|
||||
</Text>
|
||||
</BoxButton>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,8 +1,5 @@
|
|||
import {
|
||||
PartialCustomInlineContentFromConfig,
|
||||
StyleSchema,
|
||||
} from '@blocknote/core';
|
||||
import { useBlockNoteEditor } from '@blocknote/react';
|
||||
import { StyleSchema } from '@blocknote/core';
|
||||
import { ReactCustomInlineContentRenderProps } from '@blocknote/react';
|
||||
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||
import { Popover } from '@mantine/core';
|
||||
import type { KeyboardEvent } from 'react';
|
||||
|
|
@ -18,16 +15,14 @@ import {
|
|||
QuickSearchItemContent,
|
||||
Text,
|
||||
} from '@/components';
|
||||
import {
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema,
|
||||
} from '@/docs/doc-editor';
|
||||
import FoundPageIcon from '@/docs/doc-editor/assets/doc-found.svg';
|
||||
import { DocsBlockNoteEditor } from '@/docs/doc-editor/types';
|
||||
import { Doc, getEmojiAndTitle, useTrans } from '@/docs/doc-management';
|
||||
import { DocSearchContent, DocSearchTarget } from '@/docs/doc-search';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { InterlinkingLinkInlineContentType } from './InterlinkingLinkInlineContent';
|
||||
|
||||
const inputStyle = css`
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
|
|
@ -38,40 +33,18 @@ const inputStyle = css`
|
|||
font-family: 'Inter';
|
||||
`;
|
||||
|
||||
type SearchPageProps = {
|
||||
trigger: '/' | '@';
|
||||
updateInlineContent: (
|
||||
update: PartialCustomInlineContentFromConfig<
|
||||
{
|
||||
type: 'interlinkingSearchInline';
|
||||
propSchema: {
|
||||
disabled: {
|
||||
default: false;
|
||||
values: [true, false];
|
||||
};
|
||||
trigger: {
|
||||
default: '/';
|
||||
values: ['/', '@'];
|
||||
};
|
||||
};
|
||||
content: 'styled';
|
||||
},
|
||||
StyleSchema
|
||||
>,
|
||||
) => void;
|
||||
contentRef: (node: HTMLElement | null) => void;
|
||||
};
|
||||
type ReactInterlinkingSearch = ReactCustomInlineContentRenderProps<
|
||||
InterlinkingLinkInlineContentType,
|
||||
StyleSchema
|
||||
>;
|
||||
|
||||
export const SearchPage = ({
|
||||
contentRef,
|
||||
trigger,
|
||||
updateInlineContent,
|
||||
}: SearchPageProps) => {
|
||||
const editor = useBlockNoteEditor<
|
||||
DocsBlockSchema,
|
||||
DocsInlineContentSchema,
|
||||
DocsStyleSchema
|
||||
>();
|
||||
editor,
|
||||
inlineContent,
|
||||
}: ReactInterlinkingSearch) => {
|
||||
const trigger = inlineContent.props.trigger;
|
||||
const { t } = useTranslation();
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
const [search, setSearch] = useState('');
|
||||
|
|
@ -107,16 +80,19 @@ export const SearchPage = ({
|
|||
}
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingSearchInline',
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
disabled: true,
|
||||
trigger,
|
||||
},
|
||||
});
|
||||
|
||||
contentRef(null);
|
||||
editor.focus();
|
||||
editor.insertInlineContent([insertContent]);
|
||||
|
||||
if (insertContent) {
|
||||
contentRef(null);
|
||||
editor.focus();
|
||||
(editor as DocsBlockNoteEditor).insertInlineContent([insertContent]);
|
||||
}
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
|
|
@ -171,7 +147,7 @@ export const SearchPage = ({
|
|||
tabIndex={-1} // Ensure the span is focusable
|
||||
>
|
||||
{' '}
|
||||
<Box as="span" aria-hidden="true">
|
||||
<Box as="span" aria-hidden="true" $height="25px">
|
||||
{trigger}
|
||||
</Box>
|
||||
<Box
|
||||
|
|
@ -282,25 +258,14 @@ export const SearchPage = ({
|
|||
}
|
||||
|
||||
updateInlineContent({
|
||||
type: 'interlinkingSearchInline',
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
disabled: true,
|
||||
trigger,
|
||||
docId: doc.id,
|
||||
title: doc.title || untitledDocument,
|
||||
},
|
||||
});
|
||||
|
||||
contentRef(null);
|
||||
|
||||
editor.insertInlineContent([
|
||||
{
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
docId: doc.id,
|
||||
title: doc.title || untitledDocument,
|
||||
},
|
||||
},
|
||||
]);
|
||||
|
||||
editor.focus();
|
||||
}}
|
||||
renderSearchElement={(doc) => {
|
||||
|
|
|
|||
|
|
@ -1,2 +1 @@
|
|||
export * from './InterlinkingLinkInlineContent';
|
||||
export * from './InterlinkingSearchInlineContent';
|
||||
|
|
|
|||
|
|
@ -99,9 +99,8 @@ export const useShortcuts = (
|
|||
event.preventDefault();
|
||||
editor.insertInlineContent([
|
||||
{
|
||||
type: 'interlinkingSearchInline',
|
||||
type: 'interlinkingLinkInline',
|
||||
props: {
|
||||
disabled: false,
|
||||
trigger: '@',
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { DocsExporterDocx } from '../types';
|
|||
|
||||
export const inlineContentMappingInterlinkingLinkDocx: DocsExporterDocx['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
if (!inline.props.docId || !inline.props.title || inline.props.disabled) {
|
||||
return new TextRun('');
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { DocsExporterODT } from '../types';
|
|||
|
||||
export const inlineContentMappingInterlinkingLinkODT: DocsExporterODT['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
if (!inline.props.docId || !inline.props.title || inline.props.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import { DocsExporterPDF } from '../types';
|
|||
|
||||
export const inlineContentMappingInterlinkingLinkPDF: DocsExporterPDF['mappings']['inlineContentMapping']['interlinkingLinkInline'] =
|
||||
(inline) => {
|
||||
if (!inline.props.docId) {
|
||||
if (!inline.props.docId || !inline.props.title || inline.props.disabled) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
import { docxDefaultSchemaMappings } from '@blocknote/xl-docx-exporter';
|
||||
import { TextRun } from 'docx';
|
||||
|
||||
import {
|
||||
blockMappingCalloutDocx,
|
||||
|
|
@ -48,7 +47,6 @@ export const docxDocsSchemaMappings: DocsExporterDocx['mappings'] = {
|
|||
},
|
||||
inlineContentMapping: {
|
||||
...docxDefaultSchemaMappings.inlineContentMapping,
|
||||
interlinkingSearchInline: () => new TextRun(''),
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkDocx,
|
||||
},
|
||||
styleMapping: {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ export const odtDocsSchemaMappings: DocsExporterODT['mappings'] = {
|
|||
|
||||
inlineContentMapping: {
|
||||
...baseInlineMappings,
|
||||
interlinkingSearchInline: () => null,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkODT,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -30,7 +30,6 @@ export const pdfDocsSchemaMappings: DocsExporterPDF['mappings'] = {
|
|||
},
|
||||
inlineContentMapping: {
|
||||
...pdfDefaultSchemaMappings.inlineContentMapping,
|
||||
interlinkingSearchInline: () => <></>,
|
||||
interlinkingLinkInline: inlineContentMappingInterlinkingLinkPDF,
|
||||
},
|
||||
styleMapping: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue