Skip to content
Draft
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
7 changes: 7 additions & 0 deletions demo/src/components/Playground.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import {YfmPageConstructorExtension} from '@gravity-ui/markdown-editor-page-cons
import {wYfmPageConstructorItemData} from '@gravity-ui/markdown-editor-page-constructor-extension/configs';
import {Button, DropdownMenu} from '@gravity-ui/uikit';

import {htmlBlockTemplates} from '../defaults/html-templates';
import {getPlugins} from '../defaults/md-plugins';
import {useLogs} from '../hooks/useLogs';
import useYfmHtmlBlockStyles from '../hooks/useYfmHtmlBlockStyles';
Expand Down Expand Up @@ -236,6 +237,12 @@ export const Playground = memo<PlaygroundProps>((props) => {
storyAdditionalControls?.yfmHtmlBlockAutoSaveEnabled ?? true,
delay: storyAdditionalControls?.yfmHtmlBlockAutoSaveDelay ?? 1000,
},
templates: {
items: htmlBlockTemplates,
showButton: true,
allowAdd: true,
},
editablePreview: true,
head: `
<base target="_blank" />
<style>
Expand Down
25 changes: 25 additions & 0 deletions demo/src/defaults/html-templates.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
export const htmlBlockTemplates = [
{
id: 'callout',
title: 'Callout',
content: `<div style="padding:16px;border-left:4px solid #2563eb;background:#eff6ff;border-radius:8px;">
<strong>Heads up</strong>
<p style="margin:8px 0 0;">Replace this text with your message.</p>
</div>`,
},
{
id: 'two-columns',
title: 'Two columns',
content: `<div style="display:grid;grid-template-columns:1fr 1fr;gap:16px;">
<div style="padding:16px;background:#f8fafc;border-radius:8px;">Left</div>
<div style="padding:16px;background:#f8fafc;border-radius:8px;">Right</div>
</div>`,
},
{
id: 'cta-button',
title: 'CTA button',
content: `<a href="#" style="display:inline-block;padding:12px 20px;background:#2563eb;color:#fff;border-radius:8px;text-decoration:none;font-weight:600;">
Get started
</a>`,
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
.g-md-yfm-html-block-templates {
display: flex;
flex-direction: column;

min-width: 240px;
max-width: 360px;

&__search {
padding: 8px 8px 4px;
}

&__editor {
display: flex;
flex-direction: column;

gap: 8px;
padding: 8px;
}

&__controls {
display: flex;
justify-content: end;

gap: 8px;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import {useMemo, useState} from 'react';

import {Plus} from '@gravity-ui/icons';
import {Button, Icon, Menu, Popup, TextInput} from '@gravity-ui/uikit';

import {cn} from 'src/classname';
import {TextAreaFixed as TextArea} from 'src/forms/TextInput';
import {i18n} from 'src/i18n/yfm-html-block';

import {type HtmlTemplate, parseTemplates, saveTemplates} from '../templates';

import {STOP_EVENT_CLASSNAME} from './const';

import './TemplatesPopup.scss';

const b = cn('yfm-html-block-templates');
const stop = STOP_EVENT_CLASSNAME;

interface TemplatesPopupProps {
anchor: HTMLElement | null;
open: boolean;
templates: HtmlTemplate[];
allowAdd: boolean;
onClose: () => void;
onApply: (template: HtmlTemplate) => void;
onAdded: (templates: HtmlTemplate[]) => void;
}

export const TemplatesPopup: React.FC<TemplatesPopupProps> = ({
anchor,
open,
templates,
allowAdd,
onClose,
onApply,
onAdded,
}) => {
const [adding, setAdding] = useState(false);
const [input, setInput] = useState('');
const [filter, setFilter] = useState('');

const filtered = useMemo(() => {
const query = filter.trim().toLowerCase();
if (!query) return templates;
return templates.filter((t) => t.title.toLowerCase().includes(query));
}, [templates, filter]);

const close = () => {
setAdding(false);
setInput('');
setFilter('');
onClose();
};

const handleSave = () => {
const parsed = parseTemplates(input);
if (parsed.length) onAdded(saveTemplates(parsed));
setInput('');
setAdding(false);
};

return (
<Popup anchorElement={anchor} open={open} onOpenChange={close} placement="bottom-end">
<div className={b(null, [stop])}>
{adding ? (
<div className={b('editor')}>
<TextArea
controlProps={{className: stop}}
value={input}
onUpdate={setInput}
placeholder={i18n('templates_input_placeholder')}
minRows={6}
autoFocus
/>
<div className={b('controls')}>
<Button view="flat" className={stop} onClick={() => setAdding(false)}>
<span className={stop}>{i18n('cancel')}</span>
</Button>
<Button
view="action"
className={stop}
disabled={!input.trim()}
onClick={handleSave}
>
<span className={stop}>{i18n('save')}</span>
</Button>
</div>
</div>
) : (
<>
{templates.length > 0 && (
<div className={b('search')}>
<TextInput
className={stop}
controlProps={{className: stop}}
size="s"
value={filter}
onUpdate={setFilter}
placeholder={i18n('search_templates')}
autoFocus
/>
</div>
)}
<Menu className={stop}>
{allowAdd && (
<Menu.Item
className={stop}
iconStart={<Icon data={Plus} />}
onClick={() => setAdding(true)}
>
{i18n('add_template')}
</Menu.Item>
)}
{filtered.map((template) => (
<Menu.Item
key={template.id}
className={stop}
onClick={() => {
onApply(template);
close();
}}
>
{template.title}
</Menu.Item>
))}
{filtered.length === 0 && (
<Menu.Item disabled className={stop}>
{i18n('templates_empty')}
</Menu.Item>
)}
</Menu>
</>
)}
</div>
</Popup>
);
};
Loading
Loading