Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue')
-rw-r--r--app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue276
1 files changed, 276 insertions, 0 deletions
diff --git a/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
new file mode 100644
index 00000000000..a9668ebdb69
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/bubble_menus/code_block_bubble_menu.vue
@@ -0,0 +1,276 @@
+<script>
+import {
+ GlDropdownForm,
+ GlFormInput,
+ GlDropdownDivider,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { getParentByTagName } from '~/lib/utils/dom_utils';
+import codeBlockLanguageLoader from '../../services/code_block_language_loader';
+import CodeBlockHighlight from '../../extensions/code_block_highlight';
+import Diagram from '../../extensions/diagram';
+import Frontmatter from '../../extensions/frontmatter';
+import EditorStateObserver from '../editor_state_observer.vue';
+import BubbleMenu from './bubble_menu.vue';
+
+const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+
+export default {
+ components: {
+ BubbleMenu,
+ GlDropdownForm,
+ GlFormInput,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlDropdownDivider,
+ GlSearchBoxByType,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor', 'contentEditor'],
+ data() {
+ return {
+ codeBlockType: undefined,
+ filterTerm: '',
+ filteredLanguages: [],
+
+ showCustomLanguageInput: false,
+ customLanguageType: '',
+
+ selectedLanguage: {},
+ isDiagram: false,
+ showPreview: false,
+ };
+ },
+ watch: {
+ filterTerm: {
+ handler(val) {
+ this.filteredLanguages = codeBlockLanguageLoader.filterLanguages(val);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ shouldShow: ({ editor }) => {
+ return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
+ },
+
+ async updateCodeBlockInfoToState() {
+ this.codeBlockType = CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type));
+
+ if (!this.codeBlockType) return;
+
+ const { language, isDiagram, showPreview } = this.tiptapEditor.getAttributes(
+ this.codeBlockType,
+ );
+ this.selectedLanguage = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(
+ language,
+ isDiagram,
+ );
+ this.isDiagram = isDiagram;
+ this.showPreview = showPreview;
+ },
+
+ getCodeBlockText() {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+ const node = getParentByTagName(view.domAtPos(from).node, 'pre');
+ return node?.textContent || '';
+ },
+
+ copyCodeBlockText() {
+ navigator.clipboard.writeText(this.getCodeBlockText());
+ },
+
+ togglePreview() {
+ this.showPreview = !this.showPreview;
+ this.tiptapEditor.commands.updateAttributes(Diagram.name, { showPreview: this.showPreview });
+ },
+
+ async applyLanguage(language) {
+ this.selectedLanguage = language;
+
+ await codeBlockLanguageLoader.loadLanguage(language.syntax);
+
+ this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
+ },
+
+ clearCustomLanguageForm() {
+ this.showCustomLanguageInput = false;
+ this.customLanguageType = '';
+ },
+
+ applyCustomLanguage() {
+ this.showCustomLanguageInput = false;
+
+ const language = codeBlockLanguageLoader.findOrCreateLanguageBySyntax(
+ this.customLanguageType,
+ );
+
+ this.applyLanguage(language);
+ },
+
+ getReferenceClientRect() {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+ const node = getParentByTagName(view.domAtPos(from).node, 'pre');
+ return node?.getBoundingClientRect() || new DOMRect(-1000, -1000, 0, 0);
+ },
+
+ deleteCodeBlock() {
+ this.tiptapEditor.chain().focus().deleteNode(this.codeBlockType).run();
+ },
+
+ tippyOptions() {
+ return { getReferenceClientRect: this.getReferenceClientRect.bind(this) };
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="code-block-bubble-menu"
+ class="gl-shadow gl-rounded-base gl-bg-white"
+ plugin-key="bubbleMenuCodeBlock"
+ :should-show="shouldShow"
+ :tippy-options="tippyOptions()"
+ >
+ <editor-state-observer @transaction="updateCodeBlockInfoToState">
+ <gl-button-group>
+ <gl-dropdown
+ category="tertiary"
+ contenteditable="false"
+ boundary="viewport"
+ :text="selectedLanguage.label"
+ @hide="clearCustomLanguageForm"
+ >
+ <template v-if="showCustomLanguageInput" #header>
+ <div class="gl-relative">
+ <gl-button
+ v-gl-tooltip
+ class="gl-absolute gl-mt-n3 gl-ml-2"
+ variant="default"
+ category="tertiary"
+ size="medium"
+ :aria-label="__('Go back')"
+ :title="__('Go back')"
+ icon="arrow-left"
+ @click.prevent.stop="showCustomLanguageInput = false"
+ />
+ <p
+ class="gl-text-center gl-new-dropdown-header-top gl-mb-0! gl-border-none! gl-pb-1!"
+ >
+ {{ __('Create custom type') }}
+ </p>
+ </div>
+ </template>
+ <template v-else #header>
+ <gl-search-box-by-type
+ v-model="filterTerm"
+ :clear-button-title="__('Clear')"
+ :placeholder="__('Search')"
+ />
+ </template>
+
+ <template v-if="!showCustomLanguageInput" #highlighted-items>
+ <gl-dropdown-item :key="selectedLanguage.syntax" is-check-item is-checked>
+ {{ selectedLanguage.label }}
+ </gl-dropdown-item>
+ </template>
+
+ <template v-if="!showCustomLanguageInput" #default>
+ <gl-dropdown-item
+ v-for="language in filteredLanguages"
+ v-show="selectedLanguage.syntax !== language.syntax"
+ :key="language.syntax"
+ @click="applyLanguage(language)"
+ >
+ {{ language.label }}
+ </gl-dropdown-item>
+ </template>
+ <template v-else #default>
+ <gl-dropdown-form @submit.prevent="applyCustomLanguage">
+ <div class="gl-mx-4 gl-mt-2 gl-mb-3">
+ <gl-form-input v-model="customLanguageType" :placeholder="__('Language type')" />
+ </div>
+ <gl-dropdown-divider />
+ <div class="gl-mx-4 gl-mt-3 gl-display-flex gl-justify-content-end">
+ <gl-button
+ variant="default"
+ size="medium"
+ category="primary"
+ class="gl-mr-2 gl-w-auto!"
+ @click.prevent.stop="showCustomLanguageInput = false"
+ >
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ variant="confirm"
+ size="medium"
+ category="primary"
+ type="submit"
+ class="gl-w-auto!"
+ >
+ {{ __('Apply') }}
+ </gl-button>
+ </div>
+ </gl-dropdown-form>
+ </template>
+
+ <template v-if="!showCustomLanguageInput" #footer>
+ <gl-dropdown-item
+ data-testid="create-custom-type"
+ @click.capture.native.stop="showCustomLanguageInput = true"
+ >
+ {{ __('Create custom type') }}
+ </gl-dropdown-item>
+ </template>
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="copy-code-block"
+ :aria-label="__('Copy code')"
+ :title="__('Copy code')"
+ icon="copy-to-clipboard"
+ @click="copyCodeBlockText"
+ />
+ <gl-button
+ v-if="isDiagram"
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ :class="{ active: showPreview }"
+ data-testid="preview-diagram"
+ :aria-label="__('Preview diagram')"
+ :title="__('Preview diagram')"
+ icon="eye"
+ @click="togglePreview"
+ />
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="tertiary"
+ size="medium"
+ data-testid="delete-code-block"
+ :aria-label="__('Delete code block')"
+ :title="__('Delete code block')"
+ icon="remove"
+ @click="deleteCodeBlock"
+ />
+ </gl-button-group>
+ </editor-state-observer>
+ </bubble-menu>
+</template>