refactor: effect context api
This commit is contained in:
parent
02cb8d597b
commit
4861b00861
|
@ -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: [] });
|
||||||
|
|
|
@ -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: {},
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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() {
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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);
|
||||||
})();
|
})();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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');
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -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 }) => {
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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 })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
Loading…
Reference in New Issue