Skip to content

Commit 587fbcc

Browse files
committed
refactor(app)!: use new component in main editor
1 parent 51b0cd0 commit 587fbcc

File tree

11 files changed

+343
-80
lines changed

11 files changed

+343
-80
lines changed

package-lock.json

Lines changed: 12 additions & 12 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/app/modules/block-editor/components/Block.vue

Lines changed: 36 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import { MarkdownNode } from '@language-kit/markdown'
33
import { useEditor } from '../composables/editor'
4+
import { syncRef, useFocusWithin, useKeyModifier } from '@vueuse/core'
45
56
// node model
67
const node = defineModel({
@@ -9,6 +10,12 @@ const node = defineModel({
910
})
1011
1112
// selected
13+
14+
const root = ref<HTMLElement | null>(null)
15+
16+
const { focused } = useFocusWithin(root)
17+
const isControlPressed = useKeyModifier('Control')
18+
1219
const editor = useEditor()
1320
1421
const selected = defineModel('selected', {
@@ -17,22 +24,33 @@ const selected = defineModel('selected', {
1724
local: true,
1825
})
1926
20-
watch(
21-
editor.selected,
22-
(value) => {
23-
selected.value = value.includes(node.value)
27+
const isSelectedInEditor = computed({
28+
get() {
29+
return editor.selected.some((n) => n.meta.id === node.value.meta.id)
2430
},
25-
{ immediate: true }
26-
)
31+
set(value) {
32+
if (value) return editor.select(node.value)
2733
28-
watch(selected, (value) => {
29-
if (value === null) return
34+
editor.unselect(node.value)
35+
},
36+
})
3037
31-
if (value) {
32-
return editor.select(node.value)
38+
syncRef(selected, isSelectedInEditor)
39+
40+
function onMousedown(e: MouseEvent) {
41+
if (e.ctrlKey) {
42+
editor.select(node.value)
43+
44+
return
3345
}
3446
35-
editor.unselect(node.value)
47+
editor.select(node.value, true)
48+
}
49+
50+
watch(focused, (value) => {
51+
if (value && !isControlPressed.value) {
52+
isSelectedInEditor.value = true
53+
}
3654
})
3755
3856
// menu
@@ -43,7 +61,12 @@ const icon = defineProp<string>('icon', {
4361
})
4462
</script>
4563
<template>
46-
<div>
64+
<div
65+
ref="root"
66+
class="flex min-h-[48px] items-center group hover:bg-b-secondary/50"
67+
:class="isSelectedInEditor ? 'bg-b-secondary/50' : ''"
68+
@mousedown="onMousedown"
69+
>
4770
<Teleport :to="`#${node.meta.toolbarId}`">
4871
<slot name="toolbar" />
4972
</Teleport>
@@ -59,7 +82,7 @@ const icon = defineProp<string>('icon', {
5982
</v-btn>
6083
</div>
6184

62-
<div class="flex-1">
85+
<div class="flex-1" :class="isControlPressed ? 'pointer-events-none' : ''">
6386
<slot />
6487
</div>
6588
</div>

packages/app/modules/block-editor/components/BlockParagraph.spec.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,27 +33,37 @@ describe('BlockParagraph', () => {
3333
it('should when html changes, emit update event with correct format', async () => {
3434
const spy = vi.fn()
3535

36+
const payload = createNodeParagraphFromHtml('Test <strong>bold</strong>')
37+
38+
payload.meta = { id: 'test', toolbarId: 'test-toolbar' }
39+
3640
component.mount({
3741
props: {
38-
'modelValue': createNodeParagraphFromHtml('Test <strong>bold</strong>'),
42+
'modelValue': payload,
3943
'onUpdate:model-value': spy,
4044
},
4145
})
4246

4347
const editable = findHTMLContentEditable()
4448

49+
const expected = createNodeParagraphFromHtml('Test <strong>bold</strong> update')
50+
51+
expected.meta = payload.meta
52+
4553
await editable.setValue('Test <strong>bold</strong> update')
4654

4755
expect(spy).toHaveBeenCalledOnce()
4856

49-
expect(spy).toHaveBeenCalledWith(
50-
createNodeParagraphFromHtml('Test <strong>bold</strong> update')
51-
)
57+
expect(spy).toHaveBeenCalledWith(expected)
5258
})
5359

5460
it.todo('should focus HTMLContentEditable when block is selected')
5561

5662
it.todo('should blur HTMLContentEditable area when block is selected')
5763

64+
it.todo('should always end block with a break line')
65+
66+
it.todo('should replace empty spaces with &nbsp; to avoid bugs')
67+
5868
it.todo('should emit update event when ToolbarTextFormat modifies the content')
5969
})
Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
<script setup lang="ts">
22
import { MarkdownNodeParagraph } from '@language-kit/markdown'
3+
import Block from './Block.vue'
34
import HTMLContentEditable from './HTMLContentEditable.vue'
45
import { createNodeParagraphFromHtml } from '../composables/helpers'
6+
import { TokenType } from '@language-kit/lexer'
7+
8+
defineOptions({
9+
inheritAttrs: false,
10+
})
511
612
const model = defineModel({
713
type: MarkdownNodeParagraph,
@@ -10,22 +16,39 @@ const model = defineModel({
1016
1117
const html = ref('')
1218
19+
const isEmpty = computed(() => {
20+
if (model.value.tokens.length > 3) return
21+
22+
return model.value.tokens.every((token) =>
23+
[TokenType.BreakLine, TokenType.EndOfFile].includes(token.type as any)
24+
)
25+
})
26+
1327
function load() {
14-
html.value = model.value.toHtml()
28+
html.value = model.value.toHtml().replaceAll(' ', '&nbsp;')
1529
}
1630
17-
function onHtmlUpdate(payload: string) {
18-
html.value = payload
31+
function update() {
32+
const node = createNodeParagraphFromHtml(html.value)
1933
20-
const node = createNodeParagraphFromHtml(payload)
34+
node.meta = model.value.meta
2135
2236
model.value = node
2337
}
2438
39+
function onBlur() {
40+
update()
41+
}
42+
2543
watch(model, load, { immediate: true })
2644
</script>
2745
<template>
28-
<div>
29-
<HTMLContentEditable :model-value="html" @update:model-value="onHtmlUpdate" />
30-
</div>
46+
<block v-model="model" :class="isEmpty ? 'hidden' : ''">
47+
<HTMLContentEditable
48+
v-model="html"
49+
@blur="onBlur"
50+
@keydown.enter.prevent="update"
51+
@keydown.ctrl.s="update"
52+
/>
53+
</block>
3154
</template>

packages/app/modules/block-editor/components/Editor.vue

Lines changed: 26 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { MarkdownNode } from '@language-kit/markdown'
77
88
const editor = useEditorOrCreate()
99
10+
const nodesReady = computed(() => {
11+
return editor.nodes.filter((node) => editor.toolbars.has(node.meta.id))
12+
})
13+
1014
function onNodeUpdate(node: MarkdownNode) {
1115
editor.update(node)
1216
}
@@ -15,18 +19,28 @@ function onNodeUpdate(node: MarkdownNode) {
1519
<div>
1620
<Toolbar />
1721

18-
<template v-for="n in editor.nodes" :key="n.meta.id">
19-
<BlockParagraph
20-
v-if="n.is('Paragraph')"
21-
:model-value="n"
22-
@update:model-value="onNodeUpdate"
23-
/>
22+
<div class="h-[calc(100%-48px)] w-full overflow-auto pb-80">
23+
<transition-group
24+
move-class="transition duration-200"
25+
enter-active-class="transition duration-200"
26+
leave-active-class="transition duration-200 absolute"
27+
enter-from-class="opacity-0"
28+
leave-to-class="opacity-0 translate-x-[-50%]"
29+
>
30+
<template v-for="n in nodesReady" :key="n.meta.id">
31+
<BlockParagraph
32+
v-if="n.is('Paragraph')"
33+
:model-value="n"
34+
@update:model-value="onNodeUpdate"
35+
/>
2436

25-
<Block v-else :model-value="n" data-test-id="invalid-block">
26-
<div class="text-danger">
27-
{{ $t('errors.errorRenderingBlock', [n.type]) }}
28-
</div>
29-
</Block>
30-
</template>
37+
<Block v-else :model-value="n" data-test-id="invalid-block">
38+
<div class="text-danger">
39+
{{ $t('errors.errorRenderingBlock', [n.type]) }}
40+
</div>
41+
</Block>
42+
</template>
43+
</transition-group>
44+
</div>
3145
</div>
3246
</template>

packages/app/modules/block-editor/components/HTMLContentEditable.vue

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,17 @@ const model = defineModel({
55
default: '',
66
})
77
8+
const emit = defineEmits(['blur'])
9+
810
const editableAreaRef = ref<HTMLElement>()
911
1012
const focused = ref(false)
1113
1214
function loadEditableArea() {
1315
if (!editableAreaRef.value) return
1416
17+
if (model.value === editableAreaRef.value.innerHTML) return
18+
1519
editableAreaRef.value.innerHTML = model.value
1620
}
1721
@@ -21,6 +25,12 @@ function onInput() {
2125
model.value = editableAreaRef.value?.innerHTML ?? ''
2226
}
2327
28+
function onBlur() {
29+
focused.value = false
30+
31+
emit('blur')
32+
}
33+
2434
watch(model, loadEditableArea)
2535
2636
onMounted(loadEditableArea)
@@ -67,16 +77,17 @@ function loadComponent() {
6777
watch([model, state], loadComponent, { immediate: true })
6878
</script>
6979
<template>
70-
<div>
80+
<div class="flex w-full">
7181
<component :is="instance" v-if="showView" data-test-id="view-area" />
7282
<div
7383
ref="editableAreaRef"
84+
:class="!focused && isDynamicRender ? 'opacity-0' : ''"
85+
class="outline-none transition-opacity min-h-[20px] w-full"
7486
data-test-id="editable-area"
7587
contenteditable="true"
76-
:class="!focused && isDynamicRender ? 'opacity-0' : ''"
7788
@input="onInput"
7889
@focus="focused = true"
79-
@blur="focused = false"
90+
@blur="onBlur"
8091
/>
8192
</div>
8293
</template>

0 commit comments

Comments
 (0)