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/content_editor.vue108
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_error.vue31
-rw-r--r--app/assets/javascripts/content_editor/components/content_editor_provider.vue24
-rw-r--r--app/assets/javascripts/content_editor/components/editor_state_observer.vue40
-rw-r--r--app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue67
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_button.vue58
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_image_button.vue19
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_link_button.vue110
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_table_button.vue50
-rw-r--r--app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue60
-rw-r--r--app/assets/javascripts/content_editor/components/top_toolbar.vue53
11 files changed, 429 insertions, 191 deletions
diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue
index 9a51def7075..a372233e543 100644
--- a/app/assets/javascripts/content_editor/components/content_editor.vue
+++ b/app/assets/javascripts/content_editor/components/content_editor.vue
@@ -1,45 +1,111 @@
<script>
-import { GlAlert } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2';
-import { ContentEditor } from '../services/content_editor';
+import { LOADING_CONTENT_EVENT, LOADING_SUCCESS_EVENT, LOADING_ERROR_EVENT } from '../constants';
+import { createContentEditor } from '../services/create_content_editor';
+import ContentEditorError from './content_editor_error.vue';
+import ContentEditorProvider from './content_editor_provider.vue';
+import EditorStateObserver from './editor_state_observer.vue';
+import FormattingBubbleMenu from './formatting_bubble_menu.vue';
import TopToolbar from './top_toolbar.vue';
export default {
components: {
- GlAlert,
+ GlLoadingIcon,
+ ContentEditorError,
+ ContentEditorProvider,
TiptapEditorContent,
TopToolbar,
+ FormattingBubbleMenu,
+ EditorStateObserver,
},
props: {
- contentEditor: {
- type: ContentEditor,
+ renderMarkdown: {
+ type: Function,
required: true,
},
+ uploadsPath: {
+ type: String,
+ required: true,
+ },
+ extensions: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ serializerConfig: {
+ type: Object,
+ required: false,
+ default: () => {},
+ },
},
data() {
return {
- error: '',
+ isLoadingContent: false,
+ focused: false,
};
},
- mounted() {
- this.contentEditor.tiptapEditor.on('error', (error) => {
- this.error = error;
+ created() {
+ const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this;
+
+ // This is a non-reactive attribute intentionally since this is a complex object.
+ this.contentEditor = createContentEditor({
+ renderMarkdown,
+ uploadsPath,
+ extensions,
+ serializerConfig,
});
+
+ this.contentEditor.on(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
+ this.contentEditor.on(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
+ this.contentEditor.on(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
+ this.$emit('initialized', this.contentEditor);
+ },
+ beforeDestroy() {
+ this.contentEditor.dispose();
+ this.contentEditor.off(LOADING_CONTENT_EVENT, this.displayLoadingIndicator);
+ this.contentEditor.off(LOADING_SUCCESS_EVENT, this.hideLoadingIndicator);
+ this.contentEditor.off(LOADING_ERROR_EVENT, this.hideLoadingIndicator);
+ },
+ methods: {
+ displayLoadingIndicator() {
+ this.isLoadingContent = true;
+ },
+ hideLoadingIndicator() {
+ this.isLoadingContent = false;
+ },
+ focus() {
+ this.focused = true;
+ },
+ blur() {
+ this.focused = false;
+ },
+ notifyChange() {
+ this.$emit('change', {
+ empty: this.contentEditor.empty,
+ });
+ },
},
};
</script>
<template>
- <div>
- <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="error = ''">
- {{ error }}
- </gl-alert>
- <div
- data-testid="content-editor"
- class="md-area"
- :class="{ 'is-focused': contentEditor.tiptapEditor.isFocused }"
- >
- <top-toolbar ref="toolbar" class="gl-mb-4" :content-editor="contentEditor" />
- <tiptap-editor-content class="md" :editor="contentEditor.tiptapEditor" />
+ <content-editor-provider :content-editor="contentEditor">
+ <div>
+ <editor-state-observer @docUpdate="notifyChange" @focus="focus" @blur="blur" />
+ <content-editor-error />
+ <div
+ data-testid="content-editor"
+ data-qa-selector="content_editor_container"
+ class="md-area"
+ :class="{ 'is-focused': focused }"
+ >
+ <top-toolbar ref="toolbar" class="gl-mb-4" />
+ <formatting-bubble-menu />
+ <div v-if="isLoadingContent" class="gl-w-full gl-display-flex gl-justify-content-center">
+ <gl-loading-icon size="sm" />
+ </div>
+ <tiptap-editor-content v-else class="md" :editor="contentEditor.tiptapEditor" />
+ </div>
</div>
- </div>
+ </content-editor-provider>
</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_error.vue b/app/assets/javascripts/content_editor/components/content_editor_error.vue
new file mode 100644
index 00000000000..031ea92a7e9
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_error.vue
@@ -0,0 +1,31 @@
+<script>
+import { GlAlert } from '@gitlab/ui';
+import EditorStateObserver from './editor_state_observer.vue';
+
+export default {
+ components: {
+ GlAlert,
+ EditorStateObserver,
+ },
+ data() {
+ return {
+ error: null,
+ };
+ },
+ methods: {
+ displayError({ error }) {
+ this.error = error;
+ },
+ dismissError() {
+ this.error = null;
+ },
+ },
+};
+</script>
+<template>
+ <editor-state-observer @error="displayError">
+ <gl-alert v-if="error" class="gl-mb-6" variant="danger" @dismiss="dismissError">
+ {{ error }}
+ </gl-alert>
+ </editor-state-observer>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/content_editor_provider.vue b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
new file mode 100644
index 00000000000..630aff9858f
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/content_editor_provider.vue
@@ -0,0 +1,24 @@
+<script>
+export default {
+ provide() {
+ // We can't use this.contentEditor due to bug in vue-apollo when
+ // provide is called in beforeCreate
+ // See https://github.com/vuejs/vue-apollo/pull/1153 for details
+ const { contentEditor } = this.$options.propsData;
+
+ return {
+ contentEditor,
+ tiptapEditor: contentEditor.tiptapEditor,
+ };
+ },
+ props: {
+ contentEditor: {
+ type: Object,
+ required: true,
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/content_editor/components/editor_state_observer.vue b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
new file mode 100644
index 00000000000..2eeb0719096
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/editor_state_observer.vue
@@ -0,0 +1,40 @@
+<script>
+import { debounce } from 'lodash';
+
+export const tiptapToComponentMap = {
+ update: 'docUpdate',
+ selectionUpdate: 'selectionUpdate',
+ transaction: 'transaction',
+ focus: 'focus',
+ blur: 'blur',
+ error: 'error',
+};
+
+const getComponentEventName = (tiptapEventName) => tiptapToComponentMap[tiptapEventName];
+
+export default {
+ inject: ['tiptapEditor'],
+ created() {
+ this.disposables = [];
+
+ Object.keys(tiptapToComponentMap).forEach((tiptapEvent) => {
+ const eventHandler = debounce((params) => this.handleTipTapEvent(tiptapEvent, params), 100);
+
+ this.tiptapEditor?.on(tiptapEvent, eventHandler);
+
+ this.disposables.push(() => this.tiptapEditor?.off(tiptapEvent, eventHandler));
+ });
+ },
+ beforeDestroy() {
+ this.disposables.forEach((dispose) => dispose());
+ },
+ methods: {
+ handleTipTapEvent(tiptapEvent, params) {
+ this.$emit(getComponentEventName(tiptapEvent), params);
+ },
+ },
+ render() {
+ return this.$slots.default;
+ },
+};
+</script>
diff --git a/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
new file mode 100644
index 00000000000..6c00480b87e
--- /dev/null
+++ b/app/assets/javascripts/content_editor/components/formatting_bubble_menu.vue
@@ -0,0 +1,67 @@
+<script>
+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 ToolbarButton from './toolbar_button.vue';
+
+export default {
+ components: {
+ BubbleMenu,
+ GlButtonGroup,
+ ToolbarButton,
+ },
+ inject: ['tiptapEditor'],
+ methods: {
+ trackToolbarControlExecution({ contentType, value }) {
+ trackUIControl({ action: BUBBLE_MENU_TRACKING_ACTION, property: contentType, value });
+ },
+ },
+};
+</script>
+<template>
+ <bubble-menu class="gl-shadow gl-rounded-base" :editor="tiptapEditor">
+ <gl-button-group>
+ <toolbar-button
+ data-testid="bold"
+ content-type="bold"
+ icon-name="bold"
+ editor-command="toggleBold"
+ category="primary"
+ size="medium"
+ :label="__('Bold text')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="italic"
+ content-type="italic"
+ icon-name="italic"
+ editor-command="toggleItalic"
+ category="primary"
+ size="medium"
+ :label="__('Italic text')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="strike"
+ content-type="strike"
+ icon-name="strikethrough"
+ editor-command="toggleStrike"
+ category="primary"
+ size="medium"
+ :label="__('Strikethrough')"
+ @execute="trackToolbarControlExecution"
+ />
+ <toolbar-button
+ data-testid="code"
+ content-type="code"
+ icon-name="code"
+ editor-command="toggleCode"
+ category="primary"
+ size="medium"
+ :label="__('Code')"
+ @execute="trackToolbarControlExecution"
+ />
+ </gl-button-group>
+ </bubble-menu>
+</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_button.vue b/app/assets/javascripts/content_editor/components/toolbar_button.vue
index 0af12812f3b..cdb877152d4 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_button.vue
@@ -1,23 +1,21 @@
<script>
import { GlButton, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
GlButton,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
+ inject: ['tiptapEditor'],
props: {
iconName: {
type: String,
required: true,
},
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
contentType: {
type: String,
required: true,
@@ -31,13 +29,31 @@ export default {
required: false,
default: '',
},
- },
- computed: {
- isActive() {
- return this.tiptapEditor.isActive(this.contentType) && this.tiptapEditor.isFocused;
+ variant: {
+ type: String,
+ required: false,
+ default: 'default',
},
+ category: {
+ type: String,
+ required: false,
+ default: 'tertiary',
+ },
+ size: {
+ type: String,
+ required: false,
+ default: 'small',
+ },
+ },
+ data() {
+ return {
+ isActive: null,
+ };
},
methods: {
+ updateActive({ editor }) {
+ this.isActive = editor.isActive(this.contentType) && editor.isFocused;
+ },
execute() {
const { contentType } = this;
@@ -51,15 +67,17 @@ export default {
};
</script>
<template>
- <gl-button
- v-gl-tooltip
- category="tertiary"
- size="small"
- class="gl-mx-2"
- :class="{ active: isActive }"
- :aria-label="label"
- :title="label"
- :icon="iconName"
- @click="execute"
- />
+ <editor-state-observer @transaction="updateActive">
+ <gl-button
+ v-gl-tooltip
+ :variant="variant"
+ :category="category"
+ :size="size"
+ :class="{ active: isActive }"
+ :aria-label="label"
+ :title="label"
+ :icon="iconName"
+ @click="execute"
+ />
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_image_button.vue b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
index ebeee16dbec..649e23c29aa 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_image_button.vue
@@ -8,9 +8,8 @@ import {
GlDropdownItem,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
-import { acceptedMimes } from '../extensions/image';
-import { getImageAlt } from '../services/utils';
+import { acceptedMimes } from '../services/upload_helpers';
+import { extractFilename } from '../services/utils';
export default {
components: {
@@ -24,12 +23,7 @@ export default {
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
- },
+ inject: ['tiptapEditor'],
data() {
return {
imgSrc: '',
@@ -47,7 +41,7 @@ export default {
.setImage({
src: this.imgSrc,
canonicalSrc: this.imgSrc,
- alt: getImageAlt(this.imgSrc),
+ alt: extractFilename(this.imgSrc),
})
.run();
@@ -64,7 +58,7 @@ export default {
this.tiptapEditor
.chain()
.focus()
- .uploadImage({
+ .uploadAttachment({
file: e.target.files[0],
})
.run();
@@ -73,7 +67,7 @@ export default {
this.emitExecute('upload');
},
},
- acceptedMimes,
+ acceptedMimes: acceptedMimes.image,
};
</script>
<template>
@@ -104,6 +98,7 @@ export default {
name="content_editor_image"
:accept="$options.acceptedMimes"
class="gl-display-none"
+ data-qa-selector="file_upload_field"
@change="onFileSelect"
/>
</gl-dropdown>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
index 8f57959a73f..ff525e52873 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_link_button.vue
@@ -8,10 +8,9 @@ import {
GlDropdownItem,
GlTooltipDirective as GlTooltip,
} from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
+import Link from '../extensions/link';
import { hasSelection } from '../services/utils';
-
-export const linkContentType = 'link';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
@@ -21,34 +20,32 @@ export default {
GlDropdownDivider,
GlDropdownItem,
GlButton,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
- },
+ inject: ['tiptapEditor'],
data() {
return {
linkHref: '',
+ isActive: false,
};
},
- computed: {
- isActive() {
- return this.tiptapEditor.isActive(linkContentType);
+ methods: {
+ resetFields() {
+ this.imgSrc = '';
+ this.$refs.fileSelector.value = '';
},
- },
- mounted() {
- this.tiptapEditor.on('selectionUpdate', ({ editor }) => {
- const { canonicalSrc, href } = editor.getAttributes(linkContentType);
+ openFileUpload() {
+ this.$refs.fileSelector.click();
+ },
+ updateLinkState({ editor }) {
+ const { canonicalSrc, href } = editor.getAttributes(Link.name);
+ this.isActive = editor.isActive(Link.name);
this.linkHref = canonicalSrc || href;
- });
- },
- methods: {
+ },
updateLink() {
this.tiptapEditor
.chain()
@@ -60,45 +57,70 @@ export default {
})
.run();
- this.$emit('execute', { contentType: linkContentType });
+ this.$emit('execute', { contentType: Link.name });
},
selectLink() {
const { tiptapEditor } = this;
// a selection has already been made by the user, so do nothing
if (!hasSelection(tiptapEditor)) {
- tiptapEditor.chain().focus().extendMarkRange(linkContentType).run();
+ tiptapEditor.chain().focus().extendMarkRange(Link.name).run();
}
},
removeLink() {
this.tiptapEditor.chain().focus().unsetLink().run();
- this.$emit('execute', { contentType: linkContentType });
+ this.$emit('execute', { contentType: Link.name });
+ },
+ onFileSelect(e) {
+ this.tiptapEditor
+ .chain()
+ .focus()
+ .uploadAttachment({
+ file: e.target.files[0],
+ })
+ .run();
+
+ this.resetFields();
+ this.$emit('execute', { contentType: Link.name });
},
},
};
</script>
<template>
- <gl-dropdown
- v-gl-tooltip
- :aria-label="__('Insert link')"
- :title="__('Insert link')"
- :toggle-class="{ active: isActive }"
- size="small"
- category="tertiary"
- icon="link"
- @show="selectLink()"
- >
- <gl-dropdown-form class="gl-px-3!">
- <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
- <template #append>
- <gl-button variant="confirm" @click="updateLink()">{{ __('Apply') }}</gl-button>
- </template>
- </gl-form-input-group>
- </gl-dropdown-form>
- <gl-dropdown-divider v-if="isActive" />
- <gl-dropdown-item v-if="isActive" @click="removeLink()">
- {{ __('Remove link') }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <editor-state-observer @transaction="updateLinkState">
+ <gl-dropdown
+ v-gl-tooltip
+ :aria-label="__('Insert link')"
+ :title="__('Insert link')"
+ :toggle-class="{ active: isActive }"
+ size="small"
+ category="tertiary"
+ icon="link"
+ @show="selectLink()"
+ >
+ <gl-dropdown-form class="gl-px-3!">
+ <gl-form-input-group v-model="linkHref" :placeholder="__('Link URL')">
+ <template #append>
+ <gl-button variant="confirm" @click="updateLink">{{ __('Apply') }}</gl-button>
+ </template>
+ </gl-form-input-group>
+ </gl-dropdown-form>
+ <gl-dropdown-divider />
+ <gl-dropdown-item v-if="isActive" @click="removeLink">
+ {{ __('Remove link') }}
+ </gl-dropdown-item>
+ <gl-dropdown-item v-else @click="openFileUpload">
+ {{ __('Upload file') }}
+ </gl-dropdown-item>
+
+ <input
+ ref="fileSelector"
+ type="file"
+ name="content_editor_attachment"
+ class="gl-display-none"
+ @change="onFileSelect"
+ />
+ </gl-dropdown>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
index 49d3006e9bf..46db806da94 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_table_button.vue
@@ -1,29 +1,23 @@
<script>
import { GlDropdown, GlDropdownDivider, GlDropdownForm, GlButton } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __, sprintf } from '~/locale';
import { clamp } from '../services/utils';
export const tableContentType = 'table';
-const MIN_ROWS = 3;
-const MIN_COLS = 3;
-const MAX_ROWS = 8;
-const MAX_COLS = 8;
+const MIN_ROWS = 5;
+const MIN_COLS = 5;
+const MAX_ROWS = 10;
+const MAX_COLS = 10;
export default {
components: {
+ GlButton,
GlDropdown,
GlDropdownDivider,
GlDropdownForm,
- GlButton,
- },
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
},
+ inject: ['tiptapEditor'],
data() {
return {
maxRows: MIN_ROWS,
@@ -68,22 +62,22 @@ export default {
};
</script>
<template>
- <gl-dropdown size="small" category="tertiary" icon="table">
- <gl-dropdown-form class="gl-px-3! gl-w-auto!">
- <div class="gl-w-auto!">
- <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
- <gl-button
- v-for="c of list(maxCols)"
- :key="c"
- :data-testid="`table-${r}-${c}`"
- :class="{ 'gl-bg-blue-50!': r <= rows && c <= cols }"
- :aria-label="getButtonLabel(r, c)"
- class="gl-display-inline! gl-px-0! gl-w-5! gl-h-5! gl-rounded-0!"
- @mouseover="setRowsAndCols(r, c)"
- @click="insertTable()"
- />
- </div>
- <gl-dropdown-divider />
+ <gl-dropdown size="small" category="tertiary" icon="table" class="table-dropdown">
+ <gl-dropdown-form class="gl-px-3!">
+ <div v-for="r of list(maxRows)" :key="r" class="gl-display-flex">
+ <gl-button
+ v-for="c of list(maxCols)"
+ :key="c"
+ :data-testid="`table-${r}-${c}`"
+ :class="{ 'active gl-bg-blue-50!': r <= rows && c <= cols }"
+ :aria-label="getButtonLabel(r, c)"
+ class="table-creator-grid-item gl-display-inline gl-rounded-0! gl-w-6! gl-h-6! gl-p-0!"
+ @mouseover="setRowsAndCols(r, c)"
+ @click="insertTable()"
+ />
+ </div>
+ <gl-dropdown-divider class="gl-my-3! gl-mx-n3!" />
+ <div class="gl-px-1">
{{ getButtonLabel(rows, cols) }}
</div>
</gl-dropdown-form>
diff --git a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
index 473fc472c1b..13728d4001d 100644
--- a/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
+++ b/app/assets/javascripts/content_editor/components/toolbar_text_style_dropdown.vue
@@ -1,29 +1,25 @@
<script>
import { GlDropdown, GlDropdownItem, GlTooltipDirective as GlTooltip } from '@gitlab/ui';
-import { Editor as TiptapEditor } from '@tiptap/vue-2';
import { __ } from '~/locale';
import { TEXT_STYLE_DROPDOWN_ITEMS } from '../constants';
+import EditorStateObserver from './editor_state_observer.vue';
export default {
components: {
GlDropdown,
GlDropdownItem,
+ EditorStateObserver,
},
directives: {
GlTooltip,
},
- props: {
- tiptapEditor: {
- type: TiptapEditor,
- required: true,
- },
+ inject: ['tiptapEditor'],
+ data() {
+ return {
+ activeItem: null,
+ };
},
computed: {
- activeItem() {
- return TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
- this.tiptapEditor.isActive(item.contentType, item.commandParams),
- );
- },
activeItemLabel() {
const { activeItem } = this;
@@ -31,6 +27,11 @@ export default {
},
},
methods: {
+ updateActiveItem({ editor }) {
+ this.activeItem = TEXT_STYLE_DROPDOWN_ITEMS.find((item) =>
+ editor.isActive(item.contentType, item.commandParams),
+ );
+ },
execute(item) {
const { editorCommand, contentType, commandParams } = item;
const value = commandParams?.level;
@@ -38,8 +39,8 @@ export default {
if (editorCommand) {
this.tiptapEditor
.chain()
- .focus()
[editorCommand](commandParams || {})
+ .focus()
.run();
}
@@ -56,20 +57,25 @@ export default {
};
</script>
<template>
- <gl-dropdown
- v-gl-tooltip="$options.i18n.placeholder"
- size="small"
- :disabled="!activeItem"
- :text="activeItemLabel"
- >
- <gl-dropdown-item
- v-for="(item, index) in $options.items"
- :key="index"
- is-check-item
- :is-checked="isActive(item)"
- @click="execute(item)"
+ <editor-state-observer @transaction="updateActiveItem">
+ <gl-dropdown
+ v-gl-tooltip="$options.i18n.placeholder"
+ size="small"
+ data-qa-selector="text_style_dropdown"
+ :disabled="!activeItem"
+ :text="activeItemLabel"
>
- {{ item.label }}
- </gl-dropdown-item>
- </gl-dropdown>
+ <gl-dropdown-item
+ v-for="(item, index) in $options.items"
+ :key="index"
+ is-check-item
+ :is-checked="isActive(item)"
+ data-qa-selector="text_style_menu_item"
+ :data-qa-text-style="item.label"
+ @click="execute(item)"
+ >
+ {{ item.label }}
+ </gl-dropdown-item>
+ </gl-dropdown>
+ </editor-state-observer>
</template>
diff --git a/app/assets/javascripts/content_editor/components/top_toolbar.vue b/app/assets/javascripts/content_editor/components/top_toolbar.vue
index fafc7a660e7..82a449ae6af 100644
--- a/app/assets/javascripts/content_editor/components/top_toolbar.vue
+++ b/app/assets/javascripts/content_editor/components/top_toolbar.vue
@@ -1,7 +1,5 @@
<script>
-import Tracking from '~/tracking';
-import { CONTENT_EDITOR_TRACKING_LABEL, TOOLBAR_CONTROL_TRACKING_ACTION } from '../constants';
-import { ContentEditor } from '../services/content_editor';
+import trackUIControl from '../services/track_ui_control';
import Divider from './divider.vue';
import ToolbarButton from './toolbar_button.vue';
import ToolbarImageButton from './toolbar_image_button.vue';
@@ -9,10 +7,6 @@ import ToolbarLinkButton from './toolbar_link_button.vue';
import ToolbarTableButton from './toolbar_table_button.vue';
import ToolbarTextStyleDropdown from './toolbar_text_style_dropdown.vue';
-const trackingMixin = Tracking.mixin({
- label: CONTENT_EDITOR_TRACKING_LABEL,
-});
-
export default {
components: {
ToolbarButton,
@@ -22,19 +16,9 @@ export default {
ToolbarImageButton,
Divider,
},
- mixins: [trackingMixin],
- props: {
- contentEditor: {
- type: ContentEditor,
- required: true,
- },
- },
methods: {
- trackToolbarControlExecution({ contentType: property, value }) {
- this.track(TOOLBAR_CONTROL_TRACKING_ACTION, {
- property,
- value,
- });
+ trackToolbarControlExecution({ contentType, value }) {
+ trackUIControl({ property: contentType, value });
},
},
};
@@ -45,7 +29,6 @@ export default {
>
<toolbar-text-style-dropdown
data-testid="text-styles"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<divider />
@@ -53,99 +36,91 @@ export default {
data-testid="bold"
content-type="bold"
icon-name="bold"
+ class="gl-mx-2"
editor-command="toggleBold"
:label="__('Bold text')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="italic"
content-type="italic"
icon-name="italic"
+ class="gl-mx-2"
editor-command="toggleItalic"
:label="__('Italic text')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="strike"
content-type="strike"
icon-name="strikethrough"
+ class="gl-mx-2"
editor-command="toggleStrike"
:label="__('Strikethrough')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="code"
content-type="code"
icon-name="code"
+ class="gl-mx-2"
editor-command="toggleCode"
:label="__('Code')"
- :tiptap-editor="contentEditor.tiptapEditor"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-link-button
- data-testid="link"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-link-button data-testid="link" @execute="trackToolbarControlExecution" />
<divider />
<toolbar-image-button
ref="imageButton"
data-testid="image"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="blockquote"
content-type="blockquote"
icon-name="quote"
+ class="gl-mx-2"
editor-command="toggleBlockquote"
:label="__('Insert a quote')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="code-block"
content-type="codeBlock"
icon-name="doc-code"
+ class="gl-mx-2"
editor-command="toggleCodeBlock"
:label="__('Insert a code block')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="bullet-list"
content-type="bulletList"
icon-name="list-bulleted"
+ class="gl-mx-2"
editor-command="toggleBulletList"
:label="__('Add a bullet list')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="ordered-list"
content-type="orderedList"
icon-name="list-numbered"
+ class="gl-mx-2"
editor-command="toggleOrderedList"
:label="__('Add a numbered list')"
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
<toolbar-button
data-testid="horizontal-rule"
content-type="horizontalRule"
icon-name="dash"
+ class="gl-mx-2"
editor-command="setHorizontalRule"
:label="__('Add a horizontal rule')"
- :tiptap-editor="contentEditor.tiptapEditor"
- @execute="trackToolbarControlExecution"
- />
- <toolbar-table-button
- :tiptap-editor="contentEditor.tiptapEditor"
@execute="trackToolbarControlExecution"
/>
+ <toolbar-table-button @execute="trackToolbarControlExecution" />
</div>
</template>
<style>