diff options
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.vue | 276 |
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> |