mirror of
https://github.com/directus/directus.git
synced 2026-04-25 03:00:53 -04:00
Code Interface (#598)
* first draft * reworked to dynamically load modes.i think it works but im not totally sure tbh * style fixes * dynamic imports with some missing .d.ts files * not sure about binding of value * Update codemirror / types * Update code story * Add code interface translation strings * Fix typing issues, use correct translation * Update styling, fix tests * Set language on init Co-authored-by: rijkvanzanten <rijkvanzanten@me.com>
This commit is contained in:
59
src/interfaces/code/code.story.ts
Normal file
59
src/interfaces/code/code.story.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import withPadding from '../../../.storybook/decorators/with-padding';
|
||||
import { defineComponent, ref } from '@vue/composition-api';
|
||||
import { boolean, withKnobs, optionsKnob } from '@storybook/addon-knobs';
|
||||
import readme from './readme.md';
|
||||
import i18n from '@/lang';
|
||||
import RawValue from '../../../.storybook/raw-value.vue';
|
||||
import CodeMirror from 'codemirror';
|
||||
import 'codemirror/mode/meta';
|
||||
|
||||
const choices = {} as Record<string, string>;
|
||||
|
||||
CodeMirror.modeInfo.forEach((e) => (choices[e.name] = e.mode));
|
||||
|
||||
export default {
|
||||
title: 'Interfaces / Code',
|
||||
decorators: [withPadding, withKnobs],
|
||||
parameters: {
|
||||
notes: readme,
|
||||
},
|
||||
};
|
||||
|
||||
export const basic = () =>
|
||||
defineComponent({
|
||||
components: { RawValue },
|
||||
i18n,
|
||||
props: {
|
||||
lineNumber: {
|
||||
default: boolean('Line Number', false),
|
||||
},
|
||||
disabled: {
|
||||
default: boolean('Disabled', false),
|
||||
},
|
||||
language: {
|
||||
default: optionsKnob('Language', choices, 'markdown', {
|
||||
display: 'select',
|
||||
}),
|
||||
},
|
||||
},
|
||||
setup(props) {
|
||||
const value = ref(
|
||||
`# This is the editor.
|
||||
_It starts out in markdown mode_,
|
||||
**use the control below to load and apply a mode**
|
||||
"you'll see the highlighting of" this text *change*.`
|
||||
);
|
||||
return { value };
|
||||
},
|
||||
template: `
|
||||
<div :style="{
|
||||
maxWidth: '632px'
|
||||
}">
|
||||
<interface-code
|
||||
v-model="value"
|
||||
v-bind="{ lineNumber, disabled, language }"
|
||||
/>
|
||||
<raw-value>{{ value }}</raw-value>
|
||||
</div>
|
||||
`,
|
||||
});
|
||||
265
src/interfaces/code/code.vue
Normal file
265
src/interfaces/code/code.vue
Normal file
@@ -0,0 +1,265 @@
|
||||
<template>
|
||||
<div class="interface-code codemirror-custom-styles">
|
||||
<textarea ref="codemirrorEl" :value="stringValue" />
|
||||
|
||||
<v-button
|
||||
v-if="template"
|
||||
v-tooltip="$t('interfaces.code.fill_template')"
|
||||
@click="fillTemplate"
|
||||
>
|
||||
<v-icon name="playlist_add" />
|
||||
</v-button>
|
||||
|
||||
<small v-if="language" class="line-count type-note">
|
||||
{{
|
||||
$tc('loc', lineCount, {
|
||||
count: lineCount,
|
||||
lang: formatTitle(language),
|
||||
})
|
||||
}}
|
||||
</small>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import CodeMirror from 'codemirror';
|
||||
|
||||
import {
|
||||
defineComponent,
|
||||
computed,
|
||||
ref,
|
||||
onMounted,
|
||||
onUnmounted,
|
||||
watch,
|
||||
} from '@vue/composition-api';
|
||||
|
||||
import 'codemirror/mode/meta';
|
||||
import 'codemirror/addon/search/searchcursor.js';
|
||||
import 'codemirror/addon/search/matchesonscrollbar.js';
|
||||
import 'codemirror/addon/scroll/annotatescrollbar.js';
|
||||
import 'codemirror/addon/search/search.js';
|
||||
|
||||
import 'codemirror/addon/comment/comment.js';
|
||||
import 'codemirror/addon/dialog/dialog.js';
|
||||
import 'codemirror/keymap/sublime.js';
|
||||
|
||||
import formatTitle from '@directus/format-title';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
altOptions: {
|
||||
type: Object,
|
||||
default: null,
|
||||
},
|
||||
template: {
|
||||
type: [Object, Array],
|
||||
default: null,
|
||||
},
|
||||
lineNumber: {
|
||||
type: Boolean,
|
||||
default: null,
|
||||
},
|
||||
language: {
|
||||
type: String,
|
||||
default: 'javascript',
|
||||
},
|
||||
},
|
||||
setup(props, { emit }) {
|
||||
const codemirrorEl = ref<HTMLTextAreaElement>(null);
|
||||
const codemirror = ref<CodeMirror.EditorFromTextArea>(null);
|
||||
|
||||
onMounted(async () => {
|
||||
if (codemirrorEl.value) {
|
||||
const codemirrorElVal = codemirrorEl.value;
|
||||
|
||||
await getImports(cmOptions.value);
|
||||
codemirror.value = CodeMirror.fromTextArea(codemirrorElVal, cmOptions.value);
|
||||
codemirror.value.setValue(stringValue.value || props.template);
|
||||
await setLanguage();
|
||||
codemirror.value.on('change', (cm) => {
|
||||
const content = cm.getValue();
|
||||
emit('input', content);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
codemirror.value?.toTextArea();
|
||||
});
|
||||
|
||||
const stringValue = computed<string>(() => {
|
||||
if (props.value == null) return '';
|
||||
|
||||
if (typeof props.value === 'object') {
|
||||
return JSON.stringify(props.value, null, 4);
|
||||
}
|
||||
|
||||
return props.value;
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.language,
|
||||
() => {
|
||||
setLanguage();
|
||||
}
|
||||
);
|
||||
|
||||
async function setLanguage() {
|
||||
if (codemirror.value) {
|
||||
await import(`codemirror/mode/${props.language}/${props.language}.js`);
|
||||
codemirror.value.setOption('mode', props.language);
|
||||
}
|
||||
}
|
||||
|
||||
async function getImports(optionsObj: Record<string, any>): Promise<void> {
|
||||
const imports = [] as Promise<any>[];
|
||||
|
||||
if (optionsObj && optionsObj.size > 0) {
|
||||
if (optionsObj.styleActiveLine) {
|
||||
imports.push(import(`codemirror/addon/selection/active-line.js`));
|
||||
}
|
||||
|
||||
if (optionsObj.markSelection) {
|
||||
// @ts-ignore - @types/codemirror is missing this export
|
||||
imports.push(import(`codemirror/addon/selection/mark-selection.js`));
|
||||
}
|
||||
if (optionsObj.highlightSelectionMatches) {
|
||||
imports.push(import(`codemirror/addon/search/match-highlighter.js`));
|
||||
}
|
||||
if (optionsObj.autoRefresh) {
|
||||
imports.push(import(`codemirror/addon/display/autorefresh.js`));
|
||||
}
|
||||
if (optionsObj.matchBrackets) {
|
||||
imports.push(import(`codemirror/addon/edit/matchbrackets.js`));
|
||||
}
|
||||
if (optionsObj.hintOptions || optionsObj.showHint) {
|
||||
imports.push(import(`codemirror/addon/hint/show-hint.js`));
|
||||
// @ts-ignore - @types/codemirror is missing this export
|
||||
imports.push(import(`codemirror/addon/hint/show-hint.css`));
|
||||
// @ts-ignore - @types/codemirror is missing this export
|
||||
imports.push(import(`codemirror/addon/hint/javascript-hint.js`));
|
||||
}
|
||||
await Promise.all(imports);
|
||||
}
|
||||
}
|
||||
|
||||
const lineCount = computed(() => {
|
||||
if (codemirror.value) {
|
||||
return codemirror.value.lineCount();
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
const defaultOptions: CodeMirror.EditorConfiguration = {
|
||||
tabSize: 4,
|
||||
autoRefresh: true,
|
||||
indentUnit: 4,
|
||||
styleActiveLine: true,
|
||||
highlightSelectionMatches: { showToken: /\w/, annotateScrollbar: true, delay: 100 },
|
||||
hintOptions: {
|
||||
completeSingle: true,
|
||||
hint: () => undefined,
|
||||
},
|
||||
matchBrackets: true,
|
||||
showCursorWhenSelecting: true,
|
||||
theme: 'default',
|
||||
extraKeys: { Ctrl: 'autocomplete' },
|
||||
};
|
||||
|
||||
const cmOptions = computed<Record<string, any>>(() => {
|
||||
return Object.assign(
|
||||
{},
|
||||
defaultOptions,
|
||||
{
|
||||
lineNumbers: props.lineNumber,
|
||||
readOnly: props.disabled ? 'nocursor' : false,
|
||||
mode: props.language,
|
||||
},
|
||||
props.altOptions ? props.altOptions : {}
|
||||
);
|
||||
});
|
||||
|
||||
watch(
|
||||
() => props.altOptions,
|
||||
async (altOptions) => {
|
||||
if (!altOptions || altOptions.size === 0) return;
|
||||
await getImports(altOptions);
|
||||
for (const key in altOptions) {
|
||||
codemirror.value?.setOption(key as any, altOptions[key]);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.lineNumber,
|
||||
(lineNumber) => {
|
||||
codemirror.value?.setOption('lineNumbers', lineNumber);
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
cmOptions,
|
||||
lineCount,
|
||||
codemirrorEl,
|
||||
stringValue,
|
||||
fillTemplate,
|
||||
formatTitle,
|
||||
};
|
||||
|
||||
function fillTemplate() {
|
||||
if (props.template instanceof Object || props.template instanceof Array) {
|
||||
return emit('input', JSON.stringify(props.template, null, 4));
|
||||
}
|
||||
|
||||
try {
|
||||
emit('input', JSON.parse(props.template));
|
||||
} catch (e) {
|
||||
emit('input', props.template);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.interface-code {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 620px;
|
||||
font-size: 12px;
|
||||
&:focus {
|
||||
border-color: var(--primary-125);
|
||||
}
|
||||
}
|
||||
|
||||
.small {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: -20px;
|
||||
font-style: italic;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.v-button {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
z-index: 10;
|
||||
color: var(--primary);
|
||||
cursor: pointer;
|
||||
transition: color var(--fast) var(--transition-out);
|
||||
user-select: none;
|
||||
&:hover {
|
||||
color: var(--primary-125);
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
46
src/interfaces/code/index.ts
Normal file
46
src/interfaces/code/index.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { defineInterface } from '@/interfaces/define';
|
||||
import InterfaceCode from './code.vue';
|
||||
import CodeMirror from 'codemirror';
|
||||
import 'codemirror/mode/meta';
|
||||
|
||||
const choices = CodeMirror.modeInfo.map((e) => ({
|
||||
text: e.name,
|
||||
value: e.mode,
|
||||
}));
|
||||
|
||||
export default defineInterface(({ i18n }) => ({
|
||||
id: 'code',
|
||||
name: i18n.t('code'),
|
||||
icon: 'code',
|
||||
component: InterfaceCode,
|
||||
options: [
|
||||
{
|
||||
field: 'template',
|
||||
name: i18n.t('template'),
|
||||
width: 'full',
|
||||
interface: 'code',
|
||||
default_value: null,
|
||||
},
|
||||
{
|
||||
field: 'lineNumber',
|
||||
name: i18n.t('line_number'),
|
||||
width: 'half',
|
||||
interface: 'toggle',
|
||||
default_value: false,
|
||||
},
|
||||
{
|
||||
field: 'language',
|
||||
name: i18n.t('language'),
|
||||
width: 'half',
|
||||
interface: 'dropdown',
|
||||
options: choices,
|
||||
},
|
||||
{
|
||||
field: 'altOptions',
|
||||
name: i18n.t('alt_options'),
|
||||
width: 'full',
|
||||
interface: 'code',
|
||||
default_value: null,
|
||||
},
|
||||
],
|
||||
}));
|
||||
0
src/interfaces/code/readme.md
Normal file
0
src/interfaces/code/readme.md
Normal file
@@ -22,6 +22,7 @@ import InterfaceSlug from './slug';
|
||||
import InterfaceUser from './user';
|
||||
import InterfaceTags from './tags';
|
||||
import InterfaceRepeater from './repeater';
|
||||
import InterfaceCode from './code';
|
||||
import InterfaceFile from './file';
|
||||
import InterfaceCollections from './collections';
|
||||
|
||||
@@ -50,6 +51,7 @@ export const interfaces = [
|
||||
InterfaceUser,
|
||||
InterfaceTags,
|
||||
InterfaceRepeater,
|
||||
InterfaceCode,
|
||||
InterfaceFile,
|
||||
InterfaceCollections,
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user