Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Documentation editor: Code block support and block format status #12406

Merged
merged 5 commits into from
Mar 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

15 changes: 10 additions & 5 deletions app/gui/src/project-view/components/MarkdownEditor.vue
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,15 @@ import { Vec2 } from '@/util/data/vec2'
import { ComponentInstance, computed, defineAsyncComponent, ref, toRef } from 'vue'
import * as Y from 'yjs'

const props = defineProps<{
content: Y.Text | string
transformImageUrl?: UrlTransformer
}>()
const props = withDefaults(
defineProps<{
content: Y.Text | string
// eslint-disable-next-line vue/require-default-prop
transformImageUrl?: UrlTransformer | undefined
toolbar?: boolean
}>(),
{ toolbar: true },
)

const inner = ref<ComponentInstance<typeof LazyMarkdownEditor>>()

Expand All @@ -33,7 +38,7 @@ defineExpose({

<template>
<Suspense>
<LazyMarkdownEditor ref="inner" v-bind="props">
<LazyMarkdownEditor ref="inner" :content="props.content" :toolbar="props.toolbar">
<template #toolbarLeft>
<slot name="toolbarLeft" />
</template>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,68 +1,67 @@
<script setup lang="ts">
import DropdownMenu from '@/components/DropdownMenu.vue'
import {
type HeaderLevel,
type ListType,
} from '@/components/MarkdownEditor/codemirror/formatting/block'
import MenuButton from '@/components/MenuButton.vue'
import MenuPanel from '@/components/MenuPanel.vue'
import SvgIcon from '@/components/SvgIcon.vue'
import { type BlockType } from '@/components/MarkdownEditor/codemirror/formatting'
import SelectionDropdown from '@/components/SelectionDropdown.vue'
import { type Icon } from '@/util/iconMetadata/iconName'
import { ref } from 'vue'
import { computed } from 'vue'

const emit = defineEmits<{
toggleHeader: [HeaderLevel]
toggleQuote: []
toggleList: [ListType]
}>()
const blockType = defineModel<BlockType | 'Unknown'>({ required: true })

interface MenuItem {
name: string
icon: Icon
action: () => void
const blockIcon: Record<BlockType | 'Unknown', Icon> = {
Unknown: 'text',
Paragraph: 'text',
BulletList: 'bullet-list',
ATXHeading1: 'header1',
ATXHeading2: 'header2',
ATXHeading3: 'header3',
OrderedList: 'numbered-list',
Blockquote: 'quote',
FencedCode: 'code',
}
const menuItems: MenuItem[] = [
{ name: 'Header 1', icon: 'header1', action: () => emit('toggleHeader', 1) },
{ name: 'Header 2', icon: 'header2', action: () => emit('toggleHeader', 2) },
{ name: 'Header 3', icon: 'header3', action: () => emit('toggleHeader', 3) },
{ name: 'Quote', icon: 'quote', action: () => emit('toggleQuote') },
{ name: 'Bullet list', icon: 'bullet-list', action: () => emit('toggleList', 'unordered') },
{ name: 'Numbered list', icon: 'numbered-list', action: () => emit('toggleList', 'ordered') },
const blockName: Record<BlockType | 'Unknown', string> = {
Unknown: 'Paragraph type',
Paragraph: 'Normal',
BulletList: 'List',
ATXHeading1: 'Header 1',
ATXHeading2: 'Header 2',
ATXHeading3: 'Header 3',
OrderedList: 'Numbered List',
Blockquote: 'Quote',
FencedCode: 'Code',
}
const blockTypesOrdered: BlockType[] = [
'Paragraph',
'ATXHeading1',
'ATXHeading2',
'ATXHeading3',
'BulletList',
'OrderedList',
'Blockquote',
]

const open = ref(false)
const blockTypeOptions = computed(() => {
// Always show the current type.
const shownTypes =
blockTypesOrdered.includes(blockType.value as BlockType) ? blockTypesOrdered : (
[...blockTypesOrdered, blockType.value]
)
// Code cannot directly be converted to other block types. Switching to `Paragraph` removes the delimiters, and allows
// whatever is contained to be interpreted as Markdown; once the content is Markdown, further styling changes can be
// made.
const disableSettingTypes = blockType.value === 'FencedCode'
return Object.fromEntries(
shownTypes.map((key) => [
key,
{
icon: blockIcon[key],
label: blockName[key],
disabled: disableSettingTypes ? key !== blockType.value && key !== 'Paragraph' : false,
hidden: key === 'Unknown',
},
]),
)
})
</script>

<template>
<DropdownMenu v-model:open="open" title="Block type">
<template #button>
<SvgIcon name="text3" />
</template>
<template #menu>
<MenuPanel>
<template v-for="item in menuItems" :key="item.name">
<MenuButton @click="(item.action(), (open = false))">
<SvgIcon :name="item.icon" />
<div class="iconLabel" v-text="item.name" />
</MenuButton>
</template>
</MenuPanel>
</template>
</DropdownMenu>
<SelectionDropdown v-model="blockType" :options="blockTypeOptions" labelButton />
</template>

<style scoped>
.MenuPanel {
box-shadow: 0 0 1px rgba(0, 0, 0, 0.2);
}

.MenuButton {
margin: -4px;
justify-content: unset;
}

.iconLabel {
margin-left: 4px;
padding-right: 4px;
}
</style>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import CodeMirrorRoot from '@/components/CodeMirrorRoot.vue'
import { transformPastedText } from '@/components/DocumentationEditor/textPaste'
import BlockTypeDropdown from '@/components/MarkdownEditor/BlockTypeDropdown.vue'
import { ensoMarkdown, useMarkdownFormatting } from '@/components/MarkdownEditor/codemirror'
import { type BlockType } from '@/components/MarkdownEditor/codemirror/formatting'
import SvgButton from '@/components/SvgButton.vue'
import ToggleIcon from '@/components/ToggleIcon.vue'
import VueHostRender, { VueHostInstance } from '@/components/VueHostRender.vue'
Expand All @@ -15,9 +16,9 @@ import { minimalSetup } from 'codemirror'
import { computed, onMounted, ref, useCssModule, useTemplateRef, type ComponentInstance } from 'vue'
import * as Y from 'yjs'

const { content } = defineProps<{
const { content, toolbar } = defineProps<{
content: Y.Text | string
toolbarContainer?: HTMLElement | undefined
toolbar: boolean
}>()

const focused = ref(false)
Expand All @@ -36,8 +37,7 @@ const { editorView, readonly, putTextAt } = useCodeMirror(editorRoot, {
],
vueHost: () => vueHost,
})
const { toggleHeader, toggleQuote, toggleList, italic, bold, insertLink } =
useMarkdownFormatting(editorView)
const { italic, bold, insertLink, blockType, insertCodeBlock } = useMarkdownFormatting(editorView)

useLinkTitles(editorView, { readonly })

Expand Down Expand Up @@ -65,32 +65,37 @@ defineExpose({

<template>
<div class="MarkdownEditorRoot">
<div class="toolbar" @pointerdown.prevent>
<div v-if="toolbar" class="toolbar" @pointerdown.prevent>
<slot name="toolbarLeft" />
<template v-if="!readonly">
<BlockTypeDropdown
@toggleHeader="toggleHeader($event)"
@toggleQuote="toggleQuote()"
@toggleList="toggleList($event)"
:modelValue="blockType.value ?? 'Unknown'"
@update:modelValue="blockType.set($event as BlockType)"
/>
<ToggleIcon
icon="italic"
:disabled="!editing || italic.value == null"
:modelValue="!!italic.value"
@update:modelValue="italic.set"
:disabled="!editing || !italic.set"
:modelValue="italic.value"
@update:modelValue="italic.set!"
/>
<ToggleIcon
icon="bold"
:disabled="!editing || bold.value == null"
:modelValue="!!bold.value"
@update:modelValue="bold.set"
:disabled="!editing || !bold.set"
:modelValue="bold.value"
@update:modelValue="bold.set!"
/>
<SvgButton
name="connector_add"
:disabled="!editing || !insertLink"
title="Insert link"
@click.stop="insertLink!"
/>
<SvgButton
name="code"
:disabled="!editing || !insertCodeBlock"
title="Insert code block"
@click.stop="insertCodeBlock!"
/>
</template>
<slot name="toolbarRight" />
</div>
Expand Down Expand Up @@ -227,7 +232,7 @@ defineExpose({
}
}

.list:not(.content) {
.list:not(*) {
/* Hide indentation spaces */
display: none;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,14 +47,14 @@ const content = computed(() => {
<thead>
<tr>
<th v-for="(cell, c) in content.headers" :key="c" class="cell">
<MarkdownEditorImpl :content="cell" />
<MarkdownEditorImpl :content="cell" :toolbar="false" />
</th>
</tr>
</thead>
<tbody class="tableBody">
<tr v-for="(row, r) in content.rows" :key="r" class="row">
<td v-for="(cell, c) in row" :key="c" class="cell">
<MarkdownEditorImpl :content="cell" />
<MarkdownEditorImpl :content="cell" :toolbar="false" />
</td>
</tr>
</tbody>
Expand Down
Loading
Loading