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')
-rw-r--r--app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue146
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor.vue3
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue13
-rw-r--r--app/assets/javascripts/content_editor/components/loading_indicator.vue1
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/image.vue32
-rw-r--r--app/assets/javascripts/content_editor/components/wrappers/media.vue51
6 files changed, 214 insertions, 32 deletions
diff --git a/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue b/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue
new file mode 100644
index 00000000000..87f22a27856
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/code_block_bubble_menu.vue
@@ -0,0 +1,146 @@
+<script>
+import {
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ GlTooltipDirective as GlTooltip,
+} from '@gitlab/ui';
+import { BubbleMenu } from '@tiptap/vue-2';
+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';
+
+const CODE_BLOCK_NODE_TYPES = [CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+
+export default {
+ components: {
+ BubbleMenu,
+ GlButton,
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ GlSearchBoxByType,
+ EditorStateObserver,
+ },
+ directives: {
+ GlTooltip,
+ },
+ inject: ['tiptapEditor'],
+ data() {
+ return {
+ selectedLanguage: {},
+ filterTerm: '',
+ filteredLanguages: [],
+ };
+ },
+ watch: {
+ filterTerm: {
+ handler(val) {
+ this.filteredLanguages = codeBlockLanguageLoader.filterLanguages(val);
+ },
+ immediate: true,
+ },
+ },
+ methods: {
+ shouldShow: ({ editor }) => {
+ return CODE_BLOCK_NODE_TYPES.some((type) => editor.isActive(type));
+ },
+
+ getSelectedLanguage() {
+ const { language } = this.tiptapEditor.getAttributes(this.getCodeBlockType());
+
+ this.selectedLanguage = codeBlockLanguageLoader.findLanguageBySyntax(language);
+ },
+
+ async setSelectedLanguage(language) {
+ this.selectedLanguage = language;
+
+ await codeBlockLanguageLoader.loadLanguages([language.syntax]);
+
+ this.tiptapEditor.commands.setCodeBlock({ language: this.selectedLanguage.syntax });
+ },
+
+ tippyOnBeforeUpdate(tippy, props) {
+ if (props.getReferenceClientRect) {
+ // eslint-disable-next-line no-param-reassign
+ props.getReferenceClientRect = () => {
+ const { view } = this.tiptapEditor;
+ const { from } = this.tiptapEditor.state.selection;
+
+ for (let { node } = view.domAtPos(from); node; node = node.parentElement) {
+ if (node.nodeName?.toLowerCase() === 'pre') {
+ return node.getBoundingClientRect();
+ }
+ }
+
+ return new DOMRect(-1000, -1000, 0, 0);
+ };
+ }
+ },
+
+ deleteCodeBlock() {
+ this.tiptapEditor.chain().focus().deleteNode(this.getCodeBlockType()).run();
+ },
+
+ getCodeBlockType() {
+ return (
+ CODE_BLOCK_NODE_TYPES.find((type) => this.tiptapEditor.isActive(type)) ||
+ CodeBlockHighlight.name
+ );
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu
+ data-testid="code-block-bubble-menu"
+ class="gl-shadow gl-rounded-base"
+ :editor="tiptapEditor"
+ plugin-key="bubbleMenuCodeBlock"
+ :should-show="shouldShow"
+ :tippy-options="{ onBeforeUpdate: tippyOnBeforeUpdate }"
+ >
+ <editor-state-observer @transaction="getSelectedLanguage">
+ <gl-button-group>
+ <gl-dropdown contenteditable="false" boundary="viewport" :text="selectedLanguage.label">
+ <template #header>
+ <gl-search-box-by-type
+ v-model="filterTerm"
+ :clear-button-title="__('Clear')"
+ :placeholder="__('Search')"
+ />
+ </template>
+
+ <template #highlighted-items>
+ <gl-dropdown-item :key="selectedLanguage.syntax" is-check-item :is-checked="true">
+ {{ selectedLanguage.label }}
+ </gl-dropdown-item>
+ </template>
+
+ <gl-dropdown-item
+ v-for="language in filteredLanguages"
+ v-show="selectedLanguage.syntax !== language.syntax"
+ :key="language.syntax"
+ @click="setSelectedLanguage(language)"
+ >
+ {{ language.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ <gl-button
+ v-gl-tooltip
+ variant="default"
+ category="primary"
+ size="medium"
+ :aria-label="__('Delete code block')"
+ :title="__('Delete code block')"
+ icon="remove"
+ @click="deleteCodeBlock"
+ />
+ </gl-button-group>
+ </editor-state-observer>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index a942c9f1149..5b3f4f4ddf2 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -5,6 +5,7 @@ import ContentEditorAlert from './content_editor_alert.vue';
import ContentEditorProvider from './content_editor_provider.vue';
import EditorStateObserver from './editor_state_observer.vue';
import FormattingBubbleMenu from './formatting_bubble_menu.vue';
+import CodeBlockBubbleMenu from './code_block_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
import LoadingIndicator from './loading_indicator.vue';
@@ -16,6 +17,7 @@ export default {
TiptapEditorContent,
TopToolbar,
FormattingBubbleMenu,
+ CodeBlockBubbleMenu,
EditorStateObserver,
},
props: {
@@ -89,6 +91,7 @@ export default {
<top-toolbar ref="toolbar" class="gl-mb-4" />
<div class="gl-relative">
<formatting-bubble-menu />
+ <code-block-bubble-menu />
<tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
<loading-indicator />
</div>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
index 14a553ff30b..103079534bc 100644
--- a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -3,6 +3,10 @@ import { GlButtonGroup } from '@gitlab/ui';
import { BubbleMenu } from '@tiptap/vue-2';
import { BUBBLE_MENU_TRACKING_ACTION } from '../constants';
import trackUIControl from '../services/track_ui_control';
+import Code from '../extensions/code';
+import CodeBlockHighlight from '../extensions/code_block_highlight';
+import Diagram from '../extensions/diagram';
+import Frontmatter from '../extensions/frontmatter';
import ToolbarButton from './toolbar_button.vue';
export default {
@@ -16,6 +20,14 @@ export default {
trackToolbarControlExecution({ contentType, value }) {
trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
},
+
+ shouldShow: ({ editor, from, to }) => {
+ if (from === to) return false;
+
+ const exclude = [Code.name, CodeBlockHighlight.name, Diagram.name, Frontmatter.name];
+
+ return !exclude.some((type) => editor.isActive(type));
+ },
},
};
</script>
@@ -24,6 +36,7 @@ export default {
data-testid="formatting-bubble-menu"
class="gl-shadow gl-rounded-base"
:editor="tiptapEditor"
+ :should-show="shouldShow"
>
<gl-button-group>
<toolbar-button
diff --git a/app/assets/javascripts/content_editor/components/loading_indicator.vue b/app/assets/javascripts/content_editor/components/loading_indicator.vue
index 5b9383d6e11..620324adb06 100644
--- a/app/assets/javascripts/content_editor/components/loading_indicator.vue
+++ b/app/assets/javascripts/content_editor/components/loading_indicator.vue
@@ -30,6 +30,7 @@ export default {
>
<div
v-if="isLoading"
+ data-testid="content-editor-loading-indicator"
class="gl-w-full gl-display-flex gl-justify-content-center gl-align-items-center gl-absolute gl-top-0 gl-bottom-0"
>
<div class="gl-bg-white gl-absolute gl-w-full gl-h-full gl-opacity-3"></div>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/image.vue b/app/assets/javascripts/content_editor/components/wrappers/image.vue
deleted file mode 100644
index 5b81e5fddcc..00000000000
--- a/app/assets/javascripts/content_editor/components/wrappers/image.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
-import { GlLoadingIcon } from '@gitlab/ui';
-import { NodeViewWrapper } from '@tiptap/vue-2';
-
-export default {
- name: 'ImageWrapper',
- components: {
- NodeViewWrapper,
- GlLoadingIcon,
- },
- props: {
- node: {
- type: Object,
- required: true,
- },
- },
-};
-</script>
-<template>
- <node-view-wrapper class="gl-display-inline-block">
- <span class="gl-relative">
- <img
- data-testid="image"
- class="gl-max-w-full gl-h-auto"
- :title="node.attrs.title"
- :class="{ 'gl-opacity-5': node.attrs.uploading }"
- :src="node.attrs.src"
- />
- <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" />
- </span>
- </node-view-wrapper>
-</template>
diff --git a/app/assets/javascripts/content_editor/components/wrappers/media.vue b/app/assets/javascripts/content_editor/components/wrappers/media.vue
new file mode 100644
index 00000000000..37119bdd066
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/wrappers/media.vue
@@ -0,0 +1,51 @@
+<script>
+import { GlLoadingIcon } from '@gitlab/ui';
+import { NodeViewWrapper } from '@tiptap/vue-2';
+
+const tagNameMap = {
+ image: 'img',
+ video: 'video',
+ audio: 'audio',
+};
+
+export default {
+ name: 'MediaWrapper',
+ components: {
+ NodeViewWrapper,
+ GlLoadingIcon,
+ },
+ props: {
+ node: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ tagName() {
+ return tagNameMap[this.node.type.name] || 'img';
+ },
+ },
+};
+</script>
+<template>
+ <node-view-wrapper class="gl-display-inline-block">
+ <span class="gl-relative" :class="{ [`media-container ${tagName}-container`]: true }">
+ <gl-loading-icon v-if="node.attrs.uploading" class="gl-absolute gl-left-50p gl-top-half" />
+ <component
+ :is="tagName"
+ data-testid="media"
+ :class="{
+ 'gl-max-w-full gl-h-auto': tagName !== 'audio',
+ 'gl-opacity-5': node.attrs.uploading,
+ }"
+ :title="node.attrs.title || node.attrs.alt"
+ :alt="node.attrs.alt"
+ :src="node.attrs.src"
+ controls="true"
+ />
+ <a v-if="tagName !== 'img'" :href="node.attrs.canonicalSrc || node.attrs.src" @click.prevent>
+ {{ node.attrs.title || node.attrs.alt }}
+ </a>
+ </span>
+ </node-view-wrapper>
+</template>