Skip to content

Commit 251a191

Browse files
committed
Extract the Editor toolbar into a component
1 parent 734fe45 commit 251a191

File tree

2 files changed

+175
-11
lines changed

2 files changed

+175
-11
lines changed

components/CoreEditor.vue

Lines changed: 67 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script lang="ts">
2-
import { type Options } from 'ink-mde'
2+
import { type Instance, type Options } from 'ink-mde'
33
import Ink from 'ink-mde/vue'
44
import { OverlayScrollbars } from 'overlayscrollbars'
55
import { type SystemAppearance } from '#composables/useAppearance'
@@ -37,12 +37,13 @@ export default defineComponent({
3737
const cssMaxWidth = computed(() => `${maxWidthInChars.value}ch`)
3838
const instance = computed(() => ink.value?.instance)
3939
40-
const editorOptions = computed(() => {
40+
const editorOptions = computed<Options>(() => {
4141
return {
4242
...props.options,
4343
interface: {
4444
...props.options?.interface,
4545
appearance: editorAppearance.value,
46+
toolbar: false,
4647
},
4748
}
4849
})
@@ -75,11 +76,35 @@ export default defineComponent({
7576
}
7677
})
7778
79+
const formatSelection = (type: Parameters<Instance['format']>[0]) => {
80+
instance.value?.format(type, {})
81+
}
82+
83+
const handleUpload = async (event: Event) => {
84+
const target = event.target as HTMLInputElement
85+
86+
if (target?.files) {
87+
try {
88+
const url = await props.options?.files?.handler?.(target.files)
89+
90+
if (url) {
91+
const markup = `![](${url})`
92+
93+
instance.value?.insert(markup)
94+
}
95+
} catch (error) {
96+
console.error(error)
97+
}
98+
}
99+
}
100+
78101
return {
79102
cssMaxWidth,
80103
currentLayer,
81104
editorOptions,
82105
focus,
106+
formatSelection,
107+
handleUpload,
83108
ink,
84109
instance,
85110
nextLayer,
@@ -90,14 +115,36 @@ export default defineComponent({
90115
</script>
91116

92117
<template>
93-
<Ink
94-
ref="ink"
95-
:model-value="modelValue"
96-
:options="editorOptions"
97-
class="core-editor flex flex-col flex-grow flex-shrink rounded"
98-
:class="currentLayer.class"
99-
@update:model-value="$emit('update:modelValue', $event)"
100-
/>
118+
<div class="core-editor flex flex-col-reverse lg:flex-col flex-grow flex-shrink min-h-0 rounded">
119+
<template v-if="options?.interface?.toolbar">
120+
<CoreScrollable class="core-editor-scrollable flex-shrink-0 w-full mx-auto">
121+
<CoreEditorToolbar
122+
class="core-editor-toolbar mx-auto px-1 overflow-hidden lg:px-0 py-1 flex-shrink-0 rounded min-w-full"
123+
:upload="options?.toolbar?.upload"
124+
@blockquote="formatSelection('quote')"
125+
@bold="formatSelection('bold')"
126+
@bullet-list="formatSelection('list')"
127+
@code="formatSelection('code')"
128+
@heading="formatSelection('heading')"
129+
@image="formatSelection('image')"
130+
@italic="formatSelection('italic')"
131+
@link="formatSelection('link')"
132+
@number-list="formatSelection('ordered_list')"
133+
@task-list="formatSelection('task_list')"
134+
@upload="handleUpload"
135+
/>
136+
</CoreScrollable>
137+
<CoreDivider />
138+
</template>
139+
<Ink
140+
ref="ink"
141+
:model-value="modelValue"
142+
:options="editorOptions"
143+
class="flex flex-col flex-grow flex-shrink min-h-0 rounded overflow-hidden"
144+
:class="currentLayer.class"
145+
@update:model-value="$emit('update:modelValue', $event)"
146+
/>
147+
</div>
101148
</template>
102149

103150
<style scoped>
@@ -115,9 +162,14 @@ export default defineComponent({
115162
--ink-syntax-hashtag-background-color: rgb(v-bind('nextLayer.bgCssVar'));
116163
--ink-syntax-processing-instruction-color: rgb(v-bind('nextLayer.textCssVar') / 0.1);
117164
165+
.core-editor-scrollable {
166+
max-width: var(--core-editor-max-width-in-chars);
167+
}
168+
118169
:deep(.ink-mde) {
119170
border: none;
120171
border-radius: 0;
172+
padding: 0;
121173
122174
.cm-placeholder {
123175
color: var(--core-editor-text-muted);
@@ -129,7 +181,11 @@ export default defineComponent({
129181
130182
.ink-mde-details {
131183
background-color: transparent;
132-
border-top: 1px solid var(--core-editor-layer-2-bg);
184+
border-top: 1px solid var(--core-editor-divider);
185+
padding: 0.25rem;
186+
}
187+
188+
.ink-mde-editor {
133189
padding: 0.25rem;
134190
}
135191

components/CoreEditorToolbar.vue

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
<script lang="ts" setup>
2+
import {
3+
IconBlockquote,
4+
IconBold,
5+
IconCode,
6+
IconHeading,
7+
IconItalic,
8+
IconLink,
9+
IconList,
10+
IconListCheck,
11+
IconListNumbers,
12+
IconPhoto,
13+
IconUpload,
14+
} from '@tabler/icons-vue'
15+
16+
withDefaults(defineProps<{
17+
blockquote?: boolean,
18+
bold?: boolean,
19+
bulletList?: boolean,
20+
code?: boolean,
21+
heading?: boolean,
22+
image?: boolean,
23+
italic?: boolean,
24+
link?: boolean,
25+
numberList?: boolean,
26+
taskList?: boolean,
27+
upload?: boolean,
28+
}>(), {
29+
blockquote: true,
30+
bold: true,
31+
bulletList: true,
32+
code: true,
33+
heading: true,
34+
image: true,
35+
italic: true,
36+
link: true,
37+
numberList: true,
38+
taskList: true,
39+
upload: false,
40+
})
41+
42+
defineEmits<{
43+
blockquote: [],
44+
bold: [],
45+
bulletList: [],
46+
code: [],
47+
heading: [],
48+
image: [],
49+
italic: [],
50+
link: [],
51+
numberList: [],
52+
taskList: [],
53+
upload: [event: Event],
54+
}>()
55+
56+
const fileInput = ref<HTMLElement>()
57+
58+
const triggerUpload = () => {
59+
fileInput.value?.click()
60+
}
61+
</script>
62+
63+
<template>
64+
<div class="flex gap-1 lg:gap-4">
65+
<div v-if="heading || bold || italic" class="flex gap-1">
66+
<CoreButton v-if="heading" class="p-1 border border-layer" title="Heading" @click="$emit('heading')">
67+
<IconHeading class="sq-6" :stroke-width="1.25" />
68+
</CoreButton>
69+
<CoreButton v-if="bold" class="p-1 border border-layer" title="Bold" @click="$emit('bold')">
70+
<IconBold class="sq-6" :stroke-width="1.25" />
71+
</CoreButton>
72+
<CoreButton v-if="italic" class="p-1 border border-layer" title="Italic" @click="$emit('italic')">
73+
<IconItalic class="sq-6" :stroke-width="1.25" />
74+
</CoreButton>
75+
</div>
76+
<div v-if="blockquote || code" class="flex gap-1">
77+
<CoreButton v-if="blockquote" class="p-1 border border-layer" title="Blockquote" @click="$emit('blockquote')">
78+
<IconBlockquote class="sq-6" :stroke-width="1.25" />
79+
</CoreButton>
80+
<CoreButton v-if="code" class="p-1 border border-layer" title="Code" @click="$emit('code')">
81+
<IconCode class="sq-6" :stroke-width="1.25" />
82+
</CoreButton>
83+
</div>
84+
<div v-if="bulletList || numberList || taskList" class="flex gap-1">
85+
<CoreButton v-if="bulletList" class="p-1 border border-layer" title="Bullet List" @click="$emit('bulletList')">
86+
<IconList class="sq-6" :stroke-width="1.25" />
87+
</CoreButton>
88+
<CoreButton v-if="numberList" class="p-1 border border-layer" title="Number List" @click="$emit('numberList')">
89+
<IconListNumbers class="sq-6" :stroke-width="1.25" />
90+
</CoreButton>
91+
<CoreButton v-if="taskList" class="p-1 border border-layer" title="Task List" @click="$emit('taskList')">
92+
<IconListCheck class="sq-6" :stroke-width="1.25" />
93+
</CoreButton>
94+
</div>
95+
<div v-if="link || image || upload" class="flex gap-1">
96+
<CoreButton v-if="link" class="p-1 border border-layer" title="Link" @click="$emit('link')">
97+
<IconLink class="sq-6" :stroke-width="1.25" />
98+
</CoreButton>
99+
<CoreButton v-if="image" class="p-1 border border-layer" title="Image" @click="$emit('image')">
100+
<IconPhoto class="sq-6" :stroke-width="1.25" />
101+
</CoreButton>
102+
<CoreButton v-if="upload" class="p-1 border border-layer" title="Upload" @click="triggerUpload">
103+
<IconUpload class="sq-6" :stroke-width="1.25" />
104+
<input ref="fileInput" class="hidden" type="file" @change="$emit('upload', $event)">
105+
</CoreButton>
106+
</div>
107+
</div>
108+
</template>

0 commit comments

Comments
 (0)