refactor: effect context api

This commit is contained in:
Rongjian Zhang 2020-10-14 21:30:59 +08:00
parent 02cb8d597b
commit 4861b00861
17 changed files with 75 additions and 71 deletions

View File

@ -83,8 +83,10 @@ describe('plugin', () => {
expect(editorEffect).toBeCalled(); expect(editorEffect).toBeCalled();
expect(editorEffect).toBeCalledTimes(1); expect(editorEffect).toBeCalledTimes(1);
expect(editorEffect).toBeCalledWith( expect(editorEffect).toBeCalledWith(
getCodeMirror($), expect.objectContaining({
$.container.querySelector('.bytemd') $el: $.container.querySelector('.bytemd'),
editor: getCodeMirror($),
})
); );
$.component.$set({ plugins: [] }); $.component.$set({ plugins: [] });

View File

@ -23,10 +23,12 @@ test('plugin', async () => {
expect(viewerEffect).toBeCalled(); expect(viewerEffect).toBeCalled();
expect(viewerEffect).toBeCalledTimes(1); expect(viewerEffect).toBeCalledTimes(1);
expect(viewerEffect).toBeCalledWith( expect(viewerEffect).toBeCalledWith(
$.container.querySelector('.markdown-body'),
expect.objectContaining({ expect.objectContaining({
contents: '', $el: $.container.querySelector('.markdown-body'),
data: {}, result: expect.objectContaining({
contents: '',
data: {},
}),
}) })
); );

View File

@ -19,7 +19,7 @@
sanitize, sanitize,
}; };
let textarea: HTMLTextAreaElement; let textarea: HTMLTextAreaElement;
let cm: CodeMirror.Editor; let editor: CodeMirror.Editor;
let activeTab = 0; let activeTab = 0;
let cbs: ReturnType<NonNullable<BytemdPlugin['editorEffect']>>[] = []; let cbs: ReturnType<NonNullable<BytemdPlugin['editorEffect']>>[] = [];
@ -28,16 +28,16 @@
// @ts-ignore // @ts-ignore
function setActiveTab(e) { function setActiveTab(e) {
activeTab = e.detail.value; activeTab = e.detail.value;
if (cm && activeTab === 0) { if (editor && activeTab === 0) {
tick().then(() => { tick().then(() => {
cm.focus(); editor.focus();
}); });
} }
} }
function on() { function on() {
cbs = (plugins ?? []).map( cbs = (plugins ?? []).map(({ editorEffect }) =>
({ editorEffect }) => editorEffect && editorEffect(cm, el) editorEffect?.({ editor, $el: el })
); );
} }
function off() { function off() {
@ -51,12 +51,12 @@
}; };
}, previewDebounce); }, previewDebounce);
$: if (cm && value !== cm.getValue()) { $: if (editor && value !== editor.getValue()) {
cm.setValue(value); editor.setValue(value);
} }
$: if (value != null) updateViewerValue(); $: if (value != null) updateViewerValue();
$: if (cm && el && plugins) { $: if (editor && el && plugins) {
off(); off();
tick().then(() => { tick().then(() => {
on(); on();
@ -72,19 +72,19 @@
import('codemirror/addon/display/placeholder'), import('codemirror/addon/display/placeholder'),
]); ]);
cm = codemirror.fromTextArea(textarea, { editor = codemirror.fromTextArea(textarea, {
mode: 'yaml-frontmatter', mode: 'yaml-frontmatter',
lineWrapping: true, lineWrapping: true,
placeholder: 'Start writing...', placeholder: 'Start writing...',
}); });
// https://github.com/codemirror/CodeMirror/issues/2428#issuecomment-39315423 // https://github.com/codemirror/CodeMirror/issues/2428#issuecomment-39315423
cm.addKeyMap({ editor.addKeyMap({
'Shift-Tab': 'indentLess', 'Shift-Tab': 'indentLess',
}); });
cm.setValue(value); editor.setValue(value);
cm.on('change', (doc, change) => { editor.on('change', (doc, change) => {
dispatch('change', { value: cm.getValue() }); dispatch('change', { value: editor.getValue() });
}); });
// No need to call `on` because cm instance would change once after init // No need to call `on` because cm instance would change once after init
}); });
@ -94,7 +94,7 @@
<svelte:options immutable={true} /> <svelte:options immutable={true} />
<div class="bytemd" bind:this={el} style={containerStyle}> <div class="bytemd" bind:this={el} style={containerStyle}>
<Toolbar {cm} {mode} {activeTab} {plugins} on:tab={setActiveTab} /> <Toolbar {editor} {mode} {activeTab} {plugins} on:tab={setActiveTab} />
<div class="bytemd-body"> <div class="bytemd-body">
<div <div
class="bytemd-editor" class="bytemd-editor"

