Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,8 @@ Read more:
- [How to add Mermaid extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-mermaid-extension.md)
- [How to write extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-create-extension.md)
- [How to add GPT extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-gpt-extensions.md)
- [How to add text binding extension in markdown](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-text-binding-extension-in-markdown.md)



### i18n
Expand Down
12 changes: 11 additions & 1 deletion demo/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ import {
MarkupString,
NumberInput,
RenderPreview,
ToolbarGroupData,
UseMarkdownEditorProps,
logger,
markupToolbarConfigs,
useMarkdownEditor,
wysiwygToolbarConfigs,
} from '../src';
import type {EscapeConfig, ToolbarActionData} from '../src/bundle/Editor';
import {Extension} from '../src/cm/state';
import {FoldingHeading} from '../src/extensions/yfm/FoldingHeading';
import {Math} from '../src/extensions/yfm/Math';
import {Mermaid} from '../src/extensions/yfm/Mermaid';
import {YfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock';
import {getSanitizeYfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock/utils';
import {cloneDeep} from '../src/lodash';
import {CodeEditor} from '../src/markup/editor';
import type {FileUploadHandler} from '../src/utils/upload';
import {VERSION} from '../src/version';

Expand Down Expand Up @@ -76,8 +79,10 @@ export type PlaygroundProps = {
initialSplitModeEnabled?: boolean;
renderPreviewDefined?: boolean;
height?: CSSProperties['height'];
markupConfigExtensions?: Extension[];
escapeConfig?: EscapeConfig;
wysiwygCommandMenuConfig?: wysiwygToolbarConfigs.WToolbarItemData[];
markupToolbarConfig?: ToolbarGroupData<CodeEditor>[];
onChangeEditorType?: (mode: MarkdownEditorMode) => void;
onChangeSplitModeEnabled?: (splitModeEnabled: boolean) => void;
} & Pick<
Expand Down Expand Up @@ -123,6 +128,8 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
extensionOptions,
wysiwygToolbarConfig,
wysiwygCommandMenuConfig,
markupConfigExtensions,
markupToolbarConfig,
escapeConfig,
enableSubmitInPreview,
hidePreviewAfterSubmit,
Expand Down Expand Up @@ -171,6 +178,9 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
commandMenu: {actions: wysiwygCommandMenuConfig ?? wCommandMenuConfig},
...extensionOptions,
},
markupConfig: {
extensions: markupConfigExtensions,
},
extraExtensions: (builder) => {
builder
.use(Math, {
Expand Down Expand Up @@ -339,7 +349,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
className={b('editor-view')}
stickyToolbar={Boolean(stickyToolbar)}
wysiwygToolbarConfig={wysiwygToolbarConfig ?? wToolbarConfig}
markupToolbarConfig={mToolbarConfig}
markupToolbarConfig={markupToolbarConfig ?? mToolbarConfig}
settingsVisible={settingsVisible}
editor={mdEditor}
enableSubmitInPreview={enableSubmitInPreview}
Expand Down
18 changes: 18 additions & 0 deletions demo/ghostExample/PlaygroundGhostExample.stories.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import React from 'react';

// eslint-disable-next-line import/no-extraneous-dependencies
import type {StoryFn} from '@storybook/react';

import {PlaygroundGhostExample} from './PlaygroundGhostExample';

export default {
title: 'Experiments / Popup in markup mode',
component: PlaygroundGhostExample,
};

type PlaygroundStoryProps = {};
export const Playground: StoryFn<PlaygroundStoryProps> = (props) => (
<PlaygroundGhostExample {...props} />
);

Playground.storyName = 'Ghost';
34 changes: 34 additions & 0 deletions demo/ghostExample/PlaygroundGhostExample.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import React from 'react';

import cloneDeep from 'lodash/cloneDeep';

import {logger, markupToolbarConfigs} from '../../src';
import {Playground} from '../Playground';

import {ghostPopupExtension, ghostPopupToolbarItem} from './ghostExtension';
import {initialMdContent} from './md-content';

import '../Playground.scss';

logger.setLogger({
metrics: console.info,
action: (data) => console.info(`Action: ${data.action}`, data),
...console,
});

const mToolbarConfig = cloneDeep(markupToolbarConfigs.mToolbarConfig);

mToolbarConfig[2].unshift(ghostPopupToolbarItem);

export const PlaygroundGhostExample = React.memo(() => {
return (
<Playground
settingsVisible
markupToolbarConfig={mToolbarConfig}
markupConfigExtensions={[ghostPopupExtension]}
initial={initialMdContent}
/>
);
});

PlaygroundGhostExample.displayName = 'Ghost-example';
1 change: 1 addition & 0 deletions demo/ghostExample/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is an example for documentation on creating and adding a text-bound extension for markup mode.
11 changes: 11 additions & 0 deletions demo/ghostExample/ghostExtension/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type {EditorView} from '../../../src/cm/view';

import {HideGhostPopupEffect, ShowGhostPopupEffect} from './effects';

export const showGhostPopup = (view: EditorView) => {
view.dispatch({effects: [ShowGhostPopupEffect.of(null)]});
};

export const hideGhostPopup = (view: EditorView) => {
view.dispatch({effects: [HideGhostPopupEffect.of(null)]});
};
4 changes: 4 additions & 0 deletions demo/ghostExample/ghostExtension/effects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import {StateEffect} from '../../../src/cm/state';

export const ShowGhostPopupEffect = StateEffect.define();
export const HideGhostPopupEffect = StateEffect.define();
6 changes: 6 additions & 0 deletions demo/ghostExample/ghostExtension/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {GhostPopupPlugin} from './plugin';

export {ghostPopupToolbarItem} from './toolbar';
export {showGhostPopup, hideGhostPopup} from './commands';

export const ghostPopupExtension = GhostPopupPlugin.extension;
108 changes: 108 additions & 0 deletions demo/ghostExample/ghostExtension/plugin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import {ReactRendererFacet} from '../../../src';
import {
Decoration,
type DecorationSet,
type EditorView,
type PluginValue,
ViewPlugin,
type ViewUpdate,
WidgetType,
} from '../../../src/cm/view';

import {hideGhostPopup} from './commands';
import {HideGhostPopupEffect, ShowGhostPopupEffect} from './effects';
import {renderPopup} from './popup';

const DECO_CLASS_NAME = 'ghost-example';

class SpanWidget extends WidgetType {
private className = '';
private textContent = '';

constructor(className: string, textContent: string) {
super();
this.className = className;
this.textContent = textContent;
}

toDOM() {
const spanElem = document.createElement('span');
spanElem.className = this.className;
spanElem.textContent = this.textContent;
return spanElem;
}
}

export const GhostPopupPlugin = ViewPlugin.fromClass(
class implements PluginValue {
decos: DecorationSet = Decoration.none;
readonly _view: EditorView;
readonly _renderItem;
_anchor: Element | null = null;

constructor(view: EditorView) {
this._view = view;
this._renderItem = view.state
.facet(ReactRendererFacet)
.createItem('ghost-popup-example-in-markup-mode', () => this.renderPopup());
}

update(update: ViewUpdate) {
if (update.docChanged || update.selectionSet) {
this.decos = Decoration.none;
return;
}

this.decos = this.decos.map(update.changes);
const {from, to} = update.state.selection.main;

for (const tr of update.transactions) {
for (const eff of tr.effects) {
if (eff.is(ShowGhostPopupEffect)) {
if (from === to) {
const decorationWidget = Decoration.widget({
widget: new SpanWidget(DECO_CLASS_NAME, ''),
});

this.decos = Decoration.set([decorationWidget.range(from)]);

return;
}

this.decos = Decoration.set([
{
from,
to,
value: Decoration.mark({class: DECO_CLASS_NAME}),
},
]);
}

if (eff.is(HideGhostPopupEffect)) {
this.decos = Decoration.none;
}
}
}
}

docViewUpdate() {
this._anchor = this._view.dom.getElementsByClassName(DECO_CLASS_NAME).item(0);
this._renderItem.rerender();
}

destroy() {
this._renderItem.remove();
}

renderPopup() {
return this._anchor
? renderPopup(this._anchor as HTMLElement, {
onClose: () => hideGhostPopup(this._view),
})
: null;
}
},
{
decorations: (value) => value.decos,
},
);
21 changes: 21 additions & 0 deletions demo/ghostExample/ghostExtension/popup.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import React from 'react';

import {Ghost} from '@gravity-ui/icons';
import {Button, Popup} from '@gravity-ui/uikit';

type Props = {
onClose: () => void;
};

export function renderPopup(anchor: HTMLElement, props: Props) {
return (
<Popup open anchorRef={{current: anchor}}>
<div style={{padding: '4px 8px', display: 'flex', alignItems: 'center'}}>
<Ghost width={'16px'} height={'16px'} />
<Button view="action" onClick={props.onClose} style={{marginLeft: '4px'}}>
Hide me
</Button>
</div>
</Popup>
);
}
16 changes: 16 additions & 0 deletions demo/ghostExample/ghostExtension/toolbar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import {Ghost} from '@gravity-ui/icons';

import {ToolbarDataType} from '../../../src';
import {MToolbarSingleItemData} from '../../../src/bundle/config/markup';

import {showGhostPopup} from './commands';

export const ghostPopupToolbarItem: MToolbarSingleItemData = {
id: 'ghost',
type: ToolbarDataType.SingleButton,
title: 'Show ghost',
icon: {data: Ghost},
exec: (e) => showGhostPopup(e.cm),
isActive: () => false,
isEnable: () => true,
};
9 changes: 9 additions & 0 deletions demo/ghostExample/md-content.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const initialMdContent = `
This is an example of working with a popup in markup editor mode. To test it:

1. Switch to markup mode.
2. Find the ghost icon in the toolbar.
3. Click on it.

Detailed documentation on how to create similar extensions can be found [here](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-add-text-binding-extension-in-markdown.md).
`;
Loading