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 @@
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 All @@ -40,7 +43,7 @@

const b = block('playground');
const fileUploadHandler: FileUploadHandler = async (file) => {
console.info('[Playground] Uploading file: ' + file.name);

Check warning on line 46 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
await randomDelay(1000, 3000);
return {url: URL.createObjectURL(file)};
};
Expand Down Expand Up @@ -76,8 +79,10 @@
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 All @@ -98,8 +103,8 @@
>;

logger.setLogger({
metrics: console.info,

Check warning on line 106 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
action: (data) => console.info(`Action: ${data.action}`, data),

Check warning on line 107 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
...console,
});

Expand All @@ -123,6 +128,8 @@
extensionOptions,
wysiwygToolbarConfig,
wysiwygCommandMenuConfig,
markupConfigExtensions,
markupToolbarConfig,
escapeConfig,
enableSubmitInPreview,
hidePreviewAfterSubmit,
Expand Down Expand Up @@ -171,6 +178,9 @@
commandMenu: {actions: wysiwygCommandMenuConfig ?? wCommandMenuConfig},
...extensionOptions,
},
markupConfig: {
extensions: markupConfigExtensions,
},
extraExtensions: (builder) => {
builder
.use(Math, {
Expand Down Expand Up @@ -226,14 +236,14 @@
setEditorMode(mode);
}
const onToolbarAction = ({id, editorMode: type}: ToolbarActionData) => {
console.info(`The '${id}' action is performed in the ${type}-editor.`);

Check warning on line 239 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
};
function onChangeSplitModeEnabled({splitModeEnabled}: {splitModeEnabled: boolean}) {
props.onChangeSplitModeEnabled?.(splitModeEnabled);
console.info(`Split mode enabled: ${splitModeEnabled}`);

Check warning on line 243 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
}
function onChangeToolbarVisibility({visible}: {visible: boolean}) {
console.info('Toolbar visible: ' + visible);

Check warning on line 246 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

Unexpected console statement
}

mdEditor.on('cancel', onCancel);
Expand All @@ -253,7 +263,7 @@
mdEditor.off('change-split-mode-enabled', onChangeSplitModeEnabled);
mdEditor.off('change-toolbar-visibility', onChangeToolbarVisibility);
};
}, [mdEditor]);

Check warning on line 266 in demo/Playground.tsx

View workflow job for this annotation

GitHub Actions / Verify Files

React Hook useEffect has a missing dependency: 'props'. Either include it or remove the dependency array. However, 'props' will change when *any* prop changes, so the preferred fix is to destructure the 'props' object outside of the useEffect call and refer to those specific props inside useEffect

return (
<div className={b()}>
Expand Down Expand Up @@ -339,7 +349,7 @@
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
Loading