View File

@ -6,7 +6,7 @@
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
export let cm: CodeMirror.Editor; export let editor: CodeMirror.Editor;
export let mode: EditorProps['mode']; export let mode: EditorProps['mode'];
export let activeTab: number; export let activeTab: number;
export let plugins: EditorProps['plugins']; export let plugins: EditorProps['plugins'];
@ -34,12 +34,12 @@
{#if !(mode === 'tab' && activeTab === 1)} {#if !(mode === 'tab' && activeTab === 1)}
{#each items.left as { tooltip, iconHtml, onClick }} {#each items.left as { tooltip, iconHtml, onClick }}
<ToolbarButton {tooltip} {iconHtml} on:click={() => onClick(cm)} /> <ToolbarButton {tooltip} {iconHtml} on:click={() => onClick(editor)} />
{/each} {/each}
{/if} {/if}
<div style="flex-grow:1" /> <div style="flex-grow:1" />
{#each items.right as { tooltip, iconHtml, onClick }} {#each items.right as { tooltip, iconHtml, onClick }}
<ToolbarButton {tooltip} {iconHtml} on:click={() => onClick(cm)} /> <ToolbarButton {tooltip} {iconHtml} on:click={() => onClick(editor)} />
{/each} {/each}
</div> </div>

View File

@ -40,29 +40,29 @@ export interface BytemdPlugin {
/** /**
* Side effect for editor, triggers when plugin list changes * Side effect for editor, triggers when plugin list changes
*/ */
editorEffect?( editorEffect?(context: {
/** /**
* CodeMirror instance * CodeMirror editor instance
*/ */
cm: CodeMirror.Editor, editor: CodeMirror.Editor;
/** /**
* Root element, `$('.bytemd')` * Root element, `$('.bytemd')`
*/ */
el: HTMLElement $el: HTMLElement;
): void | (() => void); }): void | (() => void);
/** /**
* Side effect for viewer, triggers when HTML or plugin list changes * Side effect for viewer, triggers when HTML or plugin list changes
*/ */
viewerEffect?( viewerEffect?(context: {
/** /**
* Root element of Viewer, `$('.markdown-body')` * Root element of the Viewer, `$('.markdown-body')`
*/ */
el: HTMLElement, $el: HTMLElement;
/** /**
* Markdown process result * Markdown process result
*/ */
result: VFile result: VFile;
): void | (() => void); }): void | (() => void);
} }
export interface EditorProps extends ViewerProps { export interface EditorProps extends ViewerProps {

View File

@ -12,7 +12,7 @@
function on() { function on() {
cbs = (plugins ?? []).map( cbs = (plugins ?? []).map(
({ viewerEffect }) => viewerEffect && viewerEffect(el, result) ({ viewerEffect }) => viewerEffect && viewerEffect({ $el: el, result })
); );
} }
function off() { function off() {

View File

@ -10,9 +10,9 @@ export default function highlight({
}: HighlightLazyOptions = {}): BytemdPlugin { }: HighlightLazyOptions = {}): BytemdPlugin {
let hljs: typeof H; let hljs: typeof H;
return { return {
viewerEffect(el) { viewerEffect({ $el }) {
(async () => { (async () => {
const els = el.querySelectorAll<HTMLElement>('pre>code'); const els = $el.querySelectorAll<HTMLElement>('pre>code');
if (els.length === 0) return; if (els.length === 0) return;
if (!hljs) { if (!hljs) {

View File

@ -31,7 +31,7 @@ export default function importHtml({
}, },
}: ImportHtmlOptions = {}): BytemdPlugin { }: ImportHtmlOptions = {}): BytemdPlugin {
return { return {
editorEffect(cm) { editorEffect({ editor }) {
const handler = async ( const handler = async (
_: CodeMirror.Editor, _: CodeMirror.Editor,
e: ClipboardEvent | DragEvent e: ClipboardEvent | DragEvent
@ -85,15 +85,15 @@ export default function importHtml({
processor = processor.use(remarkStringify, remarkStringifyOptions); processor = processor.use(remarkStringify, remarkStringifyOptions);
const result = await processor.process(html); const result = await processor.process(html);
cm.replaceSelection(result.toString()); editor.replaceSelection(result.toString());
}; };
cm.on('paste', handler); editor.on('paste', handler);
cm.on('drop', handler); editor.on('drop', handler);
return () => { return () => {
cm.off('paste', handler); editor.off('paste', handler);
cm.off('drop', handler); editor.off('drop', handler);
}; };
}, },
}; };

View File

@ -47,7 +47,7 @@ export default function importImage({
]; ];
}, },
}, },
editorEffect(cm) { editorEffect({ editor }) {
const handler = async ( const handler = async (
_: CodeMirror.Editor, _: CodeMirror.Editor,
e: ClipboardEvent | DragEvent e: ClipboardEvent | DragEvent
@ -62,16 +62,16 @@ export default function importImage({
.filter((f): f is File => f != null && test(f)); .filter((f): f is File => f != null && test(f));
if (files.length) { if (files.length) {
e.preventDefault(); e.preventDefault();
await handleFiles(files, cm); await handleFiles(files, editor);
} }
}; };
cm.on('paste', handler); editor.on('paste', handler);
cm.on('drop', handler); editor.on('drop', handler);
return () => { return () => {
cm.off('paste', handler); editor.off('paste', handler);
cm.off('drop', handler); editor.off('drop', handler);
}; };
}, },
}; };

View File

@ -31,14 +31,14 @@ export default function injectStyle({
}); });
}; };
}), }),
viewerEffect(el, result) { viewerEffect({ $el, result }) {
(async () => { (async () => {
const styleText = await lazyStyle?.(result.data); const styleText = await lazyStyle?.(result.data);
if (!styleText) return; if (!styleText) return;
const $style = document.createElement('style'); const $style = document.createElement('style');
$style.innerHTML = styleText; $style.innerHTML = styleText;
el.appendChild($style); $el.appendChild($style);
})(); })();
}, },
}; };

View File

@ -9,9 +9,9 @@ export interface MathOptions {
export default function math({ katexOptions }: MathOptions = {}): BytemdPlugin { export default function math({ katexOptions }: MathOptions = {}): BytemdPlugin {
return { return {
remark: (u) => u.use(remarkMath), remark: (u) => u.use(remarkMath),
viewerEffect(el) { viewerEffect({ $el }) {
const renderInline = async () => { const renderInline = async () => {
const els = el.querySelectorAll<HTMLElement>( const els = $el.querySelectorAll<HTMLElement>(
'.math.math-inline:not(.math-display)' // for `inlineMathDouble === true` case '.math.math-inline:not(.math-display)' // for `inlineMathDouble === true` case
); );
if (els.length === 0) return; if (els.length === 0) return;
@ -27,7 +27,7 @@ export default function math({ katexOptions }: MathOptions = {}): BytemdPlugin {
}; };
const renderDisplay = async () => { const renderDisplay = async () => {
const els = el.querySelectorAll<HTMLElement>('.math.math-display'); const els = $el.querySelectorAll<HTMLElement>('.math.math-display');
if (els.length === 0) return; if (els.length === 0) return;
const { render } = await import('katex'); const { render } = await import('katex');

View File

@ -3,11 +3,11 @@ import type { ZoomOptions } from 'medium-zoom';
export default function mediumZoom(options?: ZoomOptions): BytemdPlugin { export default function mediumZoom(options?: ZoomOptions): BytemdPlugin {
return { return {
viewerEffect(el) { viewerEffect({ $el }) {
const imgs = [...el.querySelectorAll('img')].filter((e) => { const imgs = [...$el.querySelectorAll('img')].filter((e) => {
// Exclude images with anchor parent // Exclude images with anchor parent
let $: HTMLElement | null = e; let $: HTMLElement | null = e;
while ($ && $ !== el) { while ($ && $ !== $el) {
if ($.tagName === 'A') return false; if ($.tagName === 'A') return false;
$ = $.parentElement; $ = $.parentElement;
} }

View File

@ -5,9 +5,9 @@ import type mermaidAPI from 'mermaid/mermaidAPI';
export default function mermaid(options?: mermaidAPI.Config): BytemdPlugin { export default function mermaid(options?: mermaidAPI.Config): BytemdPlugin {
let m: Mermaid; let m: Mermaid;
return { return {
viewerEffect(el) { viewerEffect({ $el }) {
(async () => { (async () => {
const els = el.querySelectorAll<HTMLElement>( const els = $el.querySelectorAll<HTMLElement>(
'pre>code.language-mermaid' 'pre>code.language-mermaid'
); );
if (els.length === 0) return; if (els.length === 0) return;

View File

@ -2,23 +2,23 @@ import type { BytemdPlugin } from 'bytemd';
export default function scrollSync(): BytemdPlugin { export default function scrollSync(): BytemdPlugin {
return { return {
editorEffect(cm, el) { editorEffect({ editor, $el }) {
const viewer = el.querySelector('.bytemd-preview')!; const $preview = $el.querySelector('.bytemd-preview')!;
const handleScroll = (cm: CodeMirror.Editor) => { const handleScroll = (cm: CodeMirror.Editor) => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
const editorInfo = cm.getScrollInfo(); const editorInfo = cm.getScrollInfo();
const ratio = const ratio =
editorInfo.top / (editorInfo.height - editorInfo.clientHeight); editorInfo.top / (editorInfo.height - editorInfo.clientHeight);
viewer.scrollTo( $preview.scrollTo(
0, 0,
ratio * (viewer.scrollHeight - viewer.clientHeight) ratio * ($preview.scrollHeight - $preview.clientHeight)
); );
}); });
}; };
cm.on('scroll', handleScroll); editor.on('scroll', handleScroll);
return () => { return () => {
cm.off('scroll', handleScroll); editor.off('scroll', handleScroll);
}; };
}, },
}; };

View File

@ -2,8 +2,8 @@ import type { BytemdPlugin } from 'bytemd';
export default function vega(): BytemdPlugin { export default function vega(): BytemdPlugin {
return { return {
viewerEffect(el) { viewerEffect({ $el }) {
const els = el.querySelectorAll<HTMLElement>('pre>code.language-vega'); const els = $el.querySelectorAll<HTMLElement>('pre>code.language-vega');
if (els.length === 0) return; if (els.length === 0) return;
import('vega-embed').then(({ default: embed }) => { import('vega-embed').then(({ default: embed }) => {

View File

@ -4,18 +4,18 @@ import * as bytemd from 'bytemd';
export interface ViewerProps extends bytemd.ViewerProps {} export interface ViewerProps extends bytemd.ViewerProps {}
export const Viewer: FC<ViewerProps> = ({ value, sanitize, plugins }) => { export const Viewer: FC<ViewerProps> = ({ value, sanitize, plugins }) => {
const el = useRef<HTMLDivElement>(null); const elRef = useRef<HTMLDivElement>(null);
const result = useMemo( const result = useMemo(
() => bytemd.processMarkdown({ value, sanitize, plugins }), () => bytemd.processMarkdown({ value, sanitize, plugins }),
[value, sanitize, plugins] [value, sanitize, plugins]
); );
useEffect(() => { useEffect(() => {
const $ = el.current; const $el = elRef.current;
if (!$) return; if (!$el) return;
const cbs = plugins?.map( const cbs = plugins?.map(
({ viewerEffect }) => viewerEffect && viewerEffect($, result) ({ viewerEffect }) => viewerEffect && viewerEffect({ $el, result })
); );
return () => { return () => {
cbs?.forEach((cb) => cb && cb()); cbs?.forEach((cb) => cb && cb());
@ -24,7 +24,7 @@ export const Viewer: FC<ViewerProps> = ({ value, sanitize, plugins }) => {
return ( return (
<div <div
ref={el} ref={elRef}
className="markdown-body" className="markdown-body"
dangerouslySetInnerHTML={{ __html: result.toString() }} dangerouslySetInnerHTML={{ __html: result.toString() }}
></div> ></div>

View File

@ -40,7 +40,7 @@ export default {
if (this.plugins) { if (this.plugins) {
this.cbs = this.plugins.map( this.cbs = this.plugins.map(
({ viewerEffect }) => ({ viewerEffect }) =>
viewerEffect && viewerEffect(this.$el, this.result) viewerEffect && viewerEffect({ $el: this.$el, result: this.result })
); );
} }
}, },