diff --git a/app/assets/javascripts/content_editor/components/content_editor.vue b/app/assets/javascripts/content_editor/components/content_editor.vue index 659c447e861..b5a88f97d83 100644 --- a/app/assets/javascripts/content_editor/components/content_editor.vue +++ b/app/assets/javascripts/content_editor/components/content_editor.vue @@ -3,7 +3,7 @@ import { EditorContent as TiptapEditorContent } from '@tiptap/vue-2'; import { __ } from '~/locale'; import { VARIANT_DANGER } from '~/flash'; import { createContentEditor } from '../services/create_content_editor'; -import { ALERT_EVENT } from '../constants'; +import { ALERT_EVENT, TIPTAP_AUTOFOCUS_OPTIONS } from '../constants'; import ContentEditorAlert from './content_editor_alert.vue'; import ContentEditorProvider from './content_editor_provider.vue'; import EditorStateObserver from './editor_state_observer.vue'; @@ -51,6 +51,12 @@ export default { required: false, default: '', }, + autofocus: { + type: [String, Boolean], + required: false, + default: false, + validator: (autofocus) => TIPTAP_AUTOFOCUS_OPTIONS.includes(autofocus), + }, }, data() { return { @@ -67,7 +73,7 @@ export default { }, }, created() { - const { renderMarkdown, uploadsPath, extensions, serializerConfig } = this; + const { renderMarkdown, uploadsPath, extensions, serializerConfig, autofocus } = this; // This is a non-reactive attribute intentionally since this is a complex object. this.contentEditor = createContentEditor({ @@ -75,6 +81,9 @@ export default { uploadsPath, extensions, serializerConfig, + tiptapOptions: { + autofocus, + }, }); }, mounted() { diff --git a/app/assets/javascripts/content_editor/constants/index.js b/app/assets/javascripts/content_editor/constants/index.js index 564cca23afa..96bf9a23cdc 100644 --- a/app/assets/javascripts/content_editor/constants/index.js +++ b/app/assets/javascripts/content_editor/constants/index.js @@ -66,3 +66,5 @@ export const SAFE_VIDEO_EXT = ['mp4', 'm4v', 'mov', 'webm', 'ogv']; export const SAFE_AUDIO_EXT = ['mp3', 'oga', 'ogg', 'spx', 'wav']; export const DIAGRAM_LANGUAGES = ['plantuml', 'mermaid']; + +export const TIPTAP_AUTOFOCUS_OPTIONS = [true, false, 'start', 'end', 'all']; diff --git a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue index bd3929c5f59..647cfd1c5b4 100644 --- a/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue +++ b/app/assets/javascripts/pages/shared/wikis/components/wiki_form.vue @@ -299,7 +299,7 @@ export default { :uploads-path="pageInfo.uploadsPath" :enable-content-editor="isMarkdownFormat" :enable-preview="isMarkdownFormat" - :autofocus="pageInfo.persisted" + :init-on-autofocus="pageInfo.persisted" :form-field-placeholder="$options.i18n.content.placeholder" :form-field-aria-label="$options.i18n.content.label" form-field-id="wiki_content" diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue index fd8496fa313..4f0fb7e1844 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -31,7 +31,8 @@ export default { }, uploadsPath: { type: String, - required: true, + required: false, + default: () => window.uploads_path, }, enableContentEditor: { type: Boolean, @@ -56,11 +57,6 @@ export default { required: false, default: true, }, - autofocus: { - type: Boolean, - required: false, - default: false, - }, formFieldPlaceholder: { type: String, required: false, @@ -71,17 +67,30 @@ export default { required: false, default: '', }, + initOnAutofocus: { + type: Boolean, + required: false, + default: false, + }, }, data() { return { editingMode: EDITING_MODE_MARKDOWN_FIELD, switchEditingControlEnabled: true, + autofocus: this.initOnAutofocus, }; }, computed: { isContentEditorActive() { return this.enableContentEditor && this.editingMode === EDITING_MODE_CONTENT_EDITOR; }, + contentEditorAutofocus() { + // Match textarea focus behavior + return this.autofocus ? 'end' : false; + }, + }, + mounted() { + this.autofocusTextarea(this.editingMode); }, methods: { updateMarkdownFromContentEditor({ markdown }) { @@ -99,9 +108,25 @@ export default { renderMarkdown(markdown) { return axios.post(this.renderMarkdownPath, { text: markdown }).then(({ data }) => data.body); }, + onEditingModeChange(editingMode) { + this.notifyEditingModeChange(editingMode); + this.enableAutofocus(editingMode); + }, + onEditingModeRestored(editingMode) { + this.notifyEditingModeChange(editingMode); + }, notifyEditingModeChange(editingMode) { this.$emit(editingMode); }, + enableAutofocus(editingMode) { + this.autofocus = true; + this.autofocusTextarea(editingMode); + }, + autofocusTextarea(editingMode) { + if (this.autofocus && editingMode === EDITING_MODE_MARKDOWN_FIELD) { + this.$refs.textarea.focus(); + } + }, }, switchEditingControlOptions: [ { text: __('Source'), value: EDITING_MODE_MARKDOWN_FIELD }, @@ -119,13 +144,13 @@ export default { class="gl-display-flex" :options="$options.switchEditingControlOptions" :disabled="!enableContentEditor || !switchEditingControlEnabled" - @change="notifyEditingModeChange" + @change="onEditingModeChange" />