refactor: effect context api
This commit is contained in:
parent
02cb8d597b
commit
4861b00861
|
@ -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: [] });
|
||||
|
|
|
@ -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: {},
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
|
||||
function on() {
|
||||
cbs = (plugins ?? []).map(
|
||||
({ viewerEffect }) => viewerEffect && viewerEffect(el, result)
|
||||
({ viewerEffect }) => viewerEffect && viewerEffect({ $el: el, result })
|
||||
);
|
||||
}
|
||||
function off() {
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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);
|
||||
})();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
};
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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 }) => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 })
|
||||
);
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Reference in New Issue