Skip to content

Commit

Permalink
fix: 🐛 extend from a factory cannot inject slice
Browse files Browse the repository at this point in the history
  • Loading branch information
Saul-Mirone committed Jun 8, 2022
1 parent b724b16 commit 94235f4
Show file tree
Hide file tree
Showing 19 changed files with 542 additions and 313 deletions.
14 changes: 14 additions & 0 deletions examples/vue/App.vue
@@ -0,0 +1,14 @@
<template>
<Milkdown />
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import Milkdown from './Milkdown.vue';
export default defineComponent({
name: 'App',
components: {
Milkdown,
},
});
</script>
180 changes: 180 additions & 0 deletions examples/vue/CodeFence/CodeFence.ts
@@ -0,0 +1,180 @@
/* Copyright 2021, Milkdown by Mirone. */
import { createSlice, editorViewCtx } from '@milkdown/core';
import { codeFence as originalCodeFence } from '@milkdown/preset-commonmark';
import { Fragment } from '@milkdown/prose/model';
import { RenderVue } from '@milkdown/vue';

const languageOptions = [
'',
'javascript',
'typescript',
'bash',
'sql',
'json',
'html',
'css',
'c',
'cpp',
'java',
'ruby',
'python',
'go',
'rust',
'markdown',
];

const id = 'fence';

export const languageListSlice = createSlice([] as string[], 'languageList');

export const codeFence: (view: ReturnType<RenderVue>) => typeof originalCodeFence = (view) =>
originalCodeFence.extend(
(original, utils, options) => {
const languageList = options?.languageList || languageOptions;
return {
...original,
schema: (ctx) => {
return {
...original.schema(ctx),
attrs: {
language: {
default: '',
},
filename: {
default: '',
},
fold: {
default: true,
},
showInput: {
default: false,
},
},
parseDOM: [
{
tag: 'div.code-fence-container',
preserveWhitespace: 'full',
getAttrs: (dom) => {
if (!(dom instanceof HTMLElement)) {
throw new Error('Parse DOM error.');
}
const pre = dom.querySelector('pre');
return {
language: pre?.dataset['language'],
filename: pre?.dataset['filename'],
};
},
getContent: (dom, schema) => {
if (!(dom instanceof HTMLElement)) {
throw new Error('Parse DOM error.');
}
const textNode = schema.text(dom.querySelector('pre')?.textContent ?? '');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return Fragment.from<any>(textNode);
},
},
{
tag: 'pre',
preserveWhitespace: 'full',
getAttrs: (dom) => {
if (!(dom instanceof HTMLElement)) {
throw new Error('Parse DOM error.');
}
return { language: dom.dataset['language'], filename: dom.dataset['filename'] };
},
},
],
toDOM: (node) => {
const select = document.createElement('select');
languageList.forEach((lang) => {
const option = document.createElement('option');
option.value = lang;
option.innerText = !lang ? '--' : lang;
if (lang === node.attrs['language']) {
option.selected = true;
}
select.appendChild(option);
});
select.onchange = (e) => {
const target = e.target;
if (!(target instanceof HTMLSelectElement)) {
return;
}
const view = ctx.get(editorViewCtx);
if (!view.editable) {
target.value = node.attrs['language'];
return;
}

const { top, left } = target.getBoundingClientRect();
const result = view.posAtCoords({ top, left });
if (!result) return;

const { tr } = view.state;

view.dispatch(
tr.setNodeMarkup(result.inside, undefined, {
...node.attrs,
language: target.value,
}),
);
};
return [
'div',
{
class: 'code-fence-container',
},
['span', node.attrs['filename']],
select,
[
'pre',
{
'data-language': node.attrs['language'],
'data-filename': node.attrs['filename'],
class: utils.getClassName(node.attrs, 'code-fence'),
},
['code', { spellCheck: 'false' }, 0],
],
];
},
parseMarkdown: {
match: ({ type }) => type === 'code',
runner: (state, node, type) => {
const meta = node['meta'] as string;
let filename = '';
if (meta) {
const match = meta.match(/^\[(\w+)\]$/);
if (match) {
filename = match[1] || '';
}
}
const language = node['lang'] as string;
const value = node['value'] as string;
state.openNode(type, { language, filename });
if (value) {
state.addText(value);
}
state.closeNode();
},
},
toMarkdown: {
match: (node) => node.type.name === id,
runner: (state, node) => {
const filename = node.attrs['filename'];
const meta = filename ? `[${filename}]` : '';
state.addNode('code', undefined, node.content.firstChild?.text || '', {
lang: node.attrs['language'],
meta,
});
},
},
};
},
view: (ctx) => {
ctx.set(languageListSlice, languageList);
return view(ctx);
},
};
},
[languageListSlice],
);
137 changes: 137 additions & 0 deletions examples/vue/CodeFence/CodeFence.vue
@@ -0,0 +1,137 @@
<script lang="ts">
import { nodeMetadata } from '@milkdown/vue';
import { defineComponent, inject } from 'vue';
import { languageListSlice } from './CodeFence';
export default defineComponent({
name: 'CodeFence',
});
</script>

