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).toBeCalledTimes(1);
expect(editorEffect).toBeCalledWith(
getCodeMirror($),
$.container.querySelector('.bytemd')
expect.objectContaining({
$el: $.container.querySelector('.bytemd'),
editor: getCodeMirror($),
})
);
$.component.$set({ plugins: [] });

View File

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

View File

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

View File

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

View File

@ -40,29 +40,29 @@ export interface BytemdPlugin {
/**
* 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')`
*/
el: HTMLElement
): void | (() => void);
$el: HTMLElement;
}): void | (() => void);
/**
* 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
*/
result: VFile
): void | (() => void);
result: VFile;
}): void | (() => void);
}
export interface EditorProps extends ViewerProps {

View File

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

View File

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

View File

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

View File

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

View File

@ -31,14 +31,14 @@ export default function injectStyle({
});
};
}),
viewerEffect(el, result) {
viewerEffect({ $el, result }) {
(async () => {
const styleText = await lazyStyle?.(result.data);
if (!styleText) return;
const $style = document.createElement('style');
$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 {
return {
remark: (u) => u.use(remarkMath),
viewerEffect(el) {
viewerEffect({ $el }) {
const renderInline = async () => {
const els = el.querySelectorAll<HTMLElement>(
const els = $el.querySelectorAll<HTMLElement>(
'.math.math-inline:not(.math-display)' // for `inlineMathDouble === true` case
);
if (els.length === 0) return;
@ -27,7 +27,7 @@ export default function math({ katexOptions }: MathOptions = {}): BytemdPlugin {
};
const renderDisplay = async () => {
const els = el.querySelectorAll<HTMLElement>('.math.math-display');
const els = $el.querySelectorAll<HTMLElement>('.math.math-display');
if (els.length === 0) return;
const { render } = await import('katex');

View File

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

View File

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

View File

@ -2,23 +2,23 @@ import type { BytemdPlugin } from 'bytemd';
export default function scrollSync(): BytemdPlugin {
return {
editorEffect(cm, el) {
const viewer = el.querySelector('.bytemd-preview')!;
editorEffect({ editor, $el }) {
const $preview = $el.querySelector('.bytemd-preview')!;
const handleScroll = (cm: CodeMirror.Editor) => {
requestAnimationFrame(() => {
const editorInfo = cm.getScrollInfo();
const ratio =
editorInfo.top / (editorInfo.height - editorInfo.clientHeight);
viewer.scrollTo(
$preview.scrollTo(
0,
ratio * (viewer.scrollHeight - viewer.clientHeight)
ratio * ($preview.scrollHeight - $preview.clientHeight)
);
});
};
cm.on('scroll', handleScroll);
editor.on('scroll', handleScroll);
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 {
return {
viewerEffect(el) {
const els = el.querySelectorAll<HTMLElement>('pre>code.language-vega');
viewerEffect({ $el }) {
const els = $el.querySelectorAll<HTMLElement>('pre>code.language-vega');
if (els.length === 0) return;
import('vega-embed').then(({ default: embed }) => {

View File

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

View File

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