Skip to content

Commit

Permalink
feat(quoteLink): add quote link additional plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
kseniyakuzina committed Mar 5, 2025
1 parent ed88851 commit 81b7420
Show file tree
Hide file tree
Showing 22 changed files with 599 additions and 7 deletions.
96 changes: 96 additions & 0 deletions demo/stories/quoteLink/QuoteLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import {memo, useCallback} from 'react';

import {transform as quoteLink} from '@diplodoc/quote-link-extension';
import type {PluginWithParams} from 'markdown-it/lib';

import {QuoteLink as QuoteLinkExtension} from 'src/extensions/additional/QuoteLink';
import {
MarkdownEditorView,
type RenderPreview,
mQuoteLinkItemData,
markupToolbarConfigs,
useMarkdownEditor,
wQuoteLinkItemData,
wysiwygToolbarConfigs,
} from 'src/index';
import {cloneDeep} from 'src/lodash';

import {PlaygroundLayout} from '../../components/PlaygroundLayout';
import {SplitModePreview} from '../../components/SplitModePreview';
import {plugins as defaultPlugins} from '../../defaults/md-plugins';
import {useLogs} from '../../hooks/useLogs';

const wToolbarConfig = cloneDeep(wysiwygToolbarConfigs.wToolbarConfig);
wToolbarConfig.push([wQuoteLinkItemData]);

const wCommandMenuConfig = cloneDeep(wysiwygToolbarConfigs.wCommandMenuConfig);
wCommandMenuConfig.push(wQuoteLinkItemData);

const mToolbarConfig = cloneDeep(markupToolbarConfigs.mToolbarConfig);
mToolbarConfig.push([mQuoteLinkItemData]);

const plugins: PluginWithParams[] = [...defaultPlugins, quoteLink({bundle: false})];

export const QuoteLink = memo(() => {
const wSelectionMenuConfig = [
[wQuoteLinkItemData],
...wysiwygToolbarConfigs.wSelectionMenuConfig,
];

const renderPreview = useCallback<RenderPreview>(
({getValue, md}) => (
<SplitModePreview
getValue={getValue}
allowHTML={md.html}
linkify={md.linkify}
linkifyTlds={md.linkifyTlds}
breaks={md.breaks}
needToSanitizeHtml
plugins={plugins}
/>
),
[],
);

const editor = useMarkdownEditor({
initial: {markup: ''},
markupConfig: {renderPreview},
wysiwygConfig: {
extensions: QuoteLinkExtension,
extensionOptions: {
commandMenu: {
actions: wCommandMenuConfig,
},
selectionContext: {
config: wSelectionMenuConfig,
},
yfmConfigs: {
attrs: {
allowedAttributes: ['data-quotelink'],
},
},
},
},
});

useLogs(editor.logger);

return (
<PlaygroundLayout
editor={editor}
view={({className}) => (
<MarkdownEditorView
autofocus
stickyToolbar
settingsVisible
editor={editor}
className={className}
markupToolbarConfig={mToolbarConfig}
wysiwygToolbarConfig={wToolbarConfig}
/>
)}
/>
);
});

QuoteLink.displayName = 'GPT';
11 changes: 11 additions & 0 deletions demo/stories/quoteLink/quoteLink.stories.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type {StoryObj} from '@storybook/react';

import {QuoteLink as component} from './QuoteLink';

export const Story: StoryObj<typeof component> = {};
Story.storyName = 'QuoteLink';

export default {
title: 'Experiments / QuoteLink',
component,
};
25 changes: 25 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@
"@diplodoc/html-extension": "^2.5.0",
"@diplodoc/latex-extension": "1.0.3",
"@diplodoc/mermaid-extension": "1.2.1",
"@diplodoc/quote-link-extension": "0.0.0",
"@diplodoc/tabs-extension": "^3.5.1",
"@diplodoc/transform": "^4.43.0",
"@gravity-ui/eslint-config": "3.3.0",
Expand Down Expand Up @@ -295,11 +296,12 @@
},
"peerDependencies": {
"@diplodoc/cut-extension": "^0.5.0 || ^0.6.1 || ^0.7.1",
"@diplodoc/folding-headings-extension": "^0.1.0",
"@diplodoc/file-extension": "^0.2.1",
"@diplodoc/folding-headings-extension": "^0.1.0",
"@diplodoc/html-extension": "^2.3.2",
"@diplodoc/latex-extension": "^1.0.3",
"@diplodoc/mermaid-extension": "^1.0.0",
"@diplodoc/quote-link-extension": "^0.0.0",
"@diplodoc/tabs-extension": "^3.5.1",
"@diplodoc/transform": "^4.43.0",
"@gravity-ui/uikit": "^7.1.0",
Expand Down
1 change: 1 addition & 0 deletions src/bundle/config/action-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ const names = [
'orderedList',
'paragraph',
'quote',
'quoteLink',
'redo',
'sinkListItem',
'strike',
Expand Down
3 changes: 3 additions & 0 deletions src/bundle/config/icons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
BoldIcon,
CheckListIcon,
CircleLinkIcon,
CodeBlockIcon,
CodeInlineIcon,
CutIcon,
Expand Down Expand Up @@ -72,6 +73,7 @@ type Icon =
| 'image'
| 'table'
| 'quote'
| 'quoteLink'
| 'checklist'
| 'horizontalRule'
| 'file'
Expand Down Expand Up @@ -125,6 +127,7 @@ export const icons: Icons = {

table: {data: TableIcon},
quote: {data: QuoteIcon},
quoteLink: {data: CircleLinkIcon},
checklist: {data: CheckListIcon},

html: {data: HtmlBlockIcon},
Expand Down
12 changes: 12 additions & 0 deletions src/bundle/config/markup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import {i18n} from '../../i18n/menubar';
import {
insertBlockquoteLink,
insertHRule,
insertLink,
insertMermaidDiagram,
Expand Down Expand Up @@ -197,6 +198,17 @@ export const mQuoteButton: MToolbarSingleItemData = {
};
export const mQuoteItemData = mQuoteButton;

export const mQuoteLinkButton: MToolbarSingleItemData = {
id: ActionName.quoteLink,
type: ToolbarDataType.SingleButton,
title: i18n.bind(null, 'quotelink'),
icon: icons.quoteLink,
exec: (e) => insertBlockquoteLink(e.cm),
isActive: inactive,
isEnable: enable,
};
export const mQuoteLinkItemData = mQuoteLinkButton;

export const mCutButton: MToolbarSingleItemData = {
id: ActionName.yfm_cut,
type: ToolbarDataType.SingleButton,
Expand Down
13 changes: 13 additions & 0 deletions src/bundle/config/wysiwyg.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,19 @@ export const wMermaidItemData: WToolbarSingleItemData = {
isEnable: (e) => e.actions.createMermaid.isEnable(),
};

export const wQuoteLinkItemData: WToolbarSingleItemData = {
id: ActionName.quoteLink,
type: ToolbarDataType.SingleButton,
title: i18n.bind(null, 'quotelink'),
icon: icons.quoteLink,
exec: (e) => {
e.actions.quoteLink.run();
e.actions.addLinkToQuoteLink.run();
},
isActive: (e) => e.actions.quoteLink.isActive(),
isEnable: (e) => e.actions.quoteLink.isEnable(),
};

export const wCodeBlockItemData: WToolbarItemData = {
id: ActionName.code_block,
title: i18n.bind(null, 'codeblock'),
Expand Down
12 changes: 12 additions & 0 deletions src/extensions/additional/QuoteLink/PlaceholderWidget/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {Command} from 'prosemirror-state';

import type {ExtensionDeps} from '#core';

import {addPlaceholder} from './descriptor';

export const addQuoteLinkPlaceholder =
(deps: ExtensionDeps): Command =>
(state, dispatch) => {
dispatch?.(addPlaceholder(state.tr, deps).scrollIntoView());
return true;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type React from 'react';

import type {Transaction} from 'prosemirror-state';
import {TextSelection} from 'prosemirror-state';
import type {EditorView} from 'prosemirror-view';

import type {ExtensionDeps} from '#core';
import {
LinkAttr,
ReactWidgetDescriptor,
linkType,
normalizeUrlFactory,
pType,
removeDecoration,
} from 'src/extensions';
import {
LinkPlaceholderWidget,
type LinkPlaceholderWidgetProps,
} from 'src/extensions/markdown/Link/PlaceholderWidget/widget';
import {isTextSelection} from 'src/utils';

export class QuoteLinkWidgetDescriptor extends ReactWidgetDescriptor {
#domElem;
#view?: EditorView;
#getPos?: () => number;
#schema?: ExtensionDeps['schema'];

private normalizeUrl;

constructor(initPos: number, deps: ExtensionDeps) {
super(initPos, 'link-empty');
this.#domElem = document.createElement('span');
this.#schema = deps.schema;
this.normalizeUrl = normalizeUrlFactory(deps);
}

getDomElem(): HTMLElement {
return this.#domElem;
}

renderReactElement(view: EditorView, getPos: () => number): React.ReactElement {
this.#view = view;
this.#getPos = getPos;
return <LinkPlaceholderWidget onCancel={this.onCancel} onSubmit={this.onSubmit} />;
}

onCancel: LinkPlaceholderWidgetProps['onCancel'] = () => {
if (!this.#view) return;

this.#view.dispatch(removeDecoration(this.#view.state.tr, this.id));
this.#view.focus();
};

onSubmit: LinkPlaceholderWidgetProps['onSubmit'] = (params) => {
const normalizeResult = this.normalizeUrl(params.url);
if (!normalizeResult || !this.#view || !this.#getPos) return;

let tr = this.#view.state.tr;

const {url} = normalizeResult;
const text = params.text.trim() || normalizeResult.text;

const from = this.#getPos();
const isAllSelected =
from === 1 && (!isTextSelection(tr.selection) || !tr.selection.$cursor);
const to = from + text.length + (isAllSelected ? 1 : 0);

tr = tr.insertText(text, from);
tr = tr.addMark(
from,
to,
linkType(this.#view.state.schema).create({
[LinkAttr.Href]: url,
[LinkAttr.DataQuoteLink]: true,
}),
);

tr = removeDecoration(tr, this.id);

tr = tr.insert(
to,
pType(this.#view.state.schema).create(null, text ? this.#schema?.text(text) : null),
);
tr.setSelection(TextSelection.create(tr.doc, to + 1 + text.length + 1));

this.#view.dispatch(tr);
this.#view.focus();
};
}

export const addPlaceholder = (tr: Transaction, deps: ExtensionDeps) => {
const isAllSelected =
tr.selection.from === 0 && (!isTextSelection(tr.selection) || !tr.selection.$cursor);
return new QuoteLinkWidgetDescriptor(tr.selection.from + (isAllSelected ? 1 : 0), deps).applyTo(
tr,
);
};
Loading

0 comments on commit 81b7420

Please sign in to comment.