<script setup lang="ts">
import { ref } from 'vue';
const metadata = inject(nodeMetadata);
const attrs = metadata?.node.attrs;
const languages = metadata?.ctx.get(languageListSlice) ?? [];
const filename = ref(attrs?.['filename'] ?? '');
const lang = attrs?.['language'] ?? '';
const showInput = ref(attrs?.['showInput'] ?? false);
const onChange = (e: Event) => {
const view = metadata?.view;
const node = metadata?.node;
const getPos = metadata?.getPos as () => number;
const { target } = e;
if (!(target instanceof HTMLSelectElement) || !view || !node) {
return;
}
const { value } = target;
console.log(target.value);
if (!view.editable) {
target.value = node.attrs['language'];
return;
}
const { tr } = view.state;
view.dispatch(
tr.setNodeMarkup(getPos(), undefined, {
...node.attrs,
language: value,
}),
);
};
const toggleInput = () => {
const view = metadata?.view;
const node = metadata?.node;
const getPos = metadata?.getPos as () => number;
if (!view || !node) {
return;
}
if (!view.editable) {
return;
}
const show = node.attrs['showInput'];
const { tr } = view.state;
view.dispatch(
tr.setNodeMarkup(getPos(), undefined, {
...node.attrs,
showInput: !show,
}),
);
showInput.value = !show;
};
const onKeydown = (e: KeyboardEvent) => {
e.stopPropagation();
};
const onFilenameChange = (e: Event) => {
const view = metadata?.view;
const node = metadata?.node;
const getPos = metadata?.getPos as () => number;
const { target } = e;
if (!(target instanceof HTMLInputElement)) return;
if (!view || !node) {
return;
}
if (!view.editable) {
return;
}
const { value } = target;
const { tr } = view.state;
view.dispatch(
tr.setNodeMarkup(getPos(), undefined, {
...node.attrs,
filename: value,
showInput: false,
}),
);
showInput.value = false;
filename.value = value;
};
</script>

<template>
<div class="code-fence">
<div class="control">
<span class="filename">
{{ filename }}
<input v-if="showInput" @keydown="onKeydown" @change="onFilenameChange" />
<button v-else @click="toggleInput">edit</button>
</span>
<select @change="onChange">
<option v-for="language in languages" :selected="language === lang" :value="language">
{{ language }}
</option>
</select>
</div>
<div class="code">
<slot />
</div>
</div>
</template>

<style scoped>
.code-fence {
border: 1px solid #ccc;
padding: 10px;
}
.control {
margin-bottom: 10px;
display: flex;
justify-content: space-between;
}
.code {
background: #ccc;
margin: 0 auto !important;
white-space: pre;
border-radius: 4px;
padding: 10px;
}
</style>

1 comment on commit 94235f4

@vercel
Copy link

@vercel vercel bot commented on 94235f4 Jun 8, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.