Skip to content

Commit c819573

Browse files
authored
Merge pull request #665 from code16/editor-code-block-lang
Allow to set code blocks language
2 parents 79f5d35 + b3c3acf commit c819573

File tree

13 files changed

+164
-13
lines changed

13 files changed

+164
-13
lines changed

resources/css/content.css

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,9 +61,14 @@
6161
@apply rounded bg-muted px-[0.3rem] py-[0.2rem] font-mono text-sm;
6262
}
6363
> :where(pre) {
64-
@apply mb-[1.25em] !whitespace-pre overflow-x-auto;
64+
@apply relative grid grid-cols-1 grid-rows-1 mb-[1.25em] !whitespace-pre overflow-x-auto;
65+
&[data-language]::after {
66+
@apply row-start-1 col-start-1 sticky left-0 p-2 content-[attr(data-language)] pointer-events-none text-muted-foreground text-xs;
67+
}
6568
> :where(code) {
66-
@apply block py-[.625em] px-[1.125em] bg-muted rounded-md [font-size:inherit];
69+
@apply block row-start-1 col-start-1 min-w-max in-data-language:pt-[calc(1em+1.25rem)] py-[1em] px-[1.25em] bg-muted rounded-md [font-size:inherit]
70+
transition-colors in-data-has-dropdown-open:in-data-textselected:outline-primary/50 outline outline-transparent -outline-offset-3
71+
;
6772
}
6873
}
6974
> :where(hr) {

resources/js/components/ui/dropdown-menu/DropdownMenu.vue

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ const forwarded = useForwardPropsEmits(props, emits)
88
</script>
99

1010
<template>
11-
<DropdownMenuRoot v-bind="forwarded">
12-
<slot />
11+
<DropdownMenuRoot v-bind="forwarded" v-slot="{ open }">
12+
<slot :open="open" />
1313
</DropdownMenuRoot>
1414
</template>
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<script setup lang="ts">
2+
import type { PopoverAnchorProps } from "reka-ui"
3+
import { PopoverAnchor } from "reka-ui"
4+
5+
const props = defineProps<PopoverAnchorProps>()
6+
</script>
7+
8+
<template>
9+
<PopoverAnchor
10+
data-slot="popover-anchor"
11+
v-bind="props"
12+
>
13+
<slot />
14+
</PopoverAnchor>
15+
</template>
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { default as Popover } from './Popover.vue'
2+
export { default as PopoverAnchor } from './PopoverAnchor.vue'
23
export { default as PopoverTrigger } from './PopoverTrigger.vue'
34
export { default as PopoverContent } from './PopoverContent.vue'

resources/js/components/ui/toggle/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { type VariantProps, cva } from 'class-variance-authority'
33
export { default as Toggle } from './Toggle.vue'
44

55
export const toggleVariants = cva(
6-
'inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground',
6+
'inline-flex gap-2 items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors hover:bg-muted hover:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground aria-pressed:bg-accent aria-pressed:text-accent-foreground',
77
{
88
variants: {
99
variant: {

resources/js/form/components/fields/editor/Editor.vue

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@
6161
import {
6262
getTextInputReplacementsExtension
6363
} from "@/form/components/fields/editor/extensions/TextInputReplacements";
64+
import CodeBlockDropdown from "@/form/components/fields/editor/toolbar/CodeBlockDropdown.vue";
6465
6566
const emit = defineEmits<FormFieldEmits<FormEditorFieldData>>();
6667
const props = defineProps<FormFieldProps<FormEditorFieldData>>();
@@ -77,6 +78,7 @@
7778
const el = useTemplateRef<HTMLDialogElement | HTMLDivElement>('el');
7879
const embedModal = ref<InstanceType<typeof EditorEmbedModal>>();
7980
const linkDropdown = ref<InstanceType<typeof LinkDropdown>>();
81+
const codeBlockDropdown = ref<InstanceType<typeof CodeBlockDropdown>>();
8082
8183
provide<ParentEditor>('editor', {
8284
props,
@@ -254,6 +256,7 @@
254256
? 'border-none rounded-none bg-transparent'
255257
: 'focus-within:ring-2 focus-within:ring-ring focus-within:ring-offset-2 focus-within:ring-offset-background'
256258
)"
259+
:data-has-dropdown-open="codeBlockDropdown?.open ? true : null"
257260
:data-fullscreen="isFullscreen ? true : null"
258261
ref="el"
259262
>
@@ -281,6 +284,9 @@
281284
<template v-else-if="button === 'table'">
282285
<TableDropdown v-bind="props" :editor="editor" />
283286
</template>
287+
<template v-else-if="button === 'code-block'">
288+
<CodeBlockDropdown v-bind="props" :editor="editor" :ref="(c) => codeBlockDropdown = c as InstanceType<typeof CodeBlockDropdown>" />
289+
</template>
284290
<template v-else-if="button.startsWith('embed:')">
285291
<Toggle
286292
size="sm"
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { CodeBlock as BaseCodeBlock } from '@tiptap/extension-code-block';
2+
import { Plugin } from "@tiptap/pm/state";
3+
import { PluginKey } from "prosemirror-state";
4+
import { Decoration, DecorationSet } from "prosemirror-view";
5+
6+
export const CodeBlock = BaseCodeBlock.extend({
7+
addProseMirrorPlugins() {
8+
return [
9+
...this.parent(),
10+
new Plugin({
11+
key: new PluginKey('codeBlockLanguageDecoration'),
12+
props: {
13+
decorations(state) {
14+
const decos: Decoration[] = [];
15+
16+
state.doc.descendants((node, pos) => {
17+
if (node.type.name === CodeBlock.name && node.attrs.language) {
18+
decos.push(
19+
Decoration.node(pos, pos + node.nodeSize, {
20+
'data-language': node.attrs.language
21+
})
22+
);
23+
}
24+
});
25+
26+
return DecorationSet.create(state.doc, decos);
27+
}
28+
},
29+
})
30+
]
31+
}
32+
})

resources/js/form/components/fields/editor/extensions/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import { TableRow } from '@tiptap/extension-table-row';
1818
import { TableHeader } from '@tiptap/extension-table-header';
1919
import { TableCell } from '@tiptap/extension-table-cell';
2020
import { Highlight } from '@tiptap/extension-highlight';
21-
import { CodeBlock } from '@tiptap/extension-code-block';
2221
import { Superscript } from '@tiptap/extension-superscript';
2322
import { OrderedList } from '@tiptap/extension-ordered-list';
2423
import { Link } from '@tiptap/extension-link';
@@ -28,7 +27,7 @@ import { Iframe } from './iframe/Iframe';
2827
import { Clipboard } from './Clipboard';
2928
import { Small } from './Small';
3029
import { FormEditorFieldData, FormEditorToolbarButton } from "@/types";
31-
30+
import { CodeBlock } from "@/form/components/fields/editor/extensions/CodeBlock";
3231

3332
export function getExtensions(field: FormEditorFieldData) {
3433
const toolbarHas = (buttonName: FormEditorToolbarButton | FormEditorToolbarButton[]) =>
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
<script setup lang="ts">
2+
import { __ } from "@/utils/i18n";
3+
import { Button } from '@/components/ui/button';
4+
import { Editor, getMarkRange, getMarkType } from "@tiptap/vue-3";
5+
import { ref } from "vue";
6+
import { Input } from "@/components/ui/input";
7+
import { Label } from "@/components/ui/label";
8+
import { Toggle } from "@/components/ui/toggle";
9+
import { FileCode, ChevronDown } from "lucide-vue-next";
10+
import { Popover, PopoverAnchor, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
11+
import { useId } from "@/composables/useId";
12+
import { FormFieldProps } from "@/form/types";
13+
import { FormEditorFieldData } from "@/types";
14+
15+
const props = defineProps<FormFieldProps<FormEditorFieldData> & {
16+
editor: Editor,
17+
}>();
18+
19+
const open = ref(false);
20+
const language = ref<string | null>(null);
21+
22+
props.editor.on('transaction', () => {
23+
language.value = props.editor.getAttributes('codeBlock')?.language;
24+
});
25+
26+
function onOpen() {
27+
}
28+
29+
function onHide() {
30+
props.editor.chain()
31+
.focus()
32+
.run();
33+
}
34+
35+
function onSubmit() {
36+
props.editor.chain().focus().updateAttributes('codeBlock', { language: language.value?.toLowerCase() }).run();
37+
}
38+
39+
function onToggleClick() {
40+
if(props.editor.isActive('codeBlock')) {
41+
open.value = true;
42+
} else {
43+
props.editor.chain().focus().toggleCodeBlock().run();
44+
}
45+
}
46+
47+
function onUntoggleCodeBlockClick() {
48+
if(props.editor.isActive('codeBlock')) {
49+
props.editor.chain().focus().toggleCodeBlock().run();
50+
} else {
51+
props.editor.chain().focus().run();
52+
}
53+
}
54+
55+
defineExpose({
56+
open,
57+
});
58+
</script>
59+
60+
<template>
61+
<Popover
62+
v-model:open="open"
63+
@update:open="$event ? onOpen() : onHide()"
64+
:modal="false"
65+
>
66+
<PopoverAnchor as-child>
67+
<Toggle
68+
:model-value="open || props.editor.isActive('codeBlock')"
69+
size="sm"
70+
:disabled="props.field.readOnly"
71+
:title="__('sharp::form.editor.toolbar.code_block.title')"
72+
@click="onToggleClick"
73+
>
74+
<FileCode class="size-4" />
75+
</Toggle>
76+
</PopoverAnchor>
77+
78+
<PopoverContent class="w-max">
79+
<form @submit.prevent="onSubmit()">
80+
<div class="flex gap-3 ">
81+
<Input class="flex-1 min-w-0 w-24" v-model="language" :placeholder="__('sharp::form.editor.dialogs.code_block.language_label')" />
82+
<Button type="submit">{{ __('sharp::modals.ok_button') }}</Button>
83+
</div>
84+
</form>
85+
<Button class="mt-3 w-full" variant="outline" size="sm" @click="onUntoggleCodeBlockClick">
86+
{{ __('sharp::form.editor.dialogs.code_block.toggle_off') }}
87+
</Button>
88+
</PopoverContent>
89+
</Popover>
90+
</template>

resources/js/form/components/fields/editor/toolbar/LinkDropdown.vue

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@
7979
}
8080
8181
defineExpose({
82-
open: () => { open.value = true; onOpen() },
82+
open,
8383
});
8484
</script>
8585

@@ -91,8 +91,7 @@
9191
>
9292
<PopoverTrigger as-child>
9393
<Toggle
94-
class="data-[state=open]:bg-accent"
95-
:class="props.editor.isActive('link') ? 'bg-accent' : ''"
94+
:model-value="open || props.editor.isActive('link')"
9695
size="sm"
9796
:disabled="props.field.readOnly"
9897
:title="__('sharp::form.editor.toolbar.link.title')"

0 commit comments

Comments
 (0)