Skip to content

Commit c4f2b36

Browse files
authored
chore: add ghost example in markup mode (#410)
1 parent 7dd24ef commit c4f2b36

File tree

13 files changed

+488
-1
lines changed

13 files changed

+488
-1
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,8 @@ Read more:
5959
- [How to add Mermaid extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-mermaid-extension.md)
6060
- [How to write extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-create-extension.md)
6161
- [How to add GPT extension](https://github.com/gravity-ui/markdown-editor/blob/main/docs/how-to-connect-gpt-extensions.md)
62+
- [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)
63+
6264

6365

6466
### i18n

demo/Playground.tsx

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,19 +11,22 @@ import {
1111
MarkupString,
1212
NumberInput,
1313
RenderPreview,
14+
ToolbarGroupData,
1415
UseMarkdownEditorProps,
1516
logger,
1617
markupToolbarConfigs,
1718
useMarkdownEditor,
1819
wysiwygToolbarConfigs,
1920
} from '../src';
2021
import type {EscapeConfig, ToolbarActionData} from '../src/bundle/Editor';
22+
import {Extension} from '../src/cm/state';
2123
import {FoldingHeading} from '../src/extensions/yfm/FoldingHeading';
2224
import {Math} from '../src/extensions/yfm/Math';
2325
import {Mermaid} from '../src/extensions/yfm/Mermaid';
2426
import {YfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock';
2527
import {getSanitizeYfmHtmlBlock} from '../src/extensions/yfm/YfmHtmlBlock/utils';
2628
import {cloneDeep} from '../src/lodash';
29+
import {CodeEditor} from '../src/markup/editor';
2730
import type {FileUploadHandler} from '../src/utils/upload';
2831
import {VERSION} from '../src/version';
2932

@@ -76,8 +79,10 @@ export type PlaygroundProps = {
7679
initialSplitModeEnabled?: boolean;
7780
renderPreviewDefined?: boolean;
7881
height?: CSSProperties['height'];
82+
markupConfigExtensions?: Extension[];
7983
escapeConfig?: EscapeConfig;
8084
wysiwygCommandMenuConfig?: wysiwygToolbarConfigs.WToolbarItemData[];
85+
markupToolbarConfig?: ToolbarGroupData<CodeEditor>[];
8186
onChangeEditorType?: (mode: MarkdownEditorMode) => void;
8287
onChangeSplitModeEnabled?: (splitModeEnabled: boolean) => void;
8388
} & Pick<
@@ -123,6 +128,8 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
123128
extensionOptions,
124129
wysiwygToolbarConfig,
125130
wysiwygCommandMenuConfig,
131+
markupConfigExtensions,
132+
markupToolbarConfig,
126133
escapeConfig,
127134
enableSubmitInPreview,
128135
hidePreviewAfterSubmit,
@@ -171,6 +178,9 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
171178
commandMenu: {actions: wysiwygCommandMenuConfig ?? wCommandMenuConfig},
172179
...extensionOptions,
173180
},
181+
markupConfig: {
182+
extensions: markupConfigExtensions,
183+
},
174184
extraExtensions: (builder) => {
175185
builder
176186
.use(Math, {
@@ -339,7 +349,7 @@ export const Playground = React.memo<PlaygroundProps>((props) => {
339349
className={b('editor-view')}
340350
stickyToolbar={Boolean(stickyToolbar)}
341351
wysiwygToolbarConfig={wysiwygToolbarConfig ?? wToolbarConfig}
342-
markupToolbarConfig={mToolbarConfig}
352+
markupToolbarConfig={markupToolbarConfig ?? mToolbarConfig}
343353
settingsVisible={settingsVisible}
344354
editor={mdEditor}
345355
enableSubmitInPreview={enableSubmitInPreview}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import React from 'react';
2+
3+
// eslint-disable-next-line import/no-extraneous-dependencies
4+
import type {StoryFn} from '@storybook/react';
5+
6+
import {PlaygroundGhostExample} from './PlaygroundGhostExample';
7+
8+
export default {
9+
title: 'Experiments / Popup in markup mode',
10+
component: PlaygroundGhostExample,
11+
};
12+
13+
type PlaygroundStoryProps = {};
14+
export const Playground: StoryFn<PlaygroundStoryProps> = (props) => (
15+
<PlaygroundGhostExample {...props} />
16+
);
17+
18+
Playground.storyName = 'Ghost';
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import React from 'react';
2+
3+
import cloneDeep from 'lodash/cloneDeep';
4+
5+
import {logger, markupToolbarConfigs} from '../../src';
6+
import {Playground} from '../Playground';
7+
8+
import {ghostPopupExtension, ghostPopupToolbarItem} from './ghostExtension';
9+
import {initialMdContent} from './md-content';
10+
11+
import '../Playground.scss';
12+
13+
logger.setLogger({
14+
metrics: console.info,
15+
action: (data) => console.info(`Action: ${data.action}`, data),
16+
...console,
17+
});
18+
19+
const mToolbarConfig = cloneDeep(markupToolbarConfigs.mToolbarConfig);
20+
21+
mToolbarConfig[2].unshift(ghostPopupToolbarItem);
22+
23+
export const PlaygroundGhostExample = React.memo(() => {
24+
return (
25+
<Playground
26+
settingsVisible
27+
markupToolbarConfig={mToolbarConfig}
28+
markupConfigExtensions={[ghostPopupExtension]}
29+
initial={initialMdContent}
30+
/>
31+
);
32+
});
33+
34+
PlaygroundGhostExample.displayName = 'Ghost-example';

demo/ghostExample/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
This is an example for documentation on creating and adding a text-bound extension for markup mode.
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import type {EditorView} from '../../../src/cm/view';
2+
3+
import {HideGhostPopupEffect, ShowGhostPopupEffect} from './effects';
4+
5+
export const showGhostPopup = (view: EditorView) => {
6+
view.dispatch({effects: [ShowGhostPopupEffect.of(null)]});
7+
};
8+
9+
export const hideGhostPopup = (view: EditorView) => {
10+
view.dispatch({effects: [HideGhostPopupEffect.of(null)]});
11+
};
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
import {StateEffect} from '../../../src/cm/state';
2+
3+
export const ShowGhostPopupEffect = StateEffect.define();
4+
export const HideGhostPopupEffect = StateEffect.define();
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import {GhostPopupPlugin} from './plugin';
2+
3+
export {ghostPopupToolbarItem} from './toolbar';
4+
export {showGhostPopup, hideGhostPopup} from './commands';
5+
6+
export const ghostPopupExtension = GhostPopupPlugin.extension;
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import {ReactRendererFacet} from '../../../src';
2+
import {
3+
Decoration,
4+
type DecorationSet,
5+
type EditorView,
6+
type PluginValue,
7+
ViewPlugin,
8+
type ViewUpdate,
9+
WidgetType,
10+
} from '../../../src/cm/view';
11+
12+
import {hideGhostPopup} from './commands';
13+
import {HideGhostPopupEffect, ShowGhostPopupEffect} from './effects';
14+
import {renderPopup} from './popup';
15+
16+
const DECO_CLASS_NAME = 'ghost-example';
17+
18+
class SpanWidget extends WidgetType {
19+
private className = '';
20+
private textContent = '';
21+
22+
constructor(className: string, textContent: string) {
23+
super();
24+
this.className = className;
25+
this.textContent = textContent;
26+
}
27+
28+
toDOM() {
29+
const spanElem = document.createElement('span');
30+
spanElem.className = this.className;
31+
spanElem.textContent = this.textContent;
32+
return spanElem;
33+
}
34+
}
35+
36+
export const GhostPopupPlugin = ViewPlugin.fromClass(
37+
class implements PluginValue {
38+
decos: DecorationSet = Decoration.none;
39+
readonly _view: EditorView;
40+
readonly _renderItem;
41+
_anchor: Element | null = null;
42+
43+
constructor(view: EditorView) {
44+
this._view = view;
45+
this._renderItem = view.state
46+
.facet(ReactRendererFacet)
47+
.createItem('ghost-popup-example-in-markup-mode', () => this.renderPopup());
48+
}
49+
50+
update(update: ViewUpdate) {
51+
if (update.docChanged || update.selectionSet) {
52+
this.decos = Decoration.none;
53+
return;
54+
}
55+
56+
this.decos = this.decos.map(update.changes);
57+
const {from, to} = update.state.selection.main;
58+
59+
for (const tr of update.transactions) {
60+
for (const eff of tr.effects) {
61+
if (eff.is(ShowGhostPopupEffect)) {
62+
if (from === to) {
63+
const decorationWidget = Decoration.widget({
64+
widget: new SpanWidget(DECO_CLASS_NAME, ''),
65+
});
66+
67+
this.decos = Decoration.set([decorationWidget.range(from)]);
68+
69+
return;
70+
}
71+
72+
this.decos = Decoration.set([
73+
{
74+
from,
75+
to,
76+
value: Decoration.mark({class: DECO_CLASS_NAME}),
77+
},
78+
]);
79+
}
80+
81+
if (eff.is(HideGhostPopupEffect)) {
82+
this.decos = Decoration.none;
83+
}
84+
}
85+
}
86+
}
87+
88+
docViewUpdate() {
89+
this._anchor = this._view.dom.getElementsByClassName(DECO_CLASS_NAME).item(0);
90+
this._renderItem.rerender();
91+
}
92+
93+
destroy() {
94+
this._renderItem.remove();
95+
}
96+
97+
renderPopup() {
98+
return this._anchor
99+
? renderPopup(this._anchor as HTMLElement, {
100+
onClose: () => hideGhostPopup(this._view),
101+
})
102+
: null;
103+
}
104+
},
105+
{
106+
decorations: (value) => value.decos,
107+
},
108+
);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import React from 'react';
2+
3+
import {Ghost} from '@gravity-ui/icons';
4+
import {Button, Popup} from '@gravity-ui/uikit';
5+
6+
type Props = {
7+
onClose: () => void;
8+
};
9+
10+
export function renderPopup(anchor: HTMLElement, props: Props) {
11+
return (
12+
<Popup open anchorRef={{current: anchor}}>
13+
<div style={{padding: '4px 8px', display: 'flex', alignItems: 'center'}}>
14+
<Ghost width={'16px'} height={'16px'} />
15+
<Button view="action" onClick={props.onClose} style={{marginLeft: '4px'}}>
16+
Hide me
17+
</Button>
18+
</div>
19+
</Popup>
20+
);
21+
}

0 commit comments

Comments
 (0)