diff options
Diffstat (limited to 'app/assets/javascripts/vue_shared/components/markdown')
7 files changed, 272 insertions, 12 deletions
diff --git a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue index 926034efd10..caec49c557a 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/apply_suggestion.vue @@ -51,6 +51,7 @@ export default { <gl-dropdown :text="dropdownText" :disabled="disabled" + size="small" boundary="window" right lazy diff --git a/app/assets/javascripts/vue_shared/components/markdown/field.vue b/app/assets/javascripts/vue_shared/components/markdown/field.vue index 32b3a0e22c2..657e4498b53 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/field.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/field.vue @@ -3,7 +3,7 @@ import { GlIcon, GlSafeHtmlDirective } from '@gitlab/ui'; import $ from 'jquery'; import '~/behaviors/markdown/render_gfm'; import { debounce, unescape } from 'lodash'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import GLForm from '~/gl_form'; import axios from '~/lib/utils/axios_utils'; import { stripHtml } from '~/lib/utils/text_utility'; @@ -272,7 +272,7 @@ export default { this.fetchMarkdown() .then((data) => this.renderMarkdown(data)) .catch(() => - createFlash({ + createAlert({ message: __('Error loading markdown preview'), }), ); @@ -315,7 +315,7 @@ export default { this.$nextTick() .then(() => $(this.$refs['markdown-preview']).renderGFM()) .catch(() => - createFlash({ + createAlert({ message: __('Error rendering Markdown preview'), }), ); diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue index 458dfe0ed23..89fffdedbfd 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue @@ -7,6 +7,8 @@ import { ITALIC_TEXT, STRIKETHROUGH_TEXT, LINK_TEXT, + INDENT_LINE, + OUTDENT_LINE, } from '~/behaviors/shortcuts/keybindings'; import { getSelectedFragment } from '~/lib/utils/common_utils'; import { s__, __ } from '~/locale'; @@ -68,12 +70,15 @@ export default { }, computed: { mdTable() { + const header = s__('MarkdownEditor|header'); + const divider = '-'.repeat(header.length); + const cell = ' '.repeat(header.length); + return [ - // False positive i18n lint: https://gitlab.com/gitlab-org/frontend/eslint-plugin-i18n/issues/26 - '| header | header |', // eslint-disable-line @gitlab/require-i18n-strings - '| ------ | ------ |', - '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings - '| cell | cell |', // eslint-disable-line @gitlab/require-i18n-strings + `| ${header} | ${header} |`, + `| ${divider} | ${divider} |`, + `| ${cell} | ${cell} |`, + `| ${cell} | ${cell} |`, ].join('\n'); }, mdSuggestion() { @@ -82,7 +87,8 @@ export default { ); }, mdCollapsibleSection() { - return ['<details><summary>Click to expand</summary>', `{text}`, '</details>'].join('\n'); + const expandText = s__('MarkdownEditor|Click to expand'); + return [`<details><summary>${expandText}</summary>`, `{text}`, '</details>'].join('\n'); }, isMac() { // Accessing properties using ?. to allow tests to use @@ -170,6 +176,8 @@ export default { italic: keysFor(ITALIC_TEXT), strikethrough: keysFor(STRIKETHROUGH_TEXT), link: keysFor(LINK_TEXT), + indent: keysFor(INDENT_LINE), + outdent: keysFor(OUTDENT_LINE), }, i18n: { writeTabTitle: __('Write'), @@ -235,6 +243,7 @@ export default { variant="confirm" category="primary" size="small" + data-qa-selector="dismiss_suggestion_popover_button" @click="handleSuggestDismissed" > {{ __('Got it') }} @@ -318,6 +327,32 @@ export default { icon="list-task" /> <toolbar-button + v-if="!restrictedToolBarItems.includes('indent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Indent line (%{modifierKey}])'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.indent" + command="indentLines" + icon="list-indent" + /> + <toolbar-button + v-if="!restrictedToolBarItems.includes('outdent')" + class="gl-display-none" + :button-title=" + /* eslint-disable @gitlab/vue-no-new-non-primitive-in-template */ + sprintf(s__('MarkdownEditor|Outdent line (%{modifierKey}[)'), { + modifierKey /* eslint-enable @gitlab/vue-no-new-non-primitive-in-template */, + }) + " + :shortcuts="$options.shortcuts.outdent" + command="outdentLines" + icon="list-outdent" + /> + <toolbar-button v-if="!restrictedToolBarItems.includes('collapsible-section')" :tag="mdCollapsibleSection" :prepend="true" diff --git a/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue new file mode 100644 index 00000000000..b38772d5aa5 --- /dev/null +++ b/app/assets/javascripts/vue_shared/components/markdown/markdown_editor.vue @@ -0,0 +1,216 @@ +<script> +import { GlSegmentedControl } from '@gitlab/ui'; +import { __ } from '~/locale'; +import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue'; +import axios from '~/lib/utils/axios_utils'; +import { EDITING_MODE_MARKDOWN_FIELD, EDITING_MODE_CONTENT_EDITOR } from '../../constants'; +import MarkdownField from './field.vue'; + +export default { + components: { + MarkdownField, + LocalStorageSync, + GlSegmentedControl, + ContentEditor: () => + import( + /* webpackChunkName: 'content_editor' */ '~/content_editor/components/content_editor.vue' + ), + }, + props: { + value: { + type: String, + required: true, + }, + renderMarkdownPath: { + type: String, + required: true, + }, + markdownDocsPath: { + type: String, + required: true, + }, + quickActionsDocsPath: { + type: String, + required: false, + default: '', + }, + uploadsPath: { + type: String, + required: false, + default: () => window.uploads_path, + }, + enableContentEditor: { + type: Boolean, + required: false, + default: true, + }, + formFieldId: { + type: String, + required: true, + }, + formFieldName: { + type: String, + required: true, + }, + enablePreview: { + type: Boolean, + required: false, + default: true, + }, + enableAutocomplete: { + type: Boolean, + required: false, + default: true, + }, + formFieldPlaceholder: { + type: String, + required: false, + default: '', + }, + formFieldAriaLabel: { + type: String, + required: false, + default: '', + }, + initOnAutofocus: { + type: Boolean, + required: false, + default: false, + }, + supportsQuickActions: { + 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 }) { + this.$emit('input', markdown); + }, + updateMarkdownFromMarkdownField({ target }) { + this.$emit('input', target.value); + }, + enableSwitchEditingControl() { + this.switchEditingControlEnabled = true; + }, + disableSwitchEditingControl() { + this.switchEditingControlEnabled = false; + }, + 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 }, + { text: __('Rich text'), value: EDITING_MODE_CONTENT_EDITOR }, + ], +}; +</script> +<template> + <div> + <div class="gl-display-flex gl-justify-content-start gl-mb-3"> + <gl-segmented-control + v-model="editingMode" + data-testid="toggle-editing-mode-button" + data-qa-selector="editing_mode_button" + class="gl-display-flex" + :options="$options.switchEditingControlOptions" + :disabled="!enableContentEditor || !switchEditingControlEnabled" + @change="onEditingModeChange" + /> + </div> + <local-storage-sync + v-model="editingMode" + storage-key="gl-wiki-content-editor-enabled" + @input="onEditingModeRestored" + /> + <markdown-field + v-if="!isContentEditorActive" + :markdown-preview-path="renderMarkdownPath" + can-attach-file + :enable-autocomplete="enableAutocomplete" + :textarea-value="value" + :markdown-docs-path="markdownDocsPath" + :quick-actions-docs-path="quickActionsDocsPath" + :uploads-path="uploadsPath" + :enable-preview="enablePreview" + class="bordered-box" + > + <template #textarea> + <textarea + :id="formFieldId" + ref="textarea" + :value="value" + :name="formFieldName" + class="note-textarea js-gfm-input js-autosize markdown-area" + dir="auto" + :data-supports-quick-actions="supportsQuickActions" + data-qa-selector="markdown_editor_form_field" + :aria-label="formFieldAriaLabel" + :placeholder="formFieldPlaceholder" + @input="updateMarkdownFromMarkdownField" + @keydown="$emit('keydown', $event)" + > + </textarea> + </template> + </markdown-field> + <div v-else> + <content-editor + :render-markdown="renderMarkdown" + :uploads-path="uploadsPath" + :markdown="value" + :autofocus="contentEditorAutofocus" + @change="updateMarkdownFromContentEditor" + @loading="disableSwitchEditingControl" + @loadingSuccess="enableSwitchEditingControl" + @loadingError="enableSwitchEditingControl" + @keydown="$emit('keydown', $event)" + /> + <input + :id="formFieldId" + :value="value" + :name="formFieldName" + data-qa-selector="markdown_editor_form_field" + type="hidden" + /> + </div> + </div> +</template> diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue index 7646a8718d6..855c7a449c4 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue @@ -139,7 +139,7 @@ export default { </script> <template> - <div class="md-suggestion-header border-bottom-0 gl-mt-3"> + <div class="md-suggestion-header border-bottom-0 gl-px-4 gl-py-3"> <div class="js-suggestion-diff-header gl-font-weight-bold"> {{ __('Suggested change') }} <a v-if="helpPagePath" :href="helpPagePath" :aria-label="__('Help')" class="js-help-btn"> @@ -162,6 +162,7 @@ export default { <gl-button class="btn-inverted js-remove-from-batch-btn btn-grouped" :disabled="isApplying" + size="small" @click="removeSuggestionFromBatch" > {{ __('Remove from batch') }} @@ -172,6 +173,7 @@ export default { class="btn-inverted js-add-to-batch-btn btn-grouped" data-qa-selector="add_suggestion_batch_button" :disabled="isDisableButton" + size="small" @click="addSuggestionToBatch" > {{ __('Add suggestion to batch') }} diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue index 9b81444fc04..30d72332c90 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/suggestions.vue @@ -1,7 +1,7 @@ <script> import { GlSafeHtmlDirective as SafeHtml } from '@gitlab/ui'; import Vue from 'vue'; -import createFlash from '~/flash'; +import { createAlert } from '~/flash'; import { __ } from '~/locale'; import SuggestionDiff from './suggestion_diff.vue'; @@ -91,7 +91,7 @@ export default { const suggestionElements = container.querySelectorAll('.js-render-suggestion'); if (this.lineType === 'old') { - createFlash({ + createAlert({ message: __('Unable to apply suggestions to a deleted line.'), parent: this.$el, }); diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue index 49217e38a1b..5ca21522d33 100644 --- a/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue +++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar_button.vue @@ -47,6 +47,11 @@ export default { required: false, default: 0, }, + command: { + type: String, + required: false, + default: '', + }, /** * A string (or an array of strings) of @@ -81,6 +86,7 @@ export default { :data-md-tag-content="tagContent" :data-md-prepend="prepend" :data-md-shortcuts="shortcutsString" + :data-md-command="command" :title="buttonTitle" :aria-label="buttonTitle" :icon="icon